Skriptovanie v shelli: Prečo nevidím zmeny v premenných vo vnútri cyklu?

Folklórny príklad: chcete si v shellovom skripte spočítať počet riadkov súboru, čiže urobiť si vlastnú verziu wc -l.

Ako riešenie vám napadne:

#!/bin/sh   
POCET=0

cat $1 | while read
do
        POCET=$(($POCET + 1))
done

echo $POCET

Skript sa zavolá cez

./wc.sh /etc/passwd

Lenže výsledkom je…

0

Prečo? Veď /etc/passwd určite nemá nula riadkov!

Problém je dostatočne popísaný vo FAQ k Bashu. Jadro spočíva v správaní shellu: príkaz cat $1 | while ... predstavuje rúru (pipeline), ktorá je spúšťaná v podshelli (subshell). Shell spustí rúru v separátnom detskom procese, ktorý zdedí premenné, popisovače súborov, premenné prostredia atď. Lenže zmeny v týchto premenných v rámci podshellu sa po dobehnutí subshellu samozrejme stratia.

V našom príklade sa premenná POCET mení v rámci subshellu, lenže príkaz echo $POCET je opäť súčasťou rodičovského shellu. Táto premenná teda nebola nijak ovplyvnená zmenami v subshelli a na výstupe sa teda ocitne korektná hodnota: 0.

Riešenia?

Presmerovanie miesto cat

Jedno z riešení je nahradiť zbytočnú kombináciu cat / while read jednoduchým presmerovaním:

cat $1 | while read
do
        ...
done < $1

To funguje všade, s výnimkou shellu Bourne shellu (sh), kde je možné obísť túto vlastnosť trikom s execom a deskriptormi súborov.

Zoskupenie príkazov

Prostredie { ... } slúži na zoskupovanie príkazov (command grouping). Postupnosť príkazov medzi kučeravými zátvorkami sa spustí v aktuálnom shelli, čo možno použiť v kombinácii s presmerovaním na všakovaké triky.

POCET=0

cat $1 | { 
    while read
        do
            POCET=$(($POCET + 1))
    done

    echo $POCET
}

Celá rúra sa samozrejme spustí v podshelli. V tomto prípade sme však zoskupili while a echo a teda ešte pred dobehnutím subshellu vidíme správnu hodnotu premennej POCET. Samozrejme, v rámci rodiča je premenná POCET nezmenená.

Pridaj komentár

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