Kombinácia read
/while
má ešte jednu skvelú možnosť. Za normálnych okolností sa do premenných načítavajú slová oddelené medzerou. V skutočnosti sa toto rozsekávanie riadku na položky riadi zabudovanou premennou shellu s názvom IFS
.
Názov je skratka z internal field separator (interný oddeľovač položiek) a štandardnou hodnotou je reťazec pozostávajúci z medzery, tabulátora a znaku pre nový riadok. Každý znak je použitý v úlohe oddeľovača.
Vyskúšajte si napríklad:
novotnyr@s:~$ printf "Ahoj\tkrasnyt\n\nsvet\n"
Ahoj krasnyt
svet
Skúsme to presmerovať do read
:
printf "Ahoj\tkrasny svet\n" | { read slovo1 slovo2 slovo3; echo $slovo1, $slovo2, $slovo3; }
Samozrejme, keďže celý príkaz beží v rúre (a teda v subshelli), výpis premenných musíme zoskupiť. Keďže skupina sa nachádza na jednom riadku, echo
musí byť ukončené bodkočiarkou.
Skúsme teraz zmeniť obsah premennej IFS
. Pozor však: na hodnote tejto premennej závisí množstvo funkcionality shellu (napr. sekanie príkazu na medzery). Jej svojvoľná a najmä globálna zmena môže mať ďalekosiahle nevyladiteľné dôsledky.
printf "Ahoj:krasny:svet" | { IFS=:; read slovo1 slovo2 slovo3; echo $slovo1, $slovo2, $slovo3; }
Zmenu teda urobíme len vo vnútri skupiny a hneď zistíme, že read
korektne rozsekol dvojbodkami oddelený reťazec na jednotlivé premenné.
Parsovanie /etc/passwd
Použime read
/while
na spracovanie /etc/passwd
! Vieme, že položky sú oddelené dvojbodkou. V takom prípade stačí zmeniť IFS
na dvojbodkou (ale pozor! opäť lokálne) a read
om načítavať položky.
Celé to vedie k tradičnému idiómu:
while IFS=: read ...
Ten zmení IFS
len pre účely read
u a zabráni podivným vedľajším efektom.
Celý skript tak môže vyzerať:
#/bin/sh
while IFS=: read U_LOGIN U_PASSWORD U_UID U_GID U_GECOS U_HOMEDIR U_SHELL
do
echo "Pouzivatel '$U_LOGIN' vyuziva '$U_SHELL'"
done < /etc/passwd
Premenné sme pre istotu pomenovali s prefixom U_
, pretože niektoré z nich sa môžu biť s preddefinovanými premennými (napr. v bashi je UID
premenná určená len na čítanie)-
Samozrejme, celý skript je ekvivalentný:
cut -d: -f1,7 /etc/passwd | tr ":" " " | xargs printf "Pouzivatel '%s' vyuziva '%s'\n"
Zábava s IFS
Ako zistíme obsah IFS
? Toto nepomôže:
echo $IFS
Výstupom totiž bude … nič viditeľné. (Nehovoriac o tom, že ešte pred samotným výpisom prebehne expanzia premennej, ktorá výsledok ovplyvní značne negatívnym spôsobom.
Môžeme ale použiť skvelý nástroj od
, ktorý vie zanalyzovať binárny vstup a vypísať ho v rozumnej podobe:
printf "$IFS" | od -ac
Výstupom bude
0000000 sp ht nl
\t \n
0000003
Namiesto echo
, ktoré pchá na výstup zbytočný koniec riadku sme použili printf
. Všimne si, že implicitná hodnota naozaj obsahuje medzeru sp
, tabulátor \t
, resp. ht
a znak nového riadka \n
, resp. nl
.
Ak chcete zmeniť IFS
a zároveň chcete použiť špeciálne znaky, toto nebude fungovať:
IFS="\t:"
Samozrejme, od -ac
vám bude tvrdiť, že všetko je v poriadku, vo vnútri sa vraj nachádza tabulátor a dvojbodka, ale to všetko len vďaka mechanizmu expanzie shellu:
printf "$IFS" | od -ac
expanduje na:
printf "<tabulator><dvojbodka>" | od -ac
čo povedie k zdanlivému korektnému výsledku.
V skutočnosti ste len povedali, že premenná IFS
obsahuje lomku, znak malého „t“ a dvojbodku. Overte si dĺžku reťazca!
(IFS="\t:"; echo ${#IFS})
Zistíte, že výsledok je:
3
Korektná verzia je napr:
IFS=" :"
pričom pred dvojbodkou je ručne zadaný znak tabulátora. Samozrejme, nie je to ktovieako prehľadné. Analogicky sa používa
IFS="
:"
To znamená, že IFS
bude obsahovať znak nového riadka a dvojbodku. Funkčné, ale divné.
Zčitateľniť to môžeme využitím subshellu a printf
:
IFS=$(printf "\t:")
Ukážkový skript je:
printf "dog:pes\tstekajuce zviera" | (IFS=$(printf ":\t") read EN SK COMMENT; echo "$EN->$SK ($COMMENT)")
Ten vypíš korektne
dog->pes (stekajuce zviera)