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

Cílem tohoto cvičení je rozšířit vaše znalosti shellového skriptování. Zavedeme proměnné, tzv. command substitution a uvidíme také základní aritmetiku v shellu.

Toto cvičení bude postaveno na jednom příkladu, který budeme postupně rozvíjet, abyste se základní koncepty naučili na praktickém příkladu (samozřejmě existují specifické nástroje, které by se daly použít rovnou, ale doufáme, že je to lepší než zcela umělý příklad).

Náš příklad bude postaven na vytvoření malého webu ze zdrojových kódů v Markdown pomocí programu Pandoc. Nejprve si popíšeme Pandoc a poté náš příklad.

Předstartovní kontrola

  • Vaše skripty vždy začínají tím správným shebangem (a jsou označeny jako spustitelné).
  • Zvládáte přečíst základní HTML.
  • Pamatujete si význam exit kódu (návratová hodnota programu) a víte, co znamená, když má hodnotu 0.

Pandoc

Pandoc je univerzální převaděč dokumentů, který umí převádět mezi mnoha formáty včetně HTML, Markdownu, Docbooku, LaTeXu, Wordu, LibreOffice či PDF.

Ujistěte se, že vaše instalace programu Pandoc je přiměřeně aktuální (tj. alespoň verze 2.19, která byla vydána přibližně před rokem).

Základní použití

Prosím, naklonujte náš repozitář s příklady (nebo si ho git pullněte, pokud máte klon stále k dispozici).

Přesuňte se do podadresáře 06/pandoc.

cat example.md
pandoc example.md

Jak můžete vidět, výstupem je převedený Markdown do HTML, byť bez HTML hlavičky.

Markdown může být rovnou nakombinovaný s HTML (což se hodí, pokud máte složitější HTML kód: Pandoc ho pak zkopíruje tak, jak je).

<p>This is an example page in Markdown.</p>
<p>Paragraphs as well as <strong>formatting</strong> are supported.</p>
<p>Inline <code>code</code> as well.</p>
<p class="alert alert-primary">
Third paragraph with <em>HTML code</em>.
</p>

Pokud přidáte --standalone, pak Pandoc vytvoří kompletní HTML stránku. Pojďme to zkusit (obě spuštění dopadnou stejně):

pandoc --standalone example.md >example.html
pandoc --standalone -o example.html example.md

Zkuste otevřít example.html také ve vašem prohlížeči.

Jak jsme zmiňovali, Pandoc umí vytvářet i soubory ve formátu OpenDocument (to je formát používaný např. balíky OpenOffice či LibreOffice).

pandoc -o example.odt example.md

Všimněte si, že jsme nepřidali --standalone protože na nic kromě HTML vlastně není potřeba. Zkontrolujte, jak výsledný dokument vypadá v LibreOffice/OpenOffice, nebo ho můžete dokonce zkusit naimportovat do některého z online kancelářských balíků.

Neměli byste commitovat soubor example.odt do svého repozitáře, protože může být kdykoliv znovu vygenerován. To je obecné pravidlo pro jakýkoli soubor, který může být vytvořen automaticky.

Poznámka pod čarou o LibreOffice

Víte o tom, že LibreOffice jde také používat z příkazové řádky? Například můžeme po LibreOffice chtít, aby nám převedl dokument do PDF následujícím příkazem:

soffice --headless --convert-to pdf example.odt

Parametr --headless zabrání spuštění jakéhokoliv GUI a --convert-to asi vysvětlení nepotřebuje.

V kombinaci s Pandocem tak stačí tři příkazy, abychom z jednoho zdroje vytvořili HTML stránku i PDFko.

Šablony v Pandocu

Ve výchozím nastavení používá Pandoc svoji interní šablonu pro HTML výstup. Ale můžeme změnit i tuhle šablonu.

Podívejte se do template.html. Když je šablona expandována (nebo vytištěna), části mezi dolary budou nahrazeny skutečným obsahem.

Zkusme to s Pandocem.

pandoc --template template.html example.md >example.html

Podívejte se, jak výstup vypadá. Všimněte si, jak bylo nahrazené $body$ a $title$.

Další použití Pandocu

Pandoc lze používat i složitějšími způsoby, ale pro náš příklad postačí základní použití (včetně šablon).

Pandoc umí převod do/z LaTeXu a spousty dalších formátů (zkuste spustit s --list-output-formats a --list-input-formats).

Může být také použit jako univerzální parser Markdownu s argumenty -t json (část volající Python není nutná, jen přeformátuje výstup).

echo 'Hello, **world**!' | pandoc -t json | python3 -m json.tool

Průběžný příklad

Přesuňte se prosím do podadresáře 06/web a podívejte se, jaké soubory tam máme.

Naším příkladem je triviální webová stránka, kde uživatel upravuje soubory Markdown a my používáme Pandoc a vlastní šablonu k vytvoření konečného HTML. V tuto chvíli je konečnou fází příkladu vytvoření souborů HTML, které by se později zkopírovaly na webový server.

Pokud se podíváte na soubory, najdete tam nějaké zdrojové soubory v Markdownu a soubor build.sh, který vytváří web.

Spusťte jej a podívejte se, jak vypadá konečný výsledek.

Nyní si povíme více o skriptování v shellu a na našem skriptu build.sh si ukážeme, jak jej můžeme vylepšit.

Použití && a || (logická skladba programu)

Než budete pokračovat v této části, připomeňte si, co je to výstupní (návratový) kód (exit/return code) programu.

Proveďte následující příkazy:

ls / && echo "ls okay"
ls /nonexistent-filename || echo "ls failed"

Toto je příklad toho, jak lze exit kódy používat v praxi. Můžeme řetězit příkazy, které se provedou pouze tehdy, když předchozí příkaz selhal nebo skončil s nulovým exit kódem.

Porozumění následujícím konstrukcím je skutečně nutné, protože spolu s rourami a standardním přesměrováním I/O tvoří základní stavební kameny shellových skriptů.

Nejprve si představíme syntaxi pro podmíněné řetězení volání programu.

Pokud chceme jeden příkaz provést pouze v případě, že předchozí příkaz uspěl, oddělíme je pomocí && (tj. jedná se o logické a) Na druhou stranu, pokud chceme druhý příkaz provést pouze v případě, že první příkaz selže (jinými slovy, provést první nebo druhý), oddělíme je pomocí ||.

Příklad s ls je poměrně umělý, protože ls je při výskytu chyby poměrně “ukecaný”. Existuje však také program test, který je tichý a lze jej použít k porovnávání čísel nebo kontrole vlastností souborů. Například test -d ~/Desktop kontroluje, zda ~/Desktop je adresář. Pokud jej spustíte, nic se nevypíše. Ve společnosti s && nebo || však můžeme zkontrolovat jeho výsledek.

test -d .git && echo "We are in a root of a Git project"
test -f README.md || echo "README.md missing"

To by se dalo použít jako velmi primitivní větvení v našich skriptech. V jednom z příštích cvičení zavedeme úplné podmíněné příkazy, jako jsou if a while.

Navzdory své tichosti je test ve skutečnosti velmi mocný příkaz - nic nevypisuje, ale lze jej použít k ovládání jiných programů.

Příkazy je možné řetězit, && a || jsou asociativní a mají stejnou prioritu.

Porovnejte následující příkazy a jejich chování v adresáři, kde je nebo není soubor README.md:

test -f README.md || echo "README.md missing" && echo "We have README.md"
test -f README.md && echo "We have README.md" || echo "README.md missing"

Rozšiřování průběžného příkladu

Pravděpodobně jste si všimli, že získáme id posledního commitu (to dělá git rev-parse --short HEAD) a použijeme ho k vytvoření zápatí webové stránky (pomocí přepínače -A v Pandocu).

To funguje, pokud jsme skript pustili v rámci Gitového repozitáře. Zkopírujte celý adresář web mimo repozitář Gitu a znovu spusťte build.sh.

fatal: not a git repository (or any parent up to mount point /)
Stopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).

Získali jsme děsivou zprávu a web nebyl přegenerován.

Pokud změníme řádek na následující, zajistíme, že skript bude možné spustit i mimo projekt Gitu.

git rev-parse --short HEAD >>version.inc.html 2>/dev/null || echo "unknown" >>version.inc.html

Možná to není úplně dokonalé, ale alespoň lze web stále vytvářet.

Proměnné v shellu

Proměnné v shellu se často nazývají proměnné prostředí, jelikož jsou (na rozdíl od proměnných v dalších jazycích) viditelné také z dalších programů.

V tomto smyslu hrají proměnné v shellu dvě důležité role. Jako běžné proměnné pro shellové skripty (tj. proměnné se stejným významem jako v jiných programovacích jazycích), ale lze je použít i pro konfiguraci jiných programů.

Už jsme si vyzkoušeli, jak nastavit proměnnou EDITOR, která říká Gitu (a dalším programům), který textový editor pro nás má Git spustit. Vidíme tedy, že tato proměnná ovlivňuje chování i u programů, které nejsou shellové skripty.

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, dokud hodnota nevypadá jako identifikátor z Céčka.

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.

Na rozdíl od ostatních jazyků jsou proměnné v shellu vždy řetězce (stringy). Shell v základu podporuje aritmetiku s celými čísly, která jsou zapsána pomocí desítkových číslic ve stringu.

Bash podporuje také slovníky a pole. I když mohou být velmi užitečné, jejich použití často označuje hranici, kdy by použití vyššího jazyka dávalo větší smysl s ohledem na udržovatelnost kódu. V tomto předmětu se jimi vůbec nebudeme zabývat.

Rozšiřování průběžného příkladu

V současné době jsou naše soubory generovány do stejného adresáře jako zdrojové soubory. To způsobí, že kopírování souborů HTML na webový server bude náchylné k chybám, protože můžeme zapomenout na nějaký soubor nebo zkopírovat zdrojový soubor, který ve skutečnosti není potřeba.

Změníme kód tak, aby se soubory kopírovaly do samostatného adresáře. Vytvoříme si pro výstup adresář public/ a upravíme hlavní část našeho skriptu na následující:

pandoc --template template.html -A version.inc.html index.md >public/index.html
pandoc --template template.html -A version.inc.html rules.md >public/rules.html

Na konec skriptu bychom také měli přidat následující příkaz, aby soubor public obsahoval všechny požadované soubory.

cp main.css public/

Vše je v pořádku. Až na to, že cesta je na několika místech skriptu zakódována natvrdo. To může později komplikovat údržbu.

Zde však můžeme snadno použít proměnnou pro uložení cesty a umožnit uživateli změnit cílový adresář úpravou cesty na jednom místě.

html_dir="public"

...

pandoc --template template.html -A version.inc.html index.md >"$html_dir/index.html"
pandoc --template template.html -A version.inc.html rules.md >"$html_dir/index.html"
cp main.css "$html_dir/"

Může se to zdát jako práce navíc bez skutečného přínosu. Nezapomínejte však, že programy se možná píší jednou, ale čtou se mnohokrát, a každá informace, která lépe popisuje záměr/účel kódu, čtenáři pomůže.

Čtení 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 pro Python (ani pro další aplikace) 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 ...

Takové přiřazení platí pouze po dobu běhu daného příkazu a po jeho skončení se proměnná vrátí do původního stavu.

Rozšiřování průběžného příkladu

Abychom to demonstrovali na našem průběžném příkladu, použijeme proměnnou prostředí k úpravě popisku tabulky generované skriptem table.py.

    caption = os.getenv('TABLE_CAPTION', 'Points')
    print(f"""
<table>
  <caption>{caption}</caption>
  <thead>
    <tr>
      <th>Team</th>
      <th>Points</th>
    </tr>
  </thead>
  <tbody>""")

Pak můžeme popisek změnit nastavením proměnné ve skriptu build.sh:

TABLE_CAPTION="Scoring table" ./table.py <score.csv | pandoc

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.

Seznam všech proměnných získáte spuštěním příkazu set (opět jako u cd, jedná se o vestavěný příkaz shellu).

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 váš pracovní adresář.

  • $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/project_name.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 proměnné obsahující seznam 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.

Nainstalované programy však nejsou vždy nainstalovány do adresářů, které jsou v této proměnné uvedeny, a proto je obvykle nelze snadno spustit z příkazového řádku.

Extra tip pro uživatele Windows: pokud použijete Chocolatey, programy se budou instalovat do $PATH a instalace nového softwaru pomocí choco je o něco méně bolestivá :-).

Do proměnné $PATH můžete teoreticky přidat i . (aktuální adresář). 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 PROMENNA=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.

Parametry skriptů

V jazyce Python přistupujeme k parametrům skriptu prostřednictvím sys.argv. V shellu je situace poněkud složitější a bohužel je to jedno z míst, kde návrh jazyka/prostředí poněkud pokulhává.

Shell používá speciální proměnné $1, $2, …, které odkazují na jednotlivé argumenty skriptu; $0 obsahuje název skriptu (proměnná $10 neexistuje - přístup k takovému parametru je ještě složitější; detaily později).

Později uvidíme, jak můžeme zpracovat argumenty v obvyklém formátu -d , -f ..., nyní budeme používat přímo $i.

Shell také nabízí speciální proměnnou "$@", kterou lze použít k předání všech aktuálních parametrů jinému programu. Výslovně jsme zde použili uvozovky, protože bez nich dojde k poškození argumentů, které obsahují mezery.

Jako typický příklad použití "$@" vytvoříme jednoduchý wrapper pro Pandoc, který přidá některé obvyklé volby, ale stále umožní uživateli další přizpůsobení.

#!/bin/bash

pandoc --self-contained --base-header-level=2 --strip-comments "$@"

Naše níže uvedené volání by se dalo přeložit takto.

./pandoc_wrapper.sh --standalone --template main.html input.md
# pandoc --self-contained --base-header-level=2 --strip-comments --standalone --template main.html input.md

Připomeňme, že pokud uživatel zavolá tento skript jako ./pandoc_wrapper.sh <input.md, bude vše fungovat. Standardní vstup je transparentně přeposlán do Pandoc.

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"

Rozšiřování průběžného příkladu

Nyní rozšíříme náš průběžný příklad o několik volání echo, aby skript mohl vypisovat, co právě dělá.

Toto je triviální kód, který kontroluje, zda je první argument --verbose, a pokud ano, nastaví proměnnou verbose na true.

#!/bin/bash

verbose=false
test "${1:-none}" = "--verbose" && verbose=true

...

Tohle by příliš nefungovalo, kdybychom chtěli přidávat další přepínače, ale nyní nám to postačí.

A nyní můžeme přidat logovací zprávy.

...

$verbose && echo "Reading current version..." >&2
echo "<p>Version:" >version.inc.html
git rev-parse --short HEAD >>version.inc.html 2>/dev/null || echo "unknown" >>version.inc.html
echo "</p>" >>version.inc.html

$verbose && echo "Generating HTML ..." >&2
pandoc --template template.html -A version.inc.html index.md >"$html_dir/index.html"
pandoc --template template.html -A version.inc.html rules.md >"$html_dir/index.html"

...

Jak výše uvedený kód funguje? Nápověda. Odpověď.

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

Co si z toho odnést? 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ů.

Ve skutečnosti, když si budete pamatovat, že mezery a wildcardy vyžadují speciální pozornost, bude to v pohodě :-).

Rozšiřování průběžného příkladu

Provedeme pouze malou změnu. Přiřazení k $html_dir nahradíme následujícím kódem.

html_dir="${html_dir:-public}"

Co se změnilo? Odpověď.

Nyní můžeme měnit chování programu dvěma způsoby. Můžeme přidat --verbose nebo upravit proměnnou html_dir. To rozhodně není příliš uživatelsky přívětivé. Měli bychom umožnit, aby se náš skript spouštěl s --html=DIR, když budeme chtít změnit výstupní adresář. K tomu se vrátíme v některém z pozdějších cvičení.

V tuto chvíli to berte jako ilustraci toho, jaké možnosti jsou k dispozici. Použití html_dir="${html_dir:-public}" je velmi jednoduchý způsob, jak mít skript přizpůsobitelnější a v mnoha případech vlastně úplně stačí.

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říkazech 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? Odpověď.

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" )"

...
# Na konci skriptu
rm -rf "$my_temp"

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"

Rozšiřování průběžného příkladu

Pro zjednodušení generování informací o verzi použijeme nahrazení příkazu.

echo "<p>Version: $( git rev-parse --short HEAD 2>/dev/null || echo unknown )</p>" >version.inc.html

Změna je poměrně malá, ale díky ní je generování souboru version.inc.html o něco kompaktnější. Čitelnost tohoto kusu kódu zlepšíme pomocí funkcí v příští části.

Funkce v shellu

Z programovacích předmětů si připomeňte, že funkce mají jeden hlavní účel.

Funkce umožňují vývojáři zavést vyšší úroveň abstrakce tím, že pojmenuje určitý blok kódu, a tím umožní lépe vystihnout záměr většího kusu kódu.

Funkce také snižují duplicitu kódu (tj. princip DRY: neopakuj se), ale to je většinou jen vedlejší efekt vytváření nových abstrakcí.

Funkce v shellu jsou poměrně primitivně definované, protože nikde neobsahují formální seznam argumentů ani specifikaci návratového typu.

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.

Rozšiřování průběžného příkladu

Do našeho příkladu přidáme několik nových funkcí, aby byl o něco užitečnější.

Začneme s logováním.

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

Spusťte vnitřní volání date samostatně, abyste viděli, co dělá (klíčové je, že + na začátku informuje date, že chceme použít vlastní formát).

A nyní nahradíme logovací volání takto.

logger=":"
test "${1:-none}" = "--verbose" && logger=log_message

$logger "Reading current version..."
...
$logger "Generating HTML ..."

Použili jsme dva triky. Nahradili jsme true/false přímým voláním naší funkce. Proto vůbec nepotřebujeme podmíněné provádění pomocí &&.

Druhým trikem je použití dvojtečky :. To je v podstatě speciální builtin, který nic nedělá. Stále se však chová jako příkaz. Takže nastavením logger na : nebo na log_message provedeme jeden z následujících příkazů:

: "Reading current version"
log_message "Reading current version"

Druhý řádek volá logger, první nedělá nic.

Voilà, naše logování je hotovo.

Generování verze můžete zabalit do rozumné funkce sami jako další příklad.

Řešení.

Sami zabalte volání Pandocu do vhodné funkce.

Řešení.

Návratové hodnoty funkcí

Volání return ukončí funkci, volitelným parametrem je její návratová hodnota (exit kód).

Pokud použijete exit v rámci funkce, ukončí se celý skript.

Následuje příklad, který zkontroluje, zda má daný soubor správný shebang Bashového skriptu.

is_shell_script() {
    test "$( head -n 1 "$1" 2>/dev/null )" = '#!/bin/bash' && return 0
    return 1
}

Protože výstupní kód posledního programu je zároveň výstupním kódem celé funkce, můžeme kód zjednodušit na následující.

is_shell_script() {
    test "$( head -n 1 "$1" 2>/dev/null )" = '#!/bin/bash'
}

Taková funkce může být použita pro řízení skriptu takto:

is_shell_script "input.sh" || echo "Warning: shebang missing from input.sh" >&2

Všimněte si, jak dobré jméno funkce zjednodušuje čtení výše uvedeného skriptu.

Stejného efektu bychom dosáhli přímým použitím následujícího kódu, ale použití funkce nám umožňuje zachytit záměr.

test "$( head -n 1 "input.sh" 2>/dev/null)" = '#!/bin/bash' || echo "Warning: shebang missing from input.sh" >&2

Lokální proměnné ve funkcích

Je dobrý nápad 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 lepší označit tyto proměnné jako local (vizte níže):

is_shell_script() {
    local filename="$1"
    test "$( head -n 1 "$filename" 2>/dev/null)" = '#!/bin/bash' )"
}
Kód je prakticky stejný. Přiřazením $1 do správně pojmenované proměnné však zvýšíme čitelnost: čtenář okamžitě vidí, že prvním argumentem je jméno souboru.

Priority příkazů

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ř. pro funkce stejného jména).

Co si z toho odnést

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.

Často je vhodné deklarovat proměnné v rámci funkcí jako local, což omezuje rozsah proměnné na danou funkci.

Přesněji řečeno, proměnná je viditelná ve funkci a ve všech funkcích z ní volaných. Lze si představit, že předchozí hodnota proměnné se při spuštění local uloží a po návratu z funkce se obnoví. To je rozdíl oproti tomu, jak to funguje ve většině 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 rouru (pipe), 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 (a také změny pracovního adresáře nejsou viditelné mimo tento blok).

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"

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.

Rozšiřování průběžného příkladu

Jako poslední změnu v našem průběžném příkladu změříme, jak dlouho trvalo jeho provedení.

K tomu použijeme date, protože tento příkaz s argumentem +%s vypíše počet sekund od začátku Epochy.

Ve skutečnosti všechny unixové systémy interně měří čas počítáním sekund od

  1. ledna 1970 (počátek Epochy) a všechna zobrazená data se z toho přepočítávají.

Proto tyto 3 řádky kolem celého skriptu zjistí počet sekund, které byly stráveny spuštěním našeho skriptu (v tuto chvíli by skript neměl trvat déle než 1 sekundu, ale časem můžeme mít více stránek nebo více dat).

#!/bin/bash

wallclock_start="$( date +%s )"

...

wallclock_end="$( date +%s )"

$logger "Took $(( wallclock_end - wallclock_start )) seconds to generate."

Úlohy k ověření vašich znalostí

Očekáváme, že následující úlohy vyřešíte ještě před příchodem na cvičení, takže se budeme moci o vašich řešeních na cvičení pobavit.

Vraťte se k příkladům ze cvičení 04 a rozhodněte, kde by přidání funkce do implementace zlepšilo čitelnost skriptu.
Vypište informace o posledním commitu; když je skript spuštěn mimo adresář Gitového projektu, jen vypíše Not inside a Git repository. Nápověda. Řešení.
Příkaz getent passwd USERNAME vypíše informace o uživatelském účtu USERNAME (např. intro) na vašem počítači. Napište příkaz, který vypíše informace o uživateli intro nebo zprávu This is not NSWI177 disk, pokud uživatel neexistuje. Řešení.

Skript vypíše na stdout obsah souboru HEADER (v pracovním adresáři).

Pokud ale je v adresáři soubor .NO_HEADER, nic vypsáno nebude (i pokud HEADER existuje).

Pokud žádný ze souborů neexistuje, program vypíše Error: HEADER not found. na standardní chybový výstup a skončí s návratovou hodnotou 1.

Jinak skript končí úspěšně.

Pro řízení programu používejte pouze && a ||, nepoužívejte if, i když tyto konstrukce v shellu znáte. Je v pořádku se na přítomnost souboru zeptat vícekrát. Soubory nebudeme měnit za běhu vašeho skriptu.

Tento příklad vám může zkontrolovat GitLab skrz automatizované testy. Uložte vaše řešení jako 06/override.sh a commitněte ho (pushněte) do GitLabu.

Skript vypíše datum modifikace (%Y) souboru zadaného jako první argument.

Datum modifikace by mělo být vypsáno ve formátu RRRR-MM-DD, pokud soubor neexistuje (nebo se vyskytne jiný problém při čtení času změny), program by měl skončit s nenulovým exit kódem.

Nápověda: stat, date.

Tento příklad vám může zkontrolovat GitLab skrz automatizované testy. Uložte vaše řešení jako 06/mod_date.sh a commitněte ho (pushněte) do GitLabu.

Vytvořte shellový skript pro jednoduché zálohování.

Skript přijme jako argument jeden název souboru a vytvoří jeho kopii v adresáři zadaném proměnnou prostředí BACKUP_DIR (nebo ~/backup, pokud není nastavena) se jmény ve tvaru RRRR-MM-DD_hh-mm-ss_~ABSOLUTNI~CESTA~K~SOUBORU.

Časové razítko se bude vztahovat k aktuálnímu datu. V absolutní cestě k původnímu souboru nahraďte / za ~ (tilda) (může se vám hodit příkaz realpath)

Skript vypíše cestu se zazálohovaným souborem na stdout.

Příklad použití:

export BACKUP_DIR=~/my_backup
cd /home/intro/my_dir
../path/to/06/backup.sh a.zip

Ukázkový výstup:

/home/intro/my_backup/2023-03-08_10-01-23_~home~intro~my_dir~a.zip

Skript můžete použít pro rychlé časové zálohování aktuální práce a čas od času zálohy promazat.

Všimněte si, že očekáváme použití cp -R, takže skript bude fungovat i pro adresáře.

Automatické testy vždy nastaví $BACKUP_DIR, aby se nenahrávaly soubory do vašeho domovského adresáře. Očekáváme, že skript pořádně otestujete sami v případech, kdy probíhá zálohování do $HOME.

Tento příklad vám může zkontrolovat GitLab skrz automatizované testy. Uložte vaše řešení jako 06/backup.sh a commitněte ho (pushněte) do GitLabu.

Vytvořte funkci shellu, která urychlí generování webu z našeho průběžného příkladu.

V tuto chvíli generujeme stránky při každém spuštění skriptu.

Vaším úkolem je přidat funkci should_generate, která bude mít jeden argument: jméno zdrojového souboru (tj. souboru .md) a vrátí (tj. nastaví její návratový kód) na 0, pokud je třeba vygenerovat soubor .html, nebo 1, pokud není třeba soubor vygenerovat.

Zda je třeba soubor vygenerovat nebo ne, zjistíme jednoduše tak, že zkontrolujeme, zda soubor existuje a zda je .md novější než .html (tedy Markdown byl upraven po posledním generování HTML a měli bychom jej obnovit). Obě tyto funkce nabízí příkaz test(1).

Očekáváme, že hlavní část programu bude změněna následujícím způsobem:

should_generate index.md && run_pandoc index.md >"index.html"
should_generate rules.md && run_pandoc rules.md >"rules.html"

Vaším úkolem je uložit do souboru 06/should_generate.sh pouze funkci should_generate. Nic dalšího tam nevkládejte, zbytek poskytneme zevnitř našich testů.

Pro testování samozřejmě definujte tuto funkci uvnitř souboru build.sh a po dokončení ji zkopírujte do souboru should_generate.sh.

Pro zjednodušení zadání předpokládáme, že oba .md i .html jsou ve stejném adresáři, a můžete také předpokládat, že vždy obdržíte jen obyčejný název souboru, tj. není potřeba řešit kontrolu adresar/index.html pro vstup adresar/index.md.

Nápověda: basename index.md .md.

Tento příklad vám může zkontrolovat GitLab skrz automatizované testy. Uložte vaše řešení jako 06/should_generate.sh a commitněte ho (pushněte) do GitLabu.

Učební výstupy

Učební výstupy podávají zhuštěný souhrn základních konceptů a dovedností, které byste měli umět vysvětlit a/nebo použít po každém cvičení. Také obsahují absolutní minimum, které je potřebné pro pochopení navazujících cvičení (a dalších předmětů).

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, co je to proměnná prostředí

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

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

  • vysvětlit, jak je proměnné $PATH používána v shellu

  • vysvětlit, jak změna $PATH ovlivní spouštění programů

  • vysvětlit jak funguje expanze v shellu a rozdělení na argumenty příkazové řádky

  • volitelné: vysvětlit, proč $PATH obvykle neobsahuje aktuální adresář

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 …

  • použít Pandoc pro převod mezi různými textovými formáty

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

  • vyhodnocovat matematické výrazy v shellu pomocí konstrukce $(( ))

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

  • skládat programy pomocí && a || v shellových skriptech

  • vytvářet a používat funkce v shellu

  • využít subshell pro seskupení několika příkazů

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

  • volitelné: vytvoření vlastních šablon pro Pandoc