Shellové skriptovanie: zoskupovanie príkazov

Jeden príkaz

Najjednoduchšou formou je jeden príkaz:

echo "Ahoj svet"

V tomto prípade nie je čo riešiť: príkaz zbehne v aktuálnom shelli a exit code závisí na tom, či zbehol v poriadku alebo nie.

Sekvencie

Ak chceme spustiť viacero príkazov, vieme to spraviť na dvoch riadkoch:

echo "Ahoj svet" > pozdrav.txt
cat pozdrav.txt

Príkazy sa spustia sekvenčne, a nezávisle od seba. Každý príkaz sa vykoná bez ohľadu na to, či príkazy pred ním uspeli alebo nie.

Úplne ekvivalentný zápis možno dosiahnuť použitím sekvencie (sequential list), kde príkazy jednoducho oddelíme bodkočiarkami.

echo "Ahoj svet" > pozdrav.txt; cat pozdrav.txt

V tomto prípade je exit code rovný kódu posledného príkazu v sekvencii.

Skúsme zistiť, či existuje používateľ Admin a ak áno, tak ho vypíšme do estetického prostredia:

grep ^Admin /etc/passwd; echo "--------------"

Bez ohľadu na to, či používateľ existuje alebo nie, bude exit code rovný 0, pretože echo nezlyhá.

Horšie je, že v prípade neexistujúceho používateľa sa vypíše:

--------------

Toto budeme musiet vyriešiť inak: pomocou AND zoznamov, o ktorých si povieme nižšie.

Zoskupené príkazy

Niekedy sa stane, že chceme sekvenčne vykonať viacero príkazov, ale zároveň chceme, aby mohli spoločne presmerovať výstup. Skúsme si to ukázať na nesprávnom príklade:

echo "# Pouzivatelia" >> pouzivatelia.txt
cat /etc/passwd
echo "--------" >> pouzivatelia.txt

Zapísané na jednom riadku:

echo "# Pouzivatelia" >> pouzivatelia.txt; cat /etc/passwd; echo "--------" >> pouzivatelia.txt

Vidíme, že súbor pouzivatelia.txt sa používa dvakrát. Na rozdiel od štúdia však neplatí, že opakovanie je matka múdrosti a preto by sme sa ho mohli zbaviť.

Namiesto toho môžeme použiť alternatívny zápis: zoskupíme všetky príkazy a následne môžeme výstup z celej skupiny presmerovať do spoločného súboru.

Skupina príkazov je uvedená v prostredí { ... }, čo je podobné syntaxi jazyka C. Pre účely presmerovania sa tvári ako jeden príkaz: všetko, čo sa zapíše do štandardného výstupu (bez ohľadu na príkaz), sa presmeruje do súboru pouzivatelia.txt.

{
    echo "# Pouzivatelia"
    cat /etc/passwd
    echo "--------"
} > pouzivatelia.txt

Ak to uvádzame na jeden riadok, pozor na zádrheľ: pred poslednou zátvorkou } musí byť buď nový riadok alebo bodkočiarka: v tomto prípade ju musíme dať za posledné echo:

{ echo "# Pouzivatelia"; cat /etc/passwd; echo "--------"; } > pouzivatelia.txt

Skupiny a presmerovanie vstupu

Finta so skupinou a presmerovaním sa dá použiť aj v prípade presmerovania vstupu, čo sa hodí pri čítaní riadkov zo súboru.

Najprv si vyrobme ukážkový súbor:

printf "%s\n" "# Beatles" John Paul Ringo George > beatles.txt

Jeho obsahom bude:

# Beatles
John
Paul
Ringo
George

Ak chceme preskočiť prvý riadok a všetky ostatné zmeniť na veľké písmená, môžeme urobiť toto:

tail -n+2 < beatles.txt | tr '[:lower:]' '[:upper:]'

V skripte sa však môže hodiť programové spracovanie, kde s jedným riadkom chceme urobiť viacero rozličných vecí, na čo rúra už nebude stačiť.

Ako sme už spomínali, skupina príkazov v {...} sa tvári ako jeden príkaz, čo môžeme využiť pri načítavaní dát zo štandardného vstupu. Nasledovný príkaz je úplne ekvivalentný:

{ 
    read
    tr '[:lower:]' '[:upper:]'
} < beatles.txt

Prvý read načíta riadok zo štandardného vstupu, do ktorého sme presmerovali obsah súboru beatles.txt. Tým riadkom bude #Beatles. Nasledujúci tr pokračuje v čítaní zo štandardného vstupu počnúc prvým riadkom, kde každý riadok vhodným spôsobom spracuje.

Jednoriadkový variant:

{ read; tr '[:lower:]' '[:upper:]'; } < beatles.txt

Skupina príkazov beží v aktuálnom shelli

Skupina príkazov v {...} beží v rámci aktuálneho shellu. To je rozdiel oproti rúre, ktorá sa vykoná v subshelli. Dôsledok je zjavný: zmeny premenných v rámci skupiny sa prejavia aj po jej dobehnutí, zatiaľčo zmeny premenných v rámci behu rúry sú len „lokálne”.

Popis tohto problému je zmienený v samostatnom článku.

Zoskupené príkazy

Kamarátske prostredie k {...} je prostredie (...), ktoré takisto predstavuje zoskupovanie príkazov. V tomto prípade sa však príkazy spustia v subshelli, teda v samostatnom procese, ktorý je potomkovským procesom shellu (zdedí hodnoty premenných prostredia, hodnoty štandardných premenných a ďalšie vlastnosti).

Charakteristickou črtou je fakt, že zmeny obsahov premenných v rámci subshellu neovplyvnia obsahy premenných rodičovského shellu:

(HOME='/root'; echo "$HOME"; ); echo $HOME

Výpisom bude:

/root
/home/novotnyr

V samostatnom subshelli sme priradili do premennej HOME novú hodnotu a vypísali sme ju. Rozsah platnosti zmeny (scope) však siaha len po chvíľu, kým subshell nedobehne.

Po dobehnutí subshellu sa spustilo druhé echo (teraz v rámci aktuálneho shellu), ktoré vypísalo pôvodnú, nezmenenú hodnotu premennej HOME.

Asynchrónne spúšťanie

Ak ste klasickí windowsáci, čo sa k vzdialenému shellu pripájate cez Putty, a máte pocit, že shell nezvláda multitasking, nie je to pravda. Už od nepamäti máte možnosť spúšťať dlhotrvajúce úlohy asynchrónne, teda na pozadí: to znamená, že po ich spustení nebude shell čakať na dobehnutie, ale rovno vám umožní ďalšiu prácu.

Zoberme si hlúpy príklad, ktorý vygeneruje súbor so stomiliónom náhodných bajtov:

head -c 100000000 < /dev/urandom > random.dat

Beh tohto príkazu chvíľu potrvá nejaký čas, počas ktorého nemôžeme pracovať so shellom. (OK, môžeme, môžeme si otvoriť ďalší terminál / session v Putty).

Asynchrónne spúšťanie skriptu dosiahneme uvedením ampersandu:

head -c 100000000 < /dev/urandom > random.dat &

Po spustení sa vypíšu dve čísla: prvé udáva číslo asynchrónne spustenej úlohy a druhé je číslo procesu, v rámci ktorého úloha beží. Nasledovný výpis indikuje prvú úlohu spustenú na pozadí s ID procesu 11227:

[1] 11227

Po tomto výpise môžeme ihneď ďalej pracovať so shellom.

Ak úloha dobehne, shell nás na to upozorní (nie však hneď, ale až potom, čo doň odošleme prvý príkaz vykonaný po dobehnutí úlohy). Tvar výpisu je:

[1]+  Done                    head -c 100000000 < /dev/urandom > random.dat

Úlohy na pozadí a štandardný výstup

Príkaz, alebo skupinu príkazov možno spustiť na pozadí (asynchrónne) tým, že ich ukončíme ampersandom.

Ukážkou stiahneme na pozadí veľký súbor z webu:

 wget -nv http://sk.freebsd.org/pub/apache/dist/tomcat/tomcat-7/v7.0.34/bin/apache-tomcat-7.0.34.tar.gz &

Ak viacero príkazov oddelíme ampersandami, každý z nich sa spustí v separátnej úlohe na pozadí. Spustíme tri nezmyselné úlohy (pozor na finálny ampersand!):

date & date & date &

To je ekvivalentné s:

(date & date & date) &

Príkazy spúšťané na pozadí sa totiž v každom prípade spúšťajú v subshelli.

Príkazy však môžeme zoskupovať, ak chceme, aby zdieľali spoločný výstup: v tomto prípade sa na pozadí zapíše do jedného súboru trikrát sekvencia čísiel od 1 po 10.

(seq 10; seq 10; seq 10;) > ten.txt &

Zoznamy AND a OR

V sekcii o testoch sme spomínali, že množstvo vlastností záleží na exit code jednotlivých príkazov. To sa dá využívať aj pri vytváraní AND, resp. OR zoznamov, ktoré umožnia nahradiť podmienky if/test skrátenou verziou.

Zoznam AND

Zoznam AND je sekvencia príkazov oddelená dvojitými ampersandmi:

príkaz1 && príkaz2 && príkaz3

Na rozdiel od klasickej sekvencie sa príkaz2 spustí len vtedy, ak skončí príkaz1 s nulovým _exit code_om. Vyhodnocovanie pokračuje zľava doprava: príkaz3 sa vykoná len vtedy, ak skončí nulou príkaz2. Filozofia je veľmi podobná skrátenému vyhodnocovaniu booleovských podmienok z jazyka C.

Predstavme si, že skript zapisuje do logovacieho súboru, ale s každým spustením ho chceme premazať. Klasické riešenie:

if [ -f $LOGFILE ] 
then
    rm -f $LOGFILE
fi

Skrátené riešenie: mazanie má prebehnúť len vtedy, ak súbor existuje, teda v prípade, že test -f $LOGFILE skončí s nulovým návratovým kódom. Ak súbor neexistuje, mazanie sa má preskočiť:

[ -f $LOGFILE ] && rm -f $LOGFILE   

Zoznam OR

Tento zoznam sa podobne vyhodnocuje zľava doprava a príkaz v sekvencii sa vyhodnotí len vtedy, ak príkaz pred ním skončí s nenulovým návratovým kódom.

príkaz1 || príkaz2 || príkaz3

Príkaz sa dá použiť na zlyhanie v prípade, že podmienka nie je splnená. Ak sa nenájde BSD jot, skript skončí s návratovým kódom 1. Pozor, túto konštrukciu spúšťajte zo skriptu, nie z interaktívneho módu — inak sa vám ukončí shell!

jot - 1 10 || exit 1

Hláška v Bashi bude:

-bash: jot: command not found

a samozrejme výpis návratového kódu vráti 1.

S výhodou sa dá použiť napr. na generovanie čísiel, ktoré podporí buď GNU seq alebo BSD jot, podľa toho, ktorý je k dispozícii:

jot - 1 10 || seq 10 || exit 3

Zoznam NOT

Na tento zoznam si musíme počkať: súvisí totiž s rúrami.

Kombinácie zoznamov

Vyhodnocovanie výrazov funguje zľava doprava a ampersandy i rúry majú rovnakú prioritu. Zoskupovať príkazy možno pomocou tradičných obyčajných i kučeravých zátvoriek.

Nasledovný príkaz zistí, či je spúšťaný na Debiane. Ak áno, vypíše jeho verziu, inak vypíše varovnú hlášku a skončí s návratovým kódom 3.

#!/bin/bash
[ -f /etc/debian_version ] || { echo "Debian not found"; exit 3; }
cat /etc/debian_version

Všimnite si zoskupenie príkazov: v rámci skupiny sa vykoná naraz výpis chybovej hlášky i vykonanie návratového kódu. Toto je jediná fungujúca možnosť: akákoľvek iná by viedla buď nesprávnej vetve alebo k ignorovaniu výpisu hlášky.

V prípade komplexných podmienok je oveľa lepšie používať štandardné podmienky — zvýši sa tým čitateľnosť skriptov.

Rúry

Rúru (pipeline) netreba špeciálne spomínať; používame ich úplne bežne. Je to sekvencia príkazov, kde štandardný výstup príkazu je presmerovaný na štandardný vstup nasledujúceho príkazu.

V ukážke spočítame počet súborov a adresárov v aktuálnom adresári:

ls -1 | wc -l

Návratový kód rúry sa určí podľa návratového kódu posledného vykonaného príkazu: v ukážke podľa návratového kódu príkazu wc -l.

Rúry bežia v subshelli

POSIXová špecifikácia hovorí, že rúra sa spúšťa v subshelli, ale v rámci optimalizácie behu sa shell môže rozhodnúť spustiť niektoré príkazy v aktuálnom shelli.

Pre zachovanie zdravého rozumu vždy predpokladajte, že rúra beží v subshelli. To sa týka hlavne hodnoty premenných: zmeny v premenných v rúre sa neprejavia po dobehnutí rúry. Klasickou demonštráciou je použitie read v rpre

echo "Yoko Ono" | read MENO PRIEZVISKO 
echo "Meno: $MENO"
echo "Priezvisko: $PRIEZVISKO"

V tomto prípade sa nevypíše nič. Príkaz read síce načítal údaje do premenných MENO a PRIEZVISKO, ale celá rúra echo/read beží v subshelli, po jej dobehnutí sa zmenené hodnoty oboch premenných stratia.

Rúry a negácie

Rúru môžeme uviesť znakom negácie: v tom prípade sa návratový kód určí ako negácia návratového kódu posledného príkazu v rúre.

V príklade zistíme, či je v systéme prítomný priečinok a ak nie je, nebudeme to považovať za chybu, ale za pozitívny stav:

! ls /home/novotnyr 2> /dev/null && echo "Priecinok je mozne vytvorit."

V tomto prípade máme rúru pozostávajúcu z jedného komponentu (nepoužívame totiž žiadne |), návratovým kódom je teda negácia celého výrazu. Jeho hodnota pred negáciou je 2 (príkaz ls totiž vracia práve takýto návratový kód, ak adresár neexistuje), a po negovaní 0.

Podmienky a skladanie procesov

Hoci príkaz test podporuje akú-takú možnosť skladať booleovské výrazy pomocou operátorov -a a -o, silne sa odporúča využiť mechanizmus skladania procesov. Ten je totiž omnoho portabilnejší a robustnejší voči nečakaným prekvapeniam. (Príkaz test v POSIX verzii totiž podporuje maximálne 4 argumenty, v prípade väčšieho počtu nedefinuje správanie.)

Už len takáto úloha: zistiť, či premenná obsahuje cestu k regulárnemu súboru a či tento súbor má oprávnenie na čítanie:

test -f "$SUBOR" -a -r "$SUBOR"

Ani sme sa nenazdali a máme 5 argumentov, čo v závislosti od implementácie môže , ale nemusí fungovať správne (v Bashi to bude fungovať bez problémov).

Namiesto toho sa odporúča použiť:

test -f "$SUBOR" && test -r "$SUBOR"

Alternatívne môžeme použiť aj skrátenú verziu:

[ -f "$SUBOR" ] && [ -r "$SUBOR" ]

Negácia

Ak si všimneme operátor výkričníka, zistíme, že sa využíva v dvoch rozličných úlohách:

  • ako negácia návratového kódu posledného príkazu v rúre
  • ako negácia hodnoty výrazu v príkaze test

Ukážme si to na príklade: vytvorme adresár, ak neexistuje:

ADRESAR=data
! ls "$ADRESAR" > /dev/null && mkdir "$ADRESAR"

Alternatívne môžeme využiť syntax výrazov príkazu test:

[ -d "$ADRESAR" ] && mkdir "$ADRESAR"

Táto syntax je možno o niečo prehľadnejšia: nemusíme uvažovať nad detailami vyhodnotenia operátora ! prí skladaní procesov (a zisťovať, čo všetko sa považuje za posledný príkaz v rúre), a v tomto špecifickom prípade sa zbavíme jedného presmerovania do čiernej diery.

Pridaj komentár

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