Shellové skriptovanie: `while`/`read` a zábava s IFS

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 readom načítavať položky.

Celé to vedie k tradičnému idiómu:

while IFS=: read ...

Ten zmení IFS len pre účely readu 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)

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *