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.
Základní použití
Prosím, naklonujte náš repozitář s
příklady
(nebo si ho git pull
ně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í:
-
$HOME
obsahuje 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šejteecho $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.
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
.
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í.
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 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.
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' )"
}
$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.
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.
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
- 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.
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