Cvičení: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14.

Toto jsou materiály pro cvičení 05 i 06. Kvízy na čtení před cvičením budou samostatné pro každý týden ale hodnocené úlohy (skriptování) budou spojeny, ale s přiděleným dvojnásobným počtem bodů.

V tomto cvičení téměř dokončíme naši cestu shellovým skriptováním: naučíte se, jak v shellu fungují podmínky a cykly, jak fungují proměnné – a mnohem víc.

Je důležité říct, že všechna následující témata fungují jak ve skriptech (tedy neinteraktivně), tak i interaktivně v terminálu. Obzvlášť cykly skrz seznamy souborů často používáme přímo na příkazové řádce, bez ukládání do plnohodnotného skriptu.

Nezapomeňte, že Čtení před cvičením je povinné a je z něj kvíz, který musíte vyplnit před cvičením.

Tohle je čtení před pátým cvičením.

Čtení před cvičením

Proměnné v shellu

Proměnné v shellu se často nazývají proměnné prostředí, jelikož jsou (narozdíl od proměnných v dalších jazycích) viditelné také z dalších programů. Dříve jsme si již vyzkoušeli, jak se nastavuje proměnná EDITOR, kterou používá Git pro určení editoru, který má spustit.

Proměnné nastavujeme pomocí následující konstrukce:

MOJE_PROMENNA="hodnota"

Pozor, kolem znaku = nesmějí být žádné mezery. V opačném případě by shell předpokládal, že se snažíme spustit program MOJE_PROMENNA s argumenty = a hodnota.

Hodnotu většinou uzavíráme do uvozovek. Uvozovky můžete vynechat v případě, že proměnná neobsahuje žádné mezery ani speciální znaky. Obecně je ale bezpečnější uvozovky vždy použít.

Když chceme získat hodnotu proměnné, použijeme před jejím jménem jako prefix znak dolaru $. Shell pak všechny výskyty výrazu $PROMENNA expanduje na hodnotu této proměnné. Funguje to podobně, jako když shell expanduje ~ na cestu k vaší domovské složce nebo když expanduje wildcards na skutečná jména souborů. O expanzi si ještě během tohoto cvičení povíme víc.

Znamená to tedy, že můžeme použít následující příkaz na vytištění hodnoty proměnné:

echo "Proměnná MOJE_PROMENNA je $MOJE_PROMENNA."
# Vytiskne Proměnná MOJE_PROMENNA je hodnota.

Všimněte si, že proměnné prostředí (tedy ty proměnné, které mají být viditelné pro ostatní programy) mají většinou jméno zapsané velkými písmeny. Pro proměnné shellu (tedy ty proměnné ve vašich skriptech, které pro ostatní programy nejsou zajímavé) používáme spíše jména zapsaná malými písmeny. V obou případech pak většinou používáme zápis ve stylu snake_case.

Narozdíl od ostatních jazyků jsou proměnné v shellu vždy stringy. Shell v základu podporuje aritmetiku s celými čísly, které jsou zapsány pomocí číslic ve stringu.

Čtění proměnných prostředí v Pythonu (a export)

Jestliže chceme číst shellové proměnné v Pythonu, můžeme použít os.getenv(). Do této funkce můžeme jako volitelný argument (kromě názvu proměnné) zadat defaultní hodnotu této proměnné. Vždy buď specifikujte defaultní hodnotu, nebo explicitně kontrolujte, že proměnná není None - nikdy není zaručeno, že je proměnná opravdu nastavena.

Použít můžete také os.environ.

Shell v základu nezpřístupňuje v Pythonu (ani v dalších aplikacích) všechny proměnné. Mimo shell jsou vidět pouze tzv. exportované proměnné. Aby vaše proměnná byla viditelná i pro ostatní programy, použijte jeden z následujících příkazů (první volání předpokládá, že někdo již proměnnou VAR nastavil):

export VAR
export VAR="hodnota"

Můžeme také exportovat proměnnou pouze pro konkrétní příkaz pomocí následující zkratky:

VAR=hodnota prikaz_ke_spusteni ...

Proměnná je má platnost pouze po dobu běhu daného příkazu a po jeho skončení se vrátí do původního stavu.

Aritmetika v shellu

Shell podporuje základní aritmetické operace. Stačí to na jednoduché sčítání, počítání zpracovaných souborů, atd. Pro počítání diferenciálních rovnic si prosím zvolte jiný programovací jazyk.

Jednoduché výpočty se uskutečňují ve speciálním prostředí $(( )):

counter=1
counter=$(( counter + 1 ))

Povšimněte si, že v tomto prostředí nepoužíváme $ jako prefix pro proměnné. Je potřeba poznamenat, že ve většině případů bude vše fungovat i v případě, že $ použijeme (např. $(( $counter + 1 ))), ale je dobré si na to nezvykat.

Speciální proměnné, set a env

Pokud si chcete vypsat seznam exportovaných proměnných, můžete použít příkaz env, který vytiskne názvy proměnných společně s jejich hodnotami.

Příkaz set pak vytiskne seznam všech proměnných.

Možná vám přijde zvláštní, že příkaz set vidí i neexportované proměnné. Je to tím, že některé příkazy (jako set nebo cd) jsou vykonávány přímo shellem namísto externích programů. Tyto příkazy se nazývají vestavěné příkazy shellu (shell built-ins).

Některé vestavěné příkazy nemají svoji vlastní manuálovou stránku, ale jsou zdokumentovány v man bash - tedy manuálové stránce shellu, který zrovna používáme.

Existuje několik shellových proměnných, které stojí za to znát, protože se vyskytují v naprosté většině shellů a Linuxových instalací:

  • $HOMEobsahuje cestu k vaší domovské složce. Na tuto proměnnou se expanduje ~ (vlnovka).

  • $PWD obsahuje vaši současnou pracovní složku.

  • $USER obsahuje jméno aktuálně přihlášeného uživatele (např. intro).

  • $RANDOM obsahuje náhodné číslo, které je znovu vygenerováno při každé expanzi (vyzkoušejte echo $RANDOM $RANDOM $RANDOM).

$PATH

O proměnné $PATH jsme se už zmínili. Nyní je na čase se na ni podívat podrobněji.

Jsou dva základní způsoby, jak v shellu specifikovat příkaz. Můžete ho buď zadat pomocí (relativní nebo absolutní) cesty (např. ./script.sh nebo 01/factor.py nebo /bin/bash), případně jako pouhý název bez lomítek (např ls).

V prvním případě shell najde zadanou cestu (relativně k pracovní složce) a spustí v ní tento konkrétní soubor. Soubor musí mít samozřejmě nastavený spustitelný bit.

V druhém přpadě shell začne hledat program ve všech složkách, které jsou obsažené v proměnné prostředí $PATH. V případě vícenásobné shody se použije první odpovídající složka. Pokud není program nalezen ani v jedné z těchto složek, shell ohlásí chybu.

Složky v proměnné $PATH jsou odděleny dvojtečkou :. V $PATH typicky najdete alespoň /usr/local/bin, /usr/bin, a /bin. Zjistěte, jak vypadá vaše proměnná $PATH (použijte příkaz echo ve vašem terminálu).

Koncept prohledávaných cest existuje i v dalších operačních systémech. Bohužel se na nich často používají jiné oddělovače (např. ;), protože dvojtečku jednoduše použít nelze.

Většinou na těchto systémech ale nejsou programy instalovány do složek v této proměnné, a tak je nelze spustit jednoduše z příkazové řádky. Extra tip pro uživatele Windows: pokud použijete Chocolatey, programy se budou instalovat do $PATH a instalace nového softwaru pomocí choco bude o něco méně bolestivá :-).

Do proměnné $PATH můžete teoreticky přidat i . (současnou složku). To vám umožní spouštět vaše skripty pouze pomocí script.sh místo ./script.sh. Ale nedělejte to (ačkoliv je to na jiných systémech standardní chování). V tomto vlákně se dočtete několik důvodů, proč je to špatný nápad.

Ve zkratce: pokud současnou složku přidíte na začátek $PATH, často se vám nejspíš povede spustit náhodné soubory v současné složce, které se zrovna jmenují stejně jako některý standardní příkaz (což může být nebezpečné!). Pokud ji přidáte na konec, tak zase občas pravděpodobně spustíte některý standardní příkaz, o kterém jste ani nevěděli, že existuje (např. test je vestavěný příkaz v shellu).

Velmi užitečné ale naopak může být si vytvořit podsložku ve vaší domovské složce (typicky ~/bin), přidat ji do $PATH a ukládat do ní všechny vaše užitečné skripty.

$PATH a shebang (proč potřebujeme env)

Pro shebang musíme vždy zadat interpretr pomocí absolutní cesty, což se nám občas nehodí.

To je důvod, proč používají Python skripty často shebang /usr/bin/env python3. env je tu příkaz, který spouští program specifikovaný jako první argument (tedy python3) a hledá ho při tom v $PATH.

Všechno funguje podle očekávání, protože jméno skriptu je předáno jako další argument

To je něco, o čem jsme se ještě nezmínili – shebang může dostat jeden (ale pouze jeden) volitelný argument, který je přidaný mezi jméno interpretru a jméno skriptu.

Shebang typu env tedy zavolá program env s parametry python3, cesta-ke-skriptu.py a všemi dalšími argumenty. Příkaz env pak najde python3 v $PATH, spustí ho a předá mu jako první argument cesta-ke-skriptu.py.

Všimněte si, že jde o stejný příkaz env, který jsme použili k vypsání proměnných prostředí. Bez argumentů vytiskne proměnné. S argumenty spustí příkaz.

Historie Unixu je dlouhá. V sedmdesátých letech bylo hlavním účelem příkazu env zajistit práci s prostředím. Konkrétně jedním z jeho úkolů bylo spouštět programy s modifikovaným prostředím, protože shell tou dobou ještě neuměl konstrukci VAR=hodnota prikaz. O pár desítek let později se přišlo na to, že jako vedlejší efekt umí env najít programy v $PATH, což se nakonec ukázalo jako mnohem užitečnější :-).

Za několik týdnů si povíme, proč je lepší hledat Python v $PATH a ne používat přímo /usr/bin/python3.

Stručně: díky env můžete pomocí pár chytrých triků modifikovat proměnnou $PATH a jednoduše tak přepínat mezi různými verzemi Pythonu, aniž byste museli jakkoliv modifikovat váš kód.

Řídicí struktury v shellových skriptech

Pojďme se podívat na shellové struktury pro řízení toku programu: podmínky a cykly.

Ještě předtím musíme zmínit, že více příkazů může být odděleno pomocí ; (středníku). Ačkoliv v shellových skriptech je lepší psát každý příkaz na samostatný řádek, v interaktivním módu se často může hodit zapsat více příkazů na jeden řádek (třeba už jen proto, že to umožňuje rychlejší pohyb v historii pomocí šipky nahoru).

Středníky uvidíme v řídicích strukturách na různých místech v roli oddělovače.

for cykly

For cykly v shellu vždy iterují přes sadu hodnot, které jsou předány při startu cyklu.

Formát je obecně následující:

for PROMENNA in VAL1 VAL2 VAL3; do
    tělo cyklu
done

Typickým příkladem použití cyklu je iterování přes seznam souborů. Tento seznam je často vygenerovaný pomocí expandovaných wildcards.

Pojďme se podívat na příklad, který spočítá počet číslic ve všech souborech *.txt:

for i in *.txt; do
    echo -n "$i: "
    tr -c -d '0-9' <"$i" | wc -c
done

Všimněte si, že ve výrazu for je jméno proměnné i uvedeno bez $. Dále můžeme vidět, že proměnná může být expandovaná i při přesměrování stdin (nebo stdout).

Když tento výraz začneme psát přímo v shellu, prompt se změní na jednoduché > (tedy pravděpodobně, závisí na vašem nastavení). Shell tím naznačuje, že na vstupu očekává zbytek cyklu.

Celý skript můžeme také dostat na jednu řádku (což se ale hodí pouze pro jednorázové skripty):

for i in *.txt; do echo -n "$i: "; tr -c -d '0-9' <"$i" | wc -c; done

if a else

Příkaz if v shellu může být trochu náročnější na pochopení. Důležité je si zapamatovat, že podmínka je vždy příkaz, který bude vykonaný, a jehož výsledek (tzn. exit kód) bude určovat i výsledek podmínky. Podmínka tedy nebude mít tradiční podobu a rovná se b, protože pro řízení toku potřebujeme exit kód.

Syntax podmínky vypadá takto:

if prikaz_kontrolujici_podminku; then
    uspech
elif jiny_prikaz_pro_vetev_else_if; then
    dalsi_uspech
else
    prikazy_v_else_vetvi
fi

Příkaz if musí být ukončený pomoci fi a větve elif a else jsou volitelné.

Jednoduché podmínky můžeme vyhodnotit pomocí příkazu test. Například test -d JMENO vrátí exit kód 0 v případě, že složka pojmenovaná JMENO existuje; v opačném případě vrátí 1. Tento příkaz umí testovat řadu dalších věcí, např. porovnávat stringy a čísla – více se dozvíte pomocí man test.

Pojďme se podívat, jak použít if a testpro kontrolu, jestli se nacházíme v Git projektu:

if test -d .git; then
    echo "Jsme v korenovem adresari Git projektu."
fi

Můžeme použít i elegantnější syntaxi: [ (levá hranatá závorka) je synonymem pro test a chová se přesně stejně, až na to, že vyžaduje jako svůj poslední argument ]. Pomocí této syntaxe může náš příklad vypadat následovně:

if [ -d .git ]; then
    echo "Jsme v korenovem adresari Git projektu."
fi

I v tomto případě je ale [ pouze obyčejný příkaz, jehož exit kód určuje, co se provede v podmínce.

Mimochodem, když se zkusíte podívat do /usr/bin, zjistíte, že se tam opravdu nachází spustitelný soubor pojmenovaný [. Ale v Bashi (tedy v našem shellu) je [ implementovaný i jako vestavěný příkaz, takže je o něco rychlejší, než kdybychom ho spouštěli jako externí program.

Občas se můžete setkat i s následujícím kódem:

if [[ -d .git ]]; then
    echo "Jsme v korenovem adresari Git projektu."
fi

Tyto dvojité závorky [[ ... ]] jsou zase jiným konstruktem a úzce souvisí se syntaxí $(( ... )) pro aritmetické výrazy. Tuto podmínku vyhodnocuje přímo Bash. Její syntaxe je o něco mocnější, ale funguje pouze v nejnovějších verzích Bashe a pravděpodobně tedy nebude fungovat v jiných shellech.

Budeme proto používat pouze tradiční variantu s [.

while cykly

While cykly vypadají následovně:

while prikaz_kontrolujici_cyklus; do
    prikazy_ke_spusteni
done

I v tomto případě se podmínka vyhodnotí jako pravdivá, pokud prikaz_kontrolujici_cyklus vrátí exit kód 0.

Následující příklad vrátí první použitelné jméno pro logovací soubor. Pozor – tento kód není imunní vůči souběhům (race conditions), pokud je spuštěn paralelně. Může být tedy spuštěn vícekrát, ale nikdy ve více procesech současně.

counter=1
while [ -f "/var/log/myprog/main.$counter.log" ]; do
    counter=$(( counter + 1 ))
done
logfile="/var/log/myprog/main.$counter.log"
echo "Loguji do souboru $logfile" >&2

Aby byl program odolný vůči chybám z paralelního běhu, museli bychom použít příkaz mkdir. Tento příkaz nahlásí chybu v případě, že složka již existuje (a je dostatečně atomický na to, abychom pomocí něho dokázali zjistit, že náš program byl opravdu úspěšný a nekrade tedy soubor někomu jinému).

Všimněte si, že pro invertování výsledku programu v následujícím kódu používáme vykřičník !.

counter=1
while ! mkdir "/var/log/myprog/log.$counter"; do
    counter=$(( counter + 1 ))
done
logfile="/var/log/myprog/log.$counter/main.log"
echo "Loguji do souboru $logfile" >&2

V shellu existuje také cyklus do ... until. Pokud tento příkaz budete někdy potřebovat, najděte si jeho dokumentaci v manuálu.

break a continue

Stejně jako v dalších jazycích je možné z cyklu vyskočit pomocí příkazu break. Obdobně můžete používat i příkaz continue.

Switch (aneb case ... esac)

Shell nabízí i konstrukci case pro případy, ve kterých potřebujeme rozvětvit náš program na základě hodnoty proměnné. Tato konstrukce je v zásadě podobná switch konstrukci v dalších jazycích, ale obsahuje pár shellových specifik.

Syntax je následující:

case hodnota_pro_vetveni in
    option1) prikazy_pro_prvni_moznost ;;
    option2) prikazy_pro_druhou_moznost ;;
    *) defaultni_vetev ;;
esac

Podobně jako u if musíme uzavřít příkaz stejným klíčovým slovem zapsaným pozpátku a u každé možnosti musíme ukončit sadu příkazů pomocí dvou středníků ;;.

Jednoduchý příklad může vypadat takto:

case "$EDITOR" in
    mcedit) echo 'Midnight Commander je nejlepší' ;;
    joe) echo 'Malý, ale šikovný' ;;
    vim|emacs) echo 'Wow :-)' ;;
    *) echo "To někdo opravdu používá $EDITOR?" ;;
esac

Kvíz před cvičením

Soubor s kvízem je ve složce 05 v tomto GitLabím projektu.

Zkopírujte si správnou jazykovou mutaci do vašeho projektu jako 05/before.md (tj. budete muset soubor přejmenovat).

Otázky i prostor pro odpovědi jsou v souboru, odpovědi vyplňte mezi značky **[A1]** a **[/A1]**.

Pipeline before-05 na GitLabu zkontroluje, že jste odevzdali odpovědi ve správném formátu. Ze zřejmých důvodů nemůže zkontrolovat skutečnou správnost.

Odevzdejte kvízy před začátkem dalšího cvičení.

Více o proměnných

Viděli jsme, že pro základní případy stačí následující pro vytvoření a použití proměnných ve skriptech.

output_file="out.txt"
echo "Writing to $output_file." >&2
head -n 1 /etc/passwd >"$output_file"

Neinicializované hodnoty a podobné nástrahy

Pokusíte-li se použít proměnnou, která není inicializovaná, shell k ní bude přistupovat, jakoby obsahovala prázdný řetězec. Přestože to někdy může být užitečné, je to také zdroj nepříjemných překvapení.

Jak jsme zmínili dříve, měli byste vždy začínat své shellové skripty set -u, abyste byli v takových situacích varováni.

Někdy ale přesto potřebujeme číst z potenciálně neinicializované proměnné, abychom ověřili, jestli je inicializovaná. Například můžeme chtít číst $EDITOR, abychom zjistili preferovaný editor uživatele, ale nabídnout rozumnou výchozí hodnotu, pokud proměnná není nastavena. Toho je jednoduché dosáhnout použitím notace ${VAR:-default_value}. Je-li proměnná VAR nastavena, použije se její hodnota, jinak se použije default_value aniž by to způsobilo varování vyvolané set -u.

Můžeme tedy psát:

"${EDITOR:-mcedit}" file-to-edit.txt

Často je lepší ošetřit výchozí hodnoty na začátku skriptu tímto idiomem:

EDITOR="${EDITOR:-mcedit}"

Dále ve skriptu už můžeme editor spouštět jen takto:

"$EDITOR" file-to-edit.txt

Poznamenejme, že je také možné psát ${EDITOR} pro explicitní oddělení jména proměnné. To se hodí, pokud chceme vypsat proměnnou následovanou nějakým písmenem.

file_prefix=nswi177-
echo "Will store into ${file_prefix}log.txt"
echo "Will store into $file_prefixlog.txt"

Expanze proměnných (a jiných konstrukcí)

Viděli jsme, že shell provádí různé typy expanzí. Expanduje proměnné, wildcardy, tildu, aritmetické výrazy, a spoustu dalších.

Je důležité pochopit, jak tyto expanze interagují navzájem. Namísto popisování formálního procesu (který je celkem složitý), ukážeme několik příkladů na demonstraci typických situací.

Budeme volat args.py z předchozích cvičení pro ukázání, co se děje. (Samozřejmě je potřeba jej volat ze správného adresáře.)

Zaprvé, zpracování parametrů (jejich dělení) se děje až po expanzi proměnných:

VAR="value with spaces"
args.py "$VAR"
args.py $VAR

Připravte si soubory pojmenované one.sh a with space.sh pro následující příklad:

VAR="*.sh"
args.py "$VAR"
args.py $VAR
args.py "\$VAR"
args.py '$VAR'

Spusťte příkazy znova, ale odstraňte one.sh po přiřazení do VAR.

Expanze tildy (domovského adresáře) funguje trochu jinak:

VAR=~
echo "$VAR" '$VAR' $VAR
VAR="~"
echo "$VAR" '$VAR' $VAR

Důležité je si odnést, že expanze proměnných může být ošidná, ale vždy se dá snadno vyzkoušet namísto pamatování si všech chytáků. Tedy, když si budete pamatovat, že mezery a wildcardy vyžadují speciální pozornost, bude to v pohodě :-).

Nahrazování příkazů (neboli zachytávání stdout do proměnné)

Často potřebujeme uložit výstup příkazu do proměnné. To také zahrnuje uložení obsahu souboru (nebo jeho části) do proměnné.

Význačným příkladem je použití příkazu mktemp(1). Ten řeší problém s bezpečným vytvářením dočasných souborů (vzpomeňme si, že vytváření dočasných souborů s předurčeným názvem v adresáři /tmp je nebezpečné). Příkaz mktemp vytvoří soubor (nebo adresář) s unikátním názvem a vypíše jeho název na stdout. Pro použití souboru v dalších příkladech si proto musíme jeho název uložit do proměnné.

Shell nabízí následující syntaxi pro tzv. nahrazování příkazů (command substitution):

my_temp="$( mktemp -d )"

Příkaz mktemp -d se spustí a jeho výstup se uloží do proměnné $my_temp.

Kam se uloží stderr? Answer.

Jak potom zachytit stderr?

Například takto:

my_temp="$( mktemp -d )"
stdout="$( the_command 2>"$my_temp/err.txt" )"
stderr="$( cat "$my_temp/err.txt" )"

Nahrazování příkazů se také často používá při logování nebo při úpravách jmen souborů (podívejte se do manuálových stránek, co dělají date, basename a dirname):

echo "I am running on $( uname -m ) architecture."

input_filename="/some/path/to/a/file.sh"
backup="$( dirname "$input_filename" )/$( basename "$input_filename" ).bak"
other_backup="$( dirname "$input_filename" )/$( basename "$input_filename" .sh ).bak.sh"

Přesměrování větších částí shellu

Celé řídící struktury (např. for, if nebo while se všemi příkazy uvnitř) se chovají jako jeden příkaz. Můžeme na ně tedy použít přesměrování jako na celek. Například:

if test -d .git; then
    echo "We are in a root of a Git project"
else
    echo "This is not a root of a Git project"
fi | tr 'a-z' 'A-Z'

Příkaz read

Potřebujeme-li ve skriptu číst ze stdin do proměnné, můžeme použít vestavěný příkaz read:

read FIRST_LINE <input.txt
echo "$FIRST_LINE"

Příkaz read typicky používáme v cyklu while pro iterování přes celý vstup. read taky dokáže rozdělit řádek na části oddělené bílými znaky a přiřadit každou z nich do samostatné proměnné.

Máme-li vstup v tomto formátu, následující cyklus spočítá průměr čísel.

/dev/sdb 1008
/dev/sdb 1676
/dev/sdc 1505
/dev/sdc 4115
/dev/sdd 999
count=0
total=0
while read device duration; do
    count=$(( count + 1 ))
    total=$(( total + duration ))
done
echo "Average is about $(( total / count ))."

Jak můžete uhádnout z úryvku výše, read vrací 0, dokud je schopný načítat do proměnných. Dosažení konce souboru je oznámeno nenulovou návratovou hodnotou.

read se občas může chovat příliš chytře k některým vstupům. Například interpretuje zpětná lomítka. Pro potlačení tohoto chování lze použít read -r.

Další užitečné parametry jsou -t nebo -p: použijte read --help pro zobrazení jejich popisu.

Parametry skriptů a getopt

Když shell skript dostane parametry, můžeme k nim přistupovat přes speciální proměnné $1, $2, $3, …

Otestujte s následujícím skriptem:

echo "$#"
echo "${0}"
echo "${1:-parameter one not set}"
echo "${2:-parameter two not set}"
echo "${3:-parameter three not set}"
echo "${4:-parameter four not set}"
echo "${5:-parameter five not set}"

a spusťte jako

./script.sh
./script.sh one
./script.sh one two
./script.sh one two three
./script.sh one two three four
./script.sh one two three four five
./script.sh one two three four five six

Chceme-li přistupovat ke všem parametrům, je k tomu speciální proměnná $@. Zkuste přidat args.py "$@" do skriptu výše a spustit znova.

Proměnná $@ musí být ohraničená uvozovkami, aby fungovala správně (vysvětlení je mimo rámec tohoto kurzu). Speciální proměnná $# obsahuje počet argumentů na příkazové řádce a $0 obsahuje název skriptu (jako sys.argv[0]).

getopt

Potřebuje-li náš skript jeden argument, stačí přistupovat přímo k $1. Potřebujeme-li rozpoznávat přepínače, bude zpracování argumentů složitější. Shell k tomu poskytuje příkaz getopt.

Nebudeme popisovat všechny detaily tohoto příkazu. Namísto toho ukážeme příklad, který si můžete upravovat podle svých potřeb.

Hlavní argumenty řídící chování getopt jsou -o a -l, které obsahují popis přepínačů přijímaných naším programem.

Předpokládejme, že budeme chtít přijímat volby --verbose, která říká, aby byl náš skript výřečnější, a --output specifikující alternativní výstupní soubor. Také budeme chtít přijímat krátké verze těchto přepínačů: -o a -v. Při použití --version budeme chtít vypsat verzi našeho skriptu. A také nemůžeme zapomenout na --help. Ostatní argumenty (nepřepínače) budou interpretovány jako názvy vstupních souborů.

Specifikace přepínačů pro getopt je jednoduchá:

getopt -o "vho:" -l "verbose,version,help,output:"

Jednoznakové přepínače jsou specifikovány za -o, dlouhé volby za -l, a dvojtečka : za přepínačem značí, že bude očekávat argument.

Za to přidáme -- následované vlastními parametry. Vyzkoušejte si to:

getopt -o "vho:" -l "verbose,version,help,output:" -- --help input1.txt --output=file.txt
getopt -o "vho:" -l "verbose,version,help,output:" -- --help --verbose -o out.txt input2.txt
...

Jak vidíte, getopt umí zpracovat vstup a zkonvertovat parametry do unifikované podoby, přičemž argumenty, které nejsou volbami, přesune na konec.

Následující “magický” řádek (kterému nemusíte rozumět, abyste ho mohli používat) přenastaví $1, $2, atd. aby obsahovaly hodnoty po zpracování příkazem getopt.

eval set -- "$( getopt -o "vho:" -l "verbose,version,help,output:" -- "$@" )"

Vlastní zpracování je poté celkem přímočaré:

#!/bin/bash

set -ueo pipefail

opts_short="vho:"
opts_long="verbose,version,help,output:"

# Check for bad usage first (notice the ||)
getopt -Q -o "$opts_short" -l "$opts_long" -- "$@" || exit 1

# Actually parse them (we are here only if they are correct)
eval set -- "$( getopt -o "$opts_short" -l "$opts_long" -- "$@" )"

be_quiet=true
output_file=/dev/stdout

while [ $# -gt 0 ]; do
    case "$1" in
        -h|--help)
            echo "Usage: $0 ..."
            exit 0
            ;;
        -o|--output)
            output_file="$2"
            shift
            ;;
        -v|--verbose)
            be_quiet=false
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Unknown option $1" >&2
            exit 1
            ;;
    esac
    shift
done

$be_quiet || echo "Starting the script"

for inp in "$@"; do
    $be_quiet || echo "Processing $inp into $output_file ..."
done

Některé části skriptu si zaslouží vysvětlení.

true a false nejsou boolovské hodnoty, ale můžou být použity samostatně. Ve skutečnosti jde o jednoduché programy, které jen vrací správnou návratovou hodnotu (0 nebo 1). Všimněte si, jak je používáme pro řízení logování. (Mimochodem, $be_verbose && echo "Message" by nefungovalo. Vidíte proč?)

exit okamžitě ukončí shellový skript. Jeho volitelný parametr představuje návratovou hodnotu skriptu.

shift je speciální příkaz, který posune proměnné $1, $2, … o jedno místo. Po jeho zavolání se hodnota $3 přesune do $2, $2 se přesune do $1 a hodnota $1 bude zahozena. "$@" se voláním shift také upraví. Celý cyklus tedy zpracuje všechny volby po nalezení --, které oddělují volby od ostatní argumentů. Následný cyklus for poté iteruje jen přes zbylé argumenty.

Funkce v shellu

V shellu lze definovat i funkce:

function_name() {
    commands
}

Funkce má stejné rozhraní jako plnohodnotný shellový skript. Argumenty jsou předány jako $1, $2, … Výsledek funkce je celé číslo se stejnou sémantikou jako návratový kód. Kulaté závorky () jsou zde tedy jen pro označení, že jde o funkci; není to seznam argumentů.

Pro podrobnosti k tomu, které proměnné jsou uvnitř funkce viditelné, se prosím podívejte do následující sekce.

Jednoduchá logovací funkce může vypadat takto:

msg() {
    echo "$( date '+%Y-%m-%d %H:%M:%S |' )" "$@" >&2
}

Funkce vypisuje aktuální datum následované vlastní zprávou, vše do stderr.

Jako další příklad uveďme následující funkci:

get_load() {
    cut -d ' ' -f "$1" </proc/loadavg
}

load_curr="$( get_load 1 )"
load_prev="$( get_load 2 )"

Všimněte si, jak je stdout funkce zachycen do proměnné.

Volání return ukončí funkci, volitelným parametrem je její návratová hodnota. (Použijete-li ve funkci exit, ukončí se tím celý skript.)

is_shell_script() {
    case "$( head -n 1 "$1" 2>/dev/null )" in
        \#!/bin/sh|\#!/bin/bash)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

Taková funkce může být použita v if takto:

if is_shell_script "$1"; then
    echo "$1 is a shell script"
fi

Všimněte si, jak dobré pojmenování zjednoduší čtení konečného programu. Také pomůže dát argumentům funkce názvy namísto odkazování se na ně přes $1. Můžete je přiřadit do proměnné, ale je doporučeno označit proměnné jako local (viz následující sekce):

is_shell_script() {
    local file="$1"
    case "$( head -n 1 "$file" 2>/dev/null )" in
        \#!/bin/sh|\#!/bin/bash)
            return 0
            ;;
        *)
            return 1
            ;;
    esac
}

Můžete si všimnout, že aliasy, funkce, builtiny, a normální příkazy se všechny volají stejným způsobem. Shell proto má pevné pořadí priorit: Aliasy se testují jako první, poté funkce, builtiny, a nakonec příkazy v $PATH. V souvislosti s tím se můžou hodit vestavěné příkazy command a builtin (např. ve funkcích stejného jména).

Navzdory mnoha odlišnostem od funkcí v jiných programovacích jazycích, funkce v shellu pořád představují nejlepší způsob, jak členit vaše skripty. Správně nazvaná funkce vytváří vrstvu abstrakce a zachycuje záměr skriptu, zároveň skrývá implementační detaily.

Subshell a viditelnost proměnných (scoping)

Tato část vysvětluje pár pravidel a faktů o viditelnosti proměnných a proč některé konstrukce nemůžou fungovat.

Shellové proměnné jsou ve výchozím stavu globální. Všechny proměnné jsou viditelné ve všech funkcích, jejich úpravy uvnitř funkcí jsou viditelné ve zbytku skriptu, atp.

Bývá praktické deklarovat proměnné ve funkcích jako lokální (local), což omezí jejich viditelnost jen na danou funkci. (Přesněji, proměnná je viditelná v této funkci a všech funkcích volaných z ní. Můžete si představit, že předchozí hodnota proměnné je uložena během vykonávání local a obnovena po návratu z funkce. To se liší od většiny programovacích jazyků.)

Po spuštění jiného programu (včetně shellových a Pythoních skriptů), program dostane kopii všech exportovaných proměnných. Když tyto proměnné upraví, změny zůstanou jen uvnitř tohoto programu a nijak neovlivní původní shell. (Je to podobné tomu, jak funguje pracovní adresář.)

Když použijete pipu, je to stejné jako spuštění nového shellu: proměnné nastavené uvnitř pipeliny nejsou vidět v okolním kódu. (Jediný rozdíl je, že pipelina dostane i neexportované proměnné.)

Uzavření části našeho skriptu do ( .. ) vytvoří tzv. subshell, který se chová, jako kdybychom spustili jiný skript. Proměnné upravené uvnitř tedy opět nejsou viditelně pro okolní shell.

Přečtěte si a spusťte následující kód pro pochopení zmiňovaných věcí.

global_var="one"

change_global() {
    echo "change_global():"
    echo "  global_var=$global_var"
    global_var="two"
    echo "  global_var=$global_var"
}

change_local() {
    echo "change_local():"
    echo "  global_var=$global_var"
    local global_var="three"
    echo "  global_var=$global_var"
}

echo "global_var=$global_var"
change_global
echo "global_var=$global_var"
change_local
echo "global_var=$global_var"

(
    global_var="four"
    echo "global_var=$global_var"
)

echo "global_var=$global_var"

echo "loop:"
(
    echo "five"
    echo "six"
) | while read value; do
    global_var="$value"
    echo "  global_var=$global_var"
done
echo "global_var=$global_var"

Cvičení

Hromadná konverze obrázků

Program convert z ImageMagick umí převádět obrázky mezi formáty použitím convert source.png target.jpg (s téměř jakýmikoliv příponami souborů). Převeďte všechny obrázky s příponou .png v aktuálním adresáři do JPEGu (s příponou .jpg).

Answer.

Mimochodem, ImageMagick umožnuje provádět spoustu různých operací, jednou z těch, které je dobré si pamatovat je změna rozměrů obrázků:

convert DSC0000.jpg -resize 800x600 thumbs/DSC0000.jpg

Standardní vstup nebo argumenty?

Napište fact.sh s funkcí, která spočítá faktoriál zadaného čísla. Vytvořte dvě verze:

  1. Načtení vstupu ze stdin.

  2. Načtení vstupu z prvního argumentu ($1). Answer.

Kterou verzi bylo jednodušší napsat? Která dává větší smysl?

Ad-hoc zpracování souborů CSV

Napište skript csv_sum.sh, který přečte soubor CSV ze standardního vstupu. Sečtěte všechna čísla ve sloupci, který je zadaný jako jediný argument. Nezapomeňte skript ukončit s nenulovým návratovým kódem a vhodnou chybovou hláškou, není-li žádný argument zadán.

Uvažme následující soubor nazvaný file.csv.

family_name,first_name,age,points,email
Doe,Joe,22,1,joe_doe@some_mail.com
Fog,Willy,38,8,ab@some_mail.com
Zdepa,Pepa,10,1,pepa@some_mail.com

Výstupem příkazu ./csv_sum.sh points <file.csv má být 10. Answer.

Sloupcové grafy (v shellovském stylu)

Napište bar_plot.sh, který vypíše sloupcový graf s vodorovnými sloupci. Vstupní čísla značí šířku sloupců. Vyberte si, která z možností vstupů je pro vás vhodnější. Příklad:

$ ./bar_plots.sh 7 1 5
7: #######
1: #
5: #####

Je-li největší hodnota větší než 60, přeškálujte celý graf.

Answer.

Úloha tree.py v shellu

Napište implementaci úlohy tree.py v shellu.

Answer.

Hodnocené úlohy …

… pro toto cvičení jsou společné pro toto a další cvičení a budou zadány na dalším cvičení.

Učební výstupy

Znalosti konceptů

Znalost konceptů znamená, že rozumíte významu a kontextu daného tématu a jste schopni témata zasadit do většího rámce. Takže, jste schopni …

  • vysvětlit jak funguje expanze shellu a rozdělení argumentů příkazové řádky do pole

  • vysvětlit kdy se vyplatí použít Python a kdy bash

  • vysvětlit, co je to proměnná prostředí

  • vysvětlit rozdíl mezi neexportovanou a exportovanou proměnnou (prostředí) v shellu

  • vysvětlit problémy při souběžné práci s dočasnými soubory

  • vysvětlit, jak exit kódy umožňují řídit shellové skripty

  • vysvětlit, jak je vyhodnocen kód if true; then ... fi

  • vysvětlit, jak funguje proměnná prostředí $PATH a jak ovlivňuje skripty se shebangem

  • vysvětlit, jak v shellu funguje omezení oblasti platnosti (scoping) pro proměnné

Praktické dovednosti

Praktické dovednosti se obvykle týkají použití daných programů pro vyřešení různých úloh. Takže, dokážete …

  • nastavovat a číst proměnné prostředí v shellu

  • vyhodnocovat matematické výrazy v shellu

  • používat nahrazování příkazů (command substitution)

  • používat bezpečně v shellových skriptech dočasné soubory

  • používat v shellových skriptech řídicí struktury (for, while, if, case)

  • používat příkaz read

  • používat getopt pro parsování argumentů příkazové řádky

  • vytvářet a používat funkce

  • číst proměnné prostředí v Pythonu (volitelné)