Cvičení: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14.
- Pandoc
- Průběžný příklad
-
Použití
&&
a||
(logická skladba programu) - Proměnné v shellu
- Nahrazování příkazů (neboli zachytávání stdout do proměnné)
- Funkce v shellu
- Subshell a viditelnost proměnných (scoping)
- Aritmetika v shellu
- Další příklady
- Úlohy před cvičením (deadline: začátek vašeho cvičení, týden 20. března - 24. března)
- Úlohy po cvičení (deadline: 9. dubna)
- Učební výstupy
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.
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).
$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.
Za několik týdnů si povíme, proč je lepší hledat Python v $PATH
a ne
používat přímo /usr/bin/python3
.
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.
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? Hint. Answer.
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ě :-).
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?Answer.
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? Answer.
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.
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. Solution.
Sami zabalte volání Pandocu do vhodné funkce.Solution.
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.
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.
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.
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."
Další příklady
Více příkladů, na kterých si můžete vyzkoušet své znalosti předtím, než se pustíte do hodnocených úkolů.
Úlohy před cvičením (deadline: začátek vašeho cvičení, týden 20. března - 24. března)
Následující úlohy musí být vyřešeny a odevzdány před příchodem na vaše cvičení. Pokud máte cvičení ve středu v 10.40, soubory musí být nahrány do vašeho projektu (repozitáře) na GitLabu nejpozději ve středu v 10.39.
Pro virtuální cvičení je deadline úterý 9:00 (každý týden, vždy ráno, bez ohledu na možné státní svátky apod.).
Všechny úlohy (pokud není explicitně uvedeno jinak) musí být odevzdány přes váš repozitář na úkoly. Pro většinu úloh existují automatické testy, které vám mohou pomoci zkontrolovat úplnost vašeho řešení (zde je popsáno jak číst jejich výsledky).
06/override.sh
(25 bodů, skupina shell
)
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.
06/mod_date.sh
(25 bodů, skupina shell
)
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
.
06/feedback.txt
(50 bodů, skupina admin
)
Vzhledem k tomu, že se blížíme k polovině semestru, rádi bychom získali zpětnou vazbu (abychom ji mohli uplatnit ve zbytku semestru).
Vyplňte prosím následující formulář a po jeho vyplnění vytvořte ve svém
projektu prázdný soubor 06/feedback.txt
.
Odkazy míří jen na různé jazykové překlady téhož dotazníku, vyplňte, prosím, pouze jeden z nich.
- Dotazník anglicky: https://forms.gle/67H1vNcdfr2h7kA76
- Dotazník česky: https://forms.gle/BejxjPAqVdhJAt3e8
(Nevidíme žádný jiný jednoduchý způsob, jak zajistit, aby průzkum zůstal anonymní.)
Úlohy po cvičení (deadline: 9. dubna)
Očekáváme, že následující úlohy vyřešíte po cvičení, tj. poté, co budete mít zpětnou vazbu k vašim řešením úloh před cvičením.
Všechny úlohy (pokud není explicitně uvedeno jinak) musí být odevzdány přes váš repozitář na úkoly. Pro většinu úloh existují automatické testy, které vám mohou pomoci zkontrolovat úplnost vašeho řešení (zde je popsáno jak číst jejich výsledky).
06/backup.sh
(50 bodů, skupina admin
)
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.
$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
.
06/should_generate.sh
(50 bodů, skupina shell
)
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
.
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