Unixový shell se dá použít nejen pro interaktivní práci, ale i jako programovací jazyk. Jednotlivé příkazy můžeme zapsat do souboru a tento soubor nechat interpretovat příkazovým interpretem. Tento soubor je potom označován jako skript nebo shellový skript.
Shell je vynikající lepítko a jako programovací jazyk vyniká hlavně tehdy, když s jeho pomocí slepíte a sorchestrujete dohromady několik jiných programů, aby dělaly to, co zrovna potřebujete. Můžete si tak zautomatizovat a zjednodušit každodenní práci. Inspirovat se můžete třeba na mém Gitlabu.
Seskupení příkazů¶
Shell nabízí dva způsoby jak seskupit více příkazů do jednoho celku. Liší se tím, zda se příkazy vykonají v podřízeném nebo v aktuálním shellu.
( ) — podřízený shell¶
Příkazy uzavřené v kulatých závorkách se vykonají v podřízeném shellu (subshell). Veškeré změny proměnných i aktuálního adresáře zaniknou s ukončením subshell-u. Je to jako spustit zcela nové prostředí. Potomek může zdědit něco od svého rodiče, ale přenos zpět od potomka k rodiči není možný. Proměnné lze exportovat do podřízeného shellu, ale zpět k rodiči to nelze.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | |
{ } — aktuální shell¶
Příkazy uzavřené ve složených závorkách se vykonají v aktuálním shellu. Změny proměnných i aktuálního adresáře přetrvají:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
Syntaxe složených závorek
Na rozdíl od kulatých závorek musí být za { mezera a poslední příkaz před } musí být ukončen středníkem (nebo novým řádkem). Tohle nejde:
{příkaz}. Musí to vypadat takto: { příkaz; }
Seskupení jako celek¶
Skupina příkazů v ( ) nebo { } se chová jako jeden celek — má jednu
návratovou hodnotu (návratovou hodnotu posledního příkazu). Díky tomu je
možné skupinu kombinovat s operátory && a || a nahradit tak jednoduché podmínky if:
1 | |
Pokud mkdir uspěje, vykonají se všechny příkazy ve složených závorkách.
Pokud selže, celá skupina se přeskočí.
1 | |
Obě formy seskupení se dají kombinovat s přesměrováním. Výstup celé skupiny příkazů je tak možné přesměrovat najednou:
1 | |
Podobné je to se spuštěním na pozadí:
1 | |
Pole a seznamy¶
Zsh má velmi silnou podporu polí (arrays). Indexuje se od 1 (na rozdíl od Bashe).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
Pole se hodí všude tam, kde je potřeba pracovat se seznamem hodnot — soubory, hostitelé, argumenty skriptu…
1 2 3 4 5 6 7 8 | |
Typický případ — procházení pole serverů:
1 2 3 4 5 | |
| Syntaxe | Význam |
|---|---|
$ovoce[2] |
prvek na indexu 2 |
$ovoce[-1] |
poslední prvek |
$ovoce[2,4] |
rozsah (slice) — prvky 2 až 4 |
$#ovoce |
počet prvků |
"${ovoce[@]}" |
všechny prvky jako oddělené řetězce (bezpečné pro mezery) |
"${ovoce[*]}" |
všechny prvky jako jeden řetězec (spojené $IFS) |
${ovoce:#vzor} |
odfiltruje prvky shodující se se vzorem |
${(j:,:)ovoce} |
join — spojí prvky oddělovačem (zde ,) |
${(s:,:)retezec} |
split — rozdělí řetězec na pole |
$IFS — oddělovač polí¶
$IFS (Internal Field Separator) je proměnná, která určuje, jakými znaky se
rozdělují slova při expanzi. Výchozí hodnota je mezera, tabulátor a nový řádek.
Uplatní se zejména u "${pole[*]}" — prvky se spojí prvním znakem $IFS:
1 2 3 4 5 6 7 | |
Změna $IFS se běžně dělá lokálně, aby neovlivnila zbytek skriptu:
1 2 3 4 5 6 | |
$IFS funguje i opačně — při rozdělení řetězce na pole. Stačí přiřadit
řetězec do pole přes =( ):
1 2 3 4 5 6 7 8 | |
Hlavička¶
Aby bylo možné skript jednoduše spouštět je nutné opatřit ho hlavičkou: První
řádek začíná dvojicí znaků #! a pokračuje cestou k příkazovému interpretu:
1 | |
Dvojice znaků #! uvozuje tzv. shebang. Tento
mechanizmus je zcela obecný a uplatní se stejně i pro skripty v jiný jazycích. Například
pro programovací jazyk Python by hlavička mohla vypadat takto:
1 | |
nebo takto:
1 | |
(ponechám zvídavého čtenáře, aby si zjistil, co dělá příkaz/program env.)
Pokud by skript shebang na prvním řádku neměl, bylo by nutné explicitně na příkazovém řádku uvést interpret. Například:
1 | |
nebo
1 | |
Aby se skript choval jako každý jiný program je jeho správná hlavička podmínkou nutnou
nikoli postačující. Skript musí být spustitelný (chmod a+x ~/bin/mujSkript.sh) a musí
být umístěn tak, aby ho příkazový interpret
našel.
Pokud je vše toto splněno může ho uživatel spouštět, jako by to byla běžná součást systému.
1 | |
Pro Zsh nebo Bash se hned za shebang doporučuje přidat:
1 | |
set -e(errexit) — skript se okamžitě ukončí, pokud jakýkoli příkaz selže. Bez toho Zsh tiše pokračuje dál i po chybě.set -u(nounset) — selže při použití nenastavené proměnné. Bez toho se nenastavená proměnná expanduje na prázdný řetězec — klasický zdroj pohrom:rm -rf $TMPDIR/se s prázdným$TMPDIRexpanduje narm -rf /.set -o pipefail— pipeline selže, pokud selže jakýkoli příkaz v ní, nejen poslední.
Pro ladění skriptů se hodí přepínač -x — zsh před každým příkazem
(na řádcích označených +) vypíše, co spustí (po expanzi proměnných).
-x můžete použít explicitně:
1 | |
… nebo do shebang hlavičky
1 | |
… nebo jen pro konkrétní část skriptu:
1 2 3 | |
Příkaz . (tečka) a source¶
Ano . je opravdu příkaz. Výše popsaný způsob spouštění má za následek, že námi
vytvořený skript je spuštěn v podřízeném shell-u. To mimo jiné znamená, že
změny provedené v proměnných prostředí nebudou po dokončení skriptu viditelné.
Použijeme-li příkaz .:
1 2 3 | |
…bude shell postupně číst soubor commands a jednotlivé příkazy vykonávat v aktuálním
shellu. Výsledek bude stejný jako kdyby uživatel zadával příkazy rovnou
v interaktivním prostředí.
Příkaz source je téměř to stejné jako .. Rozdíl je následující:
-
.hledá v$PATHa pokud souborcommandsnení v$PATHje třeba zadat explicitně celou cestu. -
sourcehledá nejprve v pracovním adresáři a až potom v$PATH; není nuté zadávat explicitně celou cestu:
1 2 3 | |
Poznámky¶
Poznámky se zapisují za znak #. Vše od znaku # do konce řádku je
interpretem ignorováno. Názorný příklad je vidět o pár řádků výše…
Přebírání parametrů (argumentů)¶
Skriptům lze stejně jako programům předávat parametry. Ty jsou uvnitř skriptu dostupné pomocí speciálních proměnných.
| Proměnná | Význam |
|---|---|
$0 |
jméno skriptu |
$1 |
první parametr |
$2 |
druhý parametr |
$n |
n-tý parametr, n nabývá hodnoty 1 až 9 |
${n} |
libovolný n-tý parametr |
$# |
počet parametrů |
$* |
seznam všech parametrů — stejné jako "$1 $2 $3 ..." |
$@ |
seznam všech parametrů — stejné jako "$1" "$2" "$3" ... |
Příkaz shift N vymaže prvních N parametrů a posune
význam proměnných $n a ${n}
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
1 2 3 4 5 6 7 | |
Přepínače¶
Skripty mohou přijímat přepínače jako běžné programy (-v, --output soubor
apod.).
Z RAW pohledu není rozdíl mezi přepínačem a argumentem. Vše se dostane do skriptu jako
poziční parametr a je potřeba explicitně říct: “hele, tak to, co začíná znakem - je
přepínač”. Naštěstí je to všechno už vyřešené a není třeba to dělat ručně.
Zsh má pro zpracování přepínačů vestavěnou funkci
zparseopts.
Reklamu této funkci dělám proto, že práce s ní mi přijde mnohem zábavnější, než práce
s klasickým programem getopt nebo vestavě klasickou funkcí
getopts:
1 2 3 4 | |
-D— odstraní zpracované přepínače z$@, zbytek (poziční argumenty) zůstane-E— přepínače a poziční argumenty mohou být v libovolném pořadí-F— neznámý přepínač způsobí chybu (dostupné od Zsh 5.8)- přepínač bez
:je příznak (ano/ne), s:očekává hodnotu
Výsledky jsou uloženy v polích — hodnotu dostaneme přes index [2]:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
1 2 3 4 | |
Návratová hodnota¶
Jak bylo uvedeno každý program, který ukončí svou činnost korektně vrací jako svou
návratovou hodnotu 0. Pokud
se v průběhu programu objeví chyba, dává o tom program vědět svou nenulovou návratovou hodnotou.
Okamžité ukončení skriptu s konkrétní návratovou hodnotou vyvolá příkaz
exit N.
1 2 3 4 5 6 7 8 9 10 11 12 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
Funkce¶
Shell umožňuje vytvářet funkce. Funkce se chová stejně jako samostatný skript, včetně předávání parametrů a návratové hodnoty.
Ukončení funkce s konkrétní návratovou hodnotou je zajištěna příkazem
return N.
Bacha
Funkci je nuté ukončít pomocí příkazu return; nikoli příkazu exit — ten totiž
ukončí celý skript.
Sada proměnných pro předávání parametrů funguje stejně jako u skriptu.
Syntaxe funkce v shellu:
1 2 3 4 5 | |
Příklad:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
(Aritmetické výrazy $(( )) jsou popsány v článku o
expanzích.)
1 2 3 4 | |
Lokální proměnné¶
Proměnné uvnitř funkce jsou ve výchozím stavu globální — mohou přepsat
proměnné volajícího skriptu. Pomocí příkazu local se proměnná stane
lokální a po návratu z funkce zanikne:
1 2 3 4 5 6 7 8 | |
Podmínky a cykly¶
Užití podmínek a cyklů není vázáno jen na skripty. Stejně tak je možné je použít v interaktivní práci.
Pravdivostní “VÝRAZy”¶
Na následujících řádcích u podmínek if a cyklů while je často použito zobecňující
slovo VYRAZ:
- VYRAZ je příkaz nebo posloupnost příkazů oddělených pomocí metaznaku
||(logický součet — nebo) nebo metaznaku&&(logický součin — and). - O pravdivosti nebo nepravdivosti VÝRAZu rozhoduje jeho návratová hodnota.
- VYRAZ je možné negovat pomocí znaku
!na jeho začátku.
Aby mohl if nebo while rozhodnout o pokračování své činnosti
potřebuje nějakou pravdivostní hodnotu. Je velmi klíčové pochopit, že
tato pravdivostní hodnota je dána návratovou hodnotou příkazu.
Jako VYRAZ se velice často používá konstrukce [[ ]] — rozšířená podmínka
zabudovaná přímo do shellu. Starší varianta [ ] (synonymum externího programu
test) stále funguje, ale [[ ]] je bezpečnější a mocnější. Protože:
- proměnné nemusí být v uvozovkách —
[[ $x = abc ]]funguje i pokud$xje prázdné - podporuje
=~pro porovnání s regulárním výrazem - logické operátory
&&a||fungují přímo uvnitř závorek
| Operace | [[ ]] |
starý [ ] |
|---|---|---|
| Řetězec je prázdný | [[ -z $x ]] |
[ -z "$x" ] |
| Řetězce se rovnají | [[ $x == abc ]] |
[ "$x" = abc ] |
| Číslo větší než | [[ $n -gt 5 ]] |
[ "$n" -gt 5 ] |
| Soubor existuje | [[ -f $f ]] |
[ -f "$f" ] |
| Regex shoda | [[ $x =~ ^[0-9]+$ ]] |
— |
- Přehled všech přepínačů:
man zshmisc.
Onen klíčový bod je v tom, že pokud zapíšete
1 | |
… tak [[ je příkaz a $1 == --help ]] jsou jeho čtyři parametry. Příkaz [[
otestuje jestli řetězec v $1 odpovídá --help a svou návratovou hodnotou řekne,
jestli je to pravda. ]] je vlastně “jen” ukončovač.
1 2 3 4 5 6 7 | |
Jako VYRAZ je ale možné použít libovolný jiný příkaz. Například pokud potřebuji
otestovat jestli soubor obsahuje konkrétní řetězec může to vypadat takto:
1 2 3 4 | |
Příkaz grep nic nevypíše, protože jeho výstup je přesměrován
do černé díry, ale jeho návratová hodnota rozhodne o tom, zda se vykoná command1
a command2.
(( )) — aritmetická podmínka¶
Pro číselné porovnání lze místo [[ ]] použít (( )). Chová se jako příkaz —
je pravdivý (návratová hodnota 0), pokud je výsledek výrazu nenulový:
1 2 3 4 5 6 7 8 9 | |
Uvnitř (( )) lze také přiřazovat bez $:
1 2 | |
Rozdíl oproti $(( ))
$(( )) je expanze — vrací hodnotu a nahrazuje se textem. (( )) je příkaz — vrací exit code a používá se tam, kde se čeká
příkaz (podmínka, smyčka).
if — podmíněné vykonání¶
V následujících ukázkách jsem zvolil způsob zápisu, který mi připadá přehledný,
ale platí, že středník ; může být zaměněn za konec řádku a naopak.
Syntaxe obecně vypadá takto:
1 2 3 4 | |
1 2 3 4 5 6 7 | |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
case — shoda se vzorem¶
Vícenásobné větvení pomocí vzorů ukážeme na příkladu:
1 2 3 4 5 6 7 8 9 10 11 | |
Větev se ukončuje jedním z těchto ukončovačů:
| Ukončovač | Chování |
|---|---|
;; |
konec větve, pokračuj za esac |
;& |
fall-through — vykonej i tělo následující větve (bez testování vzoru) |
;| |
pokračuj testovat další vzory (může se shodovat více větví) |
- Znak
|slouží jako oddělovač vzorů ve významu nebo. - Pro vzory platí stejná pravidla jako pro expanzi jmen souborů — tedy Globbing.
Příklad ;| — slovo může spadnout do více větví najednou:
1 2 3 4 5 6 7 8 | |
for — pro každou položku seznamu¶
Syntaxe cyklu for vypadá takto:
1 2 3 4 5 | |
Nejprve se expanduje SEZNAM. PROMENNA nabývá při každé iteraci postupně jedné z hodnot SEZNAMu.
1 2 3 | |
Jako seznam může ale být uvedeno neúplné jméno souboru
1 2 3 4 | |
… nebo program, který seznam vrátí.
1 2 3 | |
Příklad s polem byl už uveden výše.
Pro čistě numerické iterace lze použít C-style zápis:
1 2 3 | |
while, until — opakování na základě podmínky¶
Syntaxe cyklu while a until vypadá naprosto stejně. Jediný rozdíl je v
podmínce. Iterace cyklu while se vykoná pokud podmínka platí. Iterace cyklu
until se vykoná pokud podmínka neplatí.
K okamžitému ukončení těla cyklu slouží příkaz break k přeskočení zbytku těla
cyklu příkaz continue.
1 2 3 4 5 | |
VYRAZ má stejný význam jako u podmínky if.
Note
Všimněte si, že zde nejsou žádné [[ ]] ani [ ]. ping svou návratovou
hodnotou zdělí, jestli se spojení povedlo.
1 2 3 4 5 | |
Standardní vstup z těla skriptu¶
Častokrát požadujeme, aby program spuštěný z těla skriptu byl “nakrmen”
vstupními daty. Pro přesměrování standardního vstupu ze souboru sice
slouží metaznak < ale vytváření samostatného souboru pro vstupní data není
vždy efektivní. Proto použijeme metaznak <<.
1 2 3 4 5 6 | |
- OMEZOVAC je libovolná posloupnost znaků.
- Ukončovací OMEZOVAC musí být uveden na samostatném řádku.
- Ve vstupních datech je možné zapsat i proměnné.
1 2 3 4 | |
<<EOF vs <<'EOF'¶
Pokud je omezovač bez uvozovek, proměnné a příkazy se expandují. Pokud je omezovač v jednoduchých uvozovkách (nebo dvojitých), vše je bráno doslova — žádná expanze se neprovede:
1 2 3 4 5 6 7 8 9 10 11 | |
<<'EOF' se hodí například pro generování skriptů nebo konfiguračních
souborů, kde $ má být součástí výstupu.
<<-EOF — odsazení tabulátory¶
Pomlčka před omezovačem způsobí, že shell odstraní úvodní tabulátory (nikoli mezery) z každého řádku. Heredoc tak může být odsazen spolu s okolním kódem a přitom nevyžaduje, aby omezovač byl na začátku řádku:
1 2 3 4 5 6 | |
eval — vykonání řetězce jako příkazu¶
Příkaz eval vezme svůj argument jako řetězec, provede na něm expanzi
proměnných a výsledek vykoná jako shellový příkaz:
1 2 3 | |
Typické použití je dynamická dereference proměnné — přístup k proměnné, jejíž jméno je uloženo v jiné proměnné:
1 2 3 4 | |
Bezpečnostní riziko
eval vykoná cokoliv — nikdy ho nepoužívejte s nevalidovaným
vstupem od uživatele nebo z externího zdroje.
trap — reakce na signály a ukončení¶
Příkaz trap registruje příkaz, který se spustí při přijetí signálu
nebo při ukončení skriptu:
1 | |
Nejpoužívanější signály:
| Signál | Kdy |
|---|---|
EXIT |
při jakémkoliv ukončení skriptu |
INT |
Ctrl+C |
TERM |
kill (výchozí signál) |
ERR |
při selhání příkazu (s set -e) |
Nejčastější případ — cleanup dočasného souboru při ukončení skriptu, ať už normálním nebo po chybě:
1 2 3 4 5 6 7 8 9 | |
trap s EXIT se spustí vždy — i při chybě nebo přerušení pomocí
Ctrl+C — takže dočasné soubory nezůstanou na disku.
read — čtení vstupu¶
Příkaz read čte jeden řádek ze standardního vstupu a uloží ho do
proměnné. Používá se pro interaktivní skripty i pro zpracování souborů:
1 | |
| Přepínač | Popis |
|---|---|
-r |
neinterpretovat \ jako pokračování řádku (doporučuje se vždy) |
-p |
zobrazit výzvu (prompt) před čtením |
-s |
skrytý vstup — nezobrazovat zadané znaky (hesla) |
-t |
timeout v sekundách; pokud uživatel nezadá nic, read selže |
-A |
uložit slova do pole (rozdělení podle $IFS) |
1 2 | |
1 2 3 | |
Velmi časté je čtení řádků ze souboru v cyklu while:
1 2 3 | |
Nebo zpracování výstupu příkazu:
1 2 3 | |