Úvod do skriptování v shellu
Text je určen pro samostudium předmětu XVPP1 v zimním semestru 2026. Obsahem je skriptování v unixovém shellu Bash. Předpokládá základní znalosti unixových prostředí a práce se shellem, zhruba v rozsahu předmětu XUNIX.
Text je napsán přehledově, strukturou se blíží [bible]. Ke studiu důrazně doporučuji i jiné zdroje, například právě [bible] nebo [practical]. Důležitou součástí studia je psaní vlastních skriptů a experimentování s nimi. Na konci textu vybrané úkoly, další úkoly jsou v literatuře.
V ukázkách interaktivní práce se shellem označuje znak > na začátku řádku
prompt shellu; text za ním je to, co uživatel zadává, řádky bez promptu jsou
výstupem. Výrazem [...] značíme, že výpis pokračuje na další řádky.
Základy
V této kapitole se seznámíme se základními stavebními kameny skriptů: jak skript vytvořit a spustit, jak v něm oddělovat a kombinovat příkazy, jak pracovat s proměnnými, co je podshell, jak shell provádí expanze, jak se přesměrovávají standardní proudy a jak se v shellu počítá.
- Vytvoření skriptu a jeho spuštění
- Operátory řídící běh skriptu
- Proměnné
- Podshell
- Expanze
- Přesměrování a roura
- Aritmetika
Vytvoření skriptu a jeho spuštění
Skript je textový soubor, který chceme spustit pomocí nějakého interpretu. Typicky jej lze spustit pomocí
> interpret script
v našem případě tedy
> bash script.sh
Alternativou je dát na začátek souboru řádek, jehož první dva symboly jsou #!
(a tvoří tzv. shebang), a zbytek prvního řádku je tvořen příkazem, který daný
interpret spustí, typicky přímo cestou k jeho binárce.1 V našem případě
skript používající shebang vypadá například následovně.
#!/bin/bash
#
echo "hello sailor!!"
echo "second line"
Symbol # je mimo prvního řádku brán jako začátek komentáře. Skript potom
obsahuje dva další řádky, na každém z nich je zapsán jeden příkaz echo. Řádky
jsou interpretovány postupně seshora, nejdříve je spuštěn příkaz na řádku 3
a poté na řádku 4. Spuštění skriptu si můžeme představit jako spuštění nového
shellu a v něm automatické zadávání příkazů z jednotlivých řádků.
Abychom jej mohli spustit, nastavíme skriptu příslušné právo, a poté jej spustíme známým způsobem
> chmod +x script.sh
> ./script.sh
hello sailor!!
second line
Co se stane v případě, že vytvoříme spustitelný skript bez shebangu a spustíme jej, závisí na použitém shellu a operačním systému.
Alternativou k /bin/bash v shebangu je nalezení spustitelného souboru shellu
pomocí programu env. Tento způsob tvoří skript přenositelným na systémy, které
bash nemají nainstalován v adresáři /bin.
#!/usr/bin/env bash
V ukázkách skriptů ve zbytku budeme shebang vynechávat, budeme ale předpokládat, že se v každém skriptu nachází.
-
Tento mechanismus je poskytován jaderným systémem spouštění programů. ↩
Operátory řídící běh skriptu
Při provádění skriptu jsou příkazy spouštěny po přečtení znaku nového řádku (nebo konce souboru). Na jeden řádek lze umístit i více příkazů tak, že mezi ně umístíme oddělovač. Použije-li se středník, jsou příkazy spuštěny za sebou až v momentě, kdy shell přečte znak nového řádku.
cmd1; cmd2; cmd3;
Příkazy lze také oddělovat pomocí &. Při vykonávání
cmd1 & rest
spustí shell cmd1 na pozadí a přejde ke zpracování rest.
Operátory && a || odpovídají líné konjunkci a disjunkci. Při vykonávání
cmd1 && rest
shell spustí cmd1 a pokračuje k rest pouze pokud cmd1 skončil úspěšně (viz
návratové hodnoty). Naopak při
cmd1 || rest
shell spustí cmd1 a pokračuje k rest pouze pokud cmd1 skončí neúspěšně.
Příkazy (jeden i více) lze také seskupovat do skupin pomocí závorek ().
Příkazy z jedné skupiny pak shell spustí ve své kopii, kterou za tímto účelem
vytvoří. Na skupinu se tak shell dívá atomicky (jako na jeden příkaz). Například
po
> (cmd1; cmd2) & (cmd3; cmd4) &
paralelně na pozadí běží dva podshelly, v prvním jsou za sebou vykonány cmd1
a cmd2 a ve druhém cmd3 a cmd4.
Proměnné
Na proměnné se lze dívat tak, že obsahují řetězce, že všechny proměnné existují a mají jako defaultní hodnotu přiřazen prázdný řetězec. Přirozeně ovšem považujeme za existující pouze proměnné, které mají přiřazenu jinou hodnotu.
Proměnnou X bash expanduje na její hodnotu pomocí expanze výrazu $X nebo
výrazu ${X}. Například hodnotu proměnné PATH zobrazíme
> echo $PATH
/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:
> echo ${PATH}
/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin:
V textu se budeme držet způsobu se závorkami. Expanze ${} je pokročilejší
mechanismus, který umí i doplňování výchozích hodnot, ořezávání a nahrazování
podřetězců; věnuje se mu podkapitola
Expanze parametrů.
Vypíšeme-li obsah neexistující proměnné, dostaneme prázdný řetězec
> echo ${DONOTEXIST}
Proměnným přiřazujeme hodnotu pomocí operátoru =. Pozor, nesmí okolo něj být
mezery.
> FOO=uname
> echo ${FOO}
uname
Následující spustí utilitku uname
> ${FOO}
Linux
Chceme-li do proměnné přiřadit hodnotu s mezerami, musíme ji napsat do uvozovek
(") nebo apostrofů (').
> FOO='uname -a'
> echo ${FOO}
uname -a
> ${FOO}
Linux phoenix 4.9.0-19-686-pae #1 SMP Debian 4.9.320-2 (2022-06-30) i686 GNU/Linux
V systému obvykle existuje řada systémových proměnných, které lze považovat za
součást konfigurace systému a shellu. Například výše uvedená proměnná PATH
obsahuje dvojtečkami oddělený seznam adresářů, ve kterých se vyhledávají
spustitelné soubory do shellu zadaných programů. Seznam nastavených proměnných
lze zobrazit pomocí1
> set
BASH=/bin/bash
[...]
(Výrazem [...] značíme, že výpis pokračuje na další řádky.)
-
Alternativy:
env,printenvzobrazí pouze systémové proměnné. ↩
Podshell
Výraz
$(command)
vede ke spuštění příkazu command a nahrazení výrazu standardním výstupem
vzniklým tímto spuštěním. Například
> echo date
date
> echo $(date)
Wed Aug 23 09:44:24 CEST 2023
Na prvním řádku echo vypíše řetězec date. Na druhém řádku ovšem shell spustí
program date a jeho výstup umístí jako argument echo, takže potom vlastně
spouštíme
> echo Wed Aug 23 09:44:24 CEST 2023
Wed Aug 23 09:44:24 CEST 2023
Typické použití ve skriptech je přiřazení výsledku do proměnné
> TODAY=$(date +%A)
> echo Today is ${TODAY}
Today is Wednesday
Technicky shell spouští nový proces shellu, tzv. podshell, ve kterém příkaz
spustí. V podshellu tak neexistují proměnné vytvořené v rodičovském shellu.1
Alternativní syntax je vložení command mezi zpětné apostrofy (`). Tuto
syntax nedoporučuji a v textu ji používat nebudeme.
-
Toto omezení lze různými způsoby obejít, například pomocí exportování proměnných. ↩
Expanze
Tato kapitola obsahuje podrobnější informace k expanzi. Protože odkazuje i do pozdějších kapitol, je vhodné číst ji vícekrát (poprvé lze pasáže odkazující vpřed přeskočit).
Shell před vlastním provedením příkazu provádí tzv. expanze. Expanzi si lze představit jako nahrazení nějakého výrazu jiným výrazem. Příkaz, který se nakonec spustí, tak může vypadat výrazně jinak, než jak jsme jej zapsali.
Expanze nejsou jeden mechanismus, ale posloupnost fází, které shell provádí v pevně daném pořadí. Řádek příkazu prochází těmito fázemi zleva doprava a v tomto pořadí:
- expanze závorek —
{a,b,c},{1..10}(viz Expanze závorek) - expanze vlnovky —
~,~uzivatel(viz Expanze vlnovky) - expanze parametrů a proměnných —
${var},${var:-default},${var%.txt}(viz Expanze parametrů) - aritmetická expanze —
$(( 2 + 3 ))(viz Aritmetická expanze) - substituce příkazů —
$(date)(viz Substituce příkazů a procesů) - dělení slov — rozdělení výsledku podle
IFS(viz Dělení slov a IFS) - expanze jmen souborů —
*.txt,zaloha?.tar(viz Expanze jmen souborů) - odstranění uvozovek — odstranění uvozovek a apostrofů, které nevznikly expanzí (viz Uvozování)
Fáze 3, 4 a 5 probíhají v jednom průchodu zleva doprava, přesto je uvádíme jako tři samostatné položky.1
Co která fáze potlačí
| fáze | syntax | potlačí '…' | potlačí "…" |
|---|---|---|---|
| expanze závorek | {a,b} | ano | ano |
| expanze vlnovky | ~, ~user | ano | ano |
| expanze parametrů | ${var} | ano | ne |
| aritmetická expanze | $(( … )) | ano | ne |
| substituce příkazů | $( … ) | ano | ne |
| dělení slov | podle IFS | ano | ano |
| expanze jmen souborů | *, ?, […] | ano | ano |
Apostrofy tedy potlačí všechny expanze, uvozovky potlačí expanzi závorek, vlnovky, dělení slov a expanzi jmen souborů, ale expanzi proměnných, aritmetiky a příkazů ponechají. Podrobně se uvozování věnuje samostatná podkapitola.
Expanze se neopakuje
Častým omylem je představa, že shell na výsledek expanze pustí celý mechanismus znovu. Není to tak: na výsledek expanze parametrů, aritmetické expanze a substituce příkazů se aplikuje pouze dělení slov a expanze jmen souborů (fáze 6 a 7), tedy jen ty fáze, které v pořadí následují.
Předpokládejme, že v aktuálním adresáři existují soubory a.txt a b.txt.
> FOO='*.txt'
> echo ${FOO}
a.txt b.txt
> echo "${FOO}"
*.txt
> echo '${FOO}'
${FOO}
První echo dostane jako argument a.txt a b.txt: hodnota proměnné se
expandovala ve fázi 3 na *.txt a tento výsledek prošel ještě fází 7, expanzí
jmen souborů. Druhé echo má argument *.txt, protože uvozovky sice nechají
proběhnout expanzi proměnné, ale potlačí expanzi jmen souborů. Pro třetí echo
neproběhne nic.
Že se opravdu nespouští celý mechanismus znovu, ukazuje následující příklad.
> BAR=hodnota
> FOO='${BAR}'
> echo ${FOO}
${BAR}
Výsledek expanze ${FOO} je řetězec ${BAR}, ale ten už se podruhé
neexpanduje — fáze 3 v tu chvíli proběhla. Chceme-li přesto expandovat obsah
proměnné jako výraz, musíme to udělat explicitně, například nepřímou expanzí
${!FOO} (viz Expanze parametrů) nebo příkazem eval.
Jak si expanze prohlédnout
Nejjednodušší nástroj je set -x, který vypisuje příkazy po expanzi (viz
podkapitola set):
> set -x
> FOO='*.txt'
+ FOO='*.txt'
> echo ${FOO}
+ echo a.txt b.txt
a.txt b.txt
Podrobněji se ladění věnuje poslední podkapitola.
-
Ve skutečnosti probíhá před 1. ještě dříve substituce aliasů a v interaktivním shellu expanze historie (
!!,!$). Ve skriptech nás nezajímají: aliasy jsou v neinteraktivním shellu vypnuté a expanze historie také. ↩
Expanze závorek
Expanze závorek (anglicky brace expansion) je první fází expanzního mechanismu. V základu z výrazu ve složených závorkách vytvoří seznam slov.
> echo {a,b,c}
a b c
Před závorkami i za nimi může být text, který se přidá ke každému vzniklému slovu jako předpona a přípona.
> echo soubor{1,2,3}.txt
soubor1.txt soubor2.txt soubor3.txt
> echo /var/{log,cache,tmp}
/var/log /var/cache /var/tmp
Uvnitř závorek nesmí být neuvozené mezery, jinak shell výraz nechá tak, jak je. Prázdná položka je povolená:
> echo soubor{,.bak}
soubor soubor.bak
Výraz cp soubor{,.bak} se expanduje na cp soubor soubor.bak.
Rozsahy
Druhou formou je rozsah {od..do}, který funguje pro čísla i písmena.
> echo {1..5}
1 2 3 4 5
> echo {a..e}
a b c d e
> echo {5..1}
5 4 3 2 1
Čísla lze doplnit nulami zleva, stačí nulu napsat do meze. Toho využijeme například při generování jmen souborů pevné šířky.
> echo {01..05}
01 02 03 04 05
> echo foo{0001..0003}.txt
foo0001.txt foo0002.txt foo0003.txt
Volitelný třetí člen udává krok.
> echo {0..20..5}
0 5 10 15 20
Vnořování a kombinace
Závorky lze vnořovat a psát za sebe; výsledkem je kombinace všech možností.
> echo {a,b}{1,2}
a1 a2 b1 b2
> echo {zaloha,archiv}-{2023,2024}.tar.gz
zaloha-2023.tar.gz zaloha-2024.tar.gz archiv-2023.tar.gz archiv-2024.tar.gz
> mkdir -p projekt/{src,doc,test/{unit,integration}}
Expanze závorek nebere v úvahu souborový systém
Toto je hlavní rozdíl proti expanzi jmen souborů. Expanze
závorek je čistě textová operace, která proběhne bez ohledu na to, jestli
vzniklá jména existují. Proto můžeme pomocí závorek soubory vytvářet, kdežto
pomocí * můžeme pouze odkazovat na soubory, které už existují.
> touch {a,b,c}.txt # vytvori tri soubory
> echo *.txt
a.txt b.txt c.txt
> touch *.txt # NEvytvori nic noveho, jen zaktualizuje casy
Protože je expanze závorek první v pořadí, její výsledek prochází všemi následujícími fázemi. Následující řádek tedy nejdříve vytvoří dva vzory a teprve potom se každý z nich pokusí expandovat na jména souborů.
> echo *.{txt,md}
a.txt b.txt c.txt poznamky.md
Naopak proměnná uvnitř závorek nefunguje tak, jak by se mohlo zdát: expanze proměnných probíhá až po expanzi závorek, takže rozsah se nestihne vytvořit.
> N=5
> echo {1..${N}}
{1..5}
Chceme-li rozsah s proměnnou mezí, musíme použít seq nebo céčkový for
(viz Cykly).
for (( i=1; i <= N; i++ ))
do
echo ${i}
done
Expanze vlnovky
Vlnovka na začátku slova se expanduje na cestu k domovskému adresáři.
> echo ~
/home/petr
> echo ~/skripty
/home/petr/skripty
Přesněji se ~ expanduje na hodnotu proměnné HOME. Napíšeme-li za vlnovku
jméno uživatele, expanduje se na jeho domovský adresář podle /etc/passwd.
> echo ~root
/root
> echo ~neexistujici
~neexistujici
Poslední řádek ukazuje obvyklé chování: pokud se expanze nepodaří, zůstane výraz nezměněn. Stejně se chová i expanze jmen souborů.
Dvě další formy odkazují na aktuální a předchozí pracovní adresář (proměnné
PWD a OLDPWD):
> cd /var/log
> cd /tmp
> echo ~+
/tmp
> echo ~-
/var/log
Kdy se vlnovka expanduje
Expanze vlnovky proběhne pouze na začátku slova. Uprostřed se neexpanduje.
> echo /home/~
/home/~
Výjimkou jsou přiřazení do proměnné: vlnovka se expanduje i po znaku =
a po každé dvojtečce. Právě díky tomu funguje obvyklé rozšíření PATH.
> PATH=~/bin:${PATH}
> echo ${PATH}
/home/petr/bin:/sbin:/bin:[...]
Protože je expanze vlnovky až druhá v pořadí a proběhne před expanzí proměnných, nepomůže nám vlnovka uvnitř hodnoty proměnné.
> DIR='~/skripty'
> echo ${DIR}
~/skripty
> ls ${DIR}
ls: cannot access '~/skripty': No such file or directory
Vlnovku je proto nutné expandovat při přiřazení, nikoliv při použití — tedy
DIR=~/skripty bez apostrofů, nebo rovnou DIR=${HOME}/skripty. Druhá varianta
je ve skriptech spolehlivější.
Uvozovky i apostrofy expanzi vlnovky potlačí:
> echo "~"
~
To je další důvod, proč ve skriptech dávat přednost ${HOME}: ten se expanduje
i v uvozovkách, a uvozovky kolem cest chceme mít z důvodů popsaných v kapitole
Dělení slov a IFS.
Expanze parametrů
Základní tvar expanze parametru známe z kapitoly Proměnné:
výraz ${var} se nahradí hodnotou proměnné var. Složené závorky ovšem
umožňují mnohem víc než jen vymezit jméno proměnné. Uvnitř nich lze zapsat
operátor, který s hodnotou před jejím dosazením něco udělá: doplní výchozí
hodnotu, ořízne část řetězce, nahradí podřetězec atd.
Všechny tyto operace provádí shell sám, bez spouštění dalších procesů. Jsou tedy
rychlejší než volání sed, cut nebo basename a ve skriptech, které
zpracovávají mnoho položek, je rozdíl znatelný.
Nejprve poznámka, kterou je dobré mít na paměti po celou podkapitolu: vzory
v expanzi parametrů jsou žolíky, ne regulární výrazy. Platí pro ně stejná
pravidla jako pro expanzi jmen souborů, tedy *, ?,
[…]. Výraz ${var#*.} tedy neznamená totéž co stejně vypadající regulární
výraz.
Výchozí hodnoty
Čtyři operátory řeší situaci, kdy proměnná nemusí být nastavena.
| výraz | význam |
|---|---|
${var:-slovo} | je-li var nastavená a neprázdná, její hodnota, jinak slovo |
${var:=slovo} | totéž, ale navíc přiřadí slovo do var |
${var:?zpráva} | hodnota var, jinak vypíše zpráva na stderr a ukončí skript |
${var:+slovo} | je-li var nastavená a neprázdná, slovo, jinak prázdný řetězec |
> unset JMENO
> echo "ahoj ${JMENO:-svete}"
ahoj svete
> echo ${JMENO}
> echo "ahoj ${JMENO:=svete}"
ahoj svete
> echo ${JMENO}
svete
Rozdíl mezi :- a := je právě v tom, že druhá varianta si výchozí hodnotu
zapamatuje. Typické použití ve skriptu je nastavení výchozích hodnot argumentů. Následující
dva úryvky dělají totéž.
# bez expanze parametru
if [ -z "${1}" ]
then
adresar="."
else
adresar="${1}"
fi
# s expanzi parametru
adresar="${1:-.}"
Operátor :? lze použít ke kontrole povinných parametrů. Skript skončí s nenulovou
návratovou hodnotou a vypíše jméno proměnné spolu se zadanou zprávou.
adresar="${1:?je nutne zadat adresar}"
> ./script.sh
./script.sh: line 1: 1: je nutne zadat adresar
Operátor :+ se hodí, chceme-li něco přidat pouze tehdy, když je proměnná
nastavena. Například sestavení volby pro jiný program:
# do VOLBY se dostane '--verbose' jen pokud je DEBUG nastavena
VOLBY="${DEBUG:+--verbose}"
grep ${VOLBY} 'vzor' soubor.txt
Vynecháme-li ve všech čtyřech operátorech dvojtečku (tj. ${var-slovo},
${var=slovo} atd.), rozlišuje shell pouze mezi nastavenou a nenastavenou
proměnnou; prázdná hodnota se pak považuje za nastavenou.
> PRAZDNA=""
> echo "[${PRAZDNA:-vychozi}]"
[vychozi]
> echo "[${PRAZDNA-vychozi}]"
[]
Délka a podřetězce
Výraz ${#var} je délka hodnoty proměnné ve znacích.
> FOO=ahoj
> echo ${#FOO}
4
Podřetězec získáme pomocí ${var:pozice} nebo ${var:pozice:delka}. Pozice se
počítá od 0.
> DATUM=20240913
> echo ${DATUM:0:4}
2024
> echo ${DATUM:4:2}
09
> echo ${DATUM:6}
13
Záporná pozice počítá od konce řetězce; před minus je nutné napsat mezeru nebo
pozici uzávorkovat, jinak by shell výraz zaměnil s operátorem :-.
> echo ${DATUM: -2}
13
Odstranění vzoru
Čtyři operátory odřezávají z hodnoty část, která se shoduje se vzorem. Vzor se
musí shodovat se začátkem (#) nebo koncem (%) hodnoty; jednoduchý symbol
znamená nejkratší shodu, zdvojený nejdelší.
| výraz | význam |
|---|---|
${var#vzor} | odstraní nejkratší shodu od začátku |
${var##vzor} | odstraní nejdelší shodu od začátku |
${var%vzor} | odstraní nejkratší shodu od konce |
${var%%vzor} | odstraní nejdelší shodu od konce |
Pomůcka k zapamatování: na české klávesnici i na většině jiných je # vlevo od
%, stejně jako začátek řetězce je vlevo od jeho konce.
Klasickým použitím je práce s cestami a příponami, kde nahradíme programy
basename a dirname.
> CESTA=/home/petr/dokumenty/text.tex
> echo ${CESTA##*/}
text.tex
> echo ${CESTA%/*}
/home/petr/dokumenty
> SOUBOR=${CESTA##*/}
> echo ${SOUBOR%.*}
text
> echo ${SOUBOR##*.}
tex
Rozdíl mezi krátkou a dlouhou variantou je vidět na jménech s více tečkami.
> FOO=archiv.tar.gz
> echo ${FOO%.*}
archiv.tar
> echo ${FOO%%.*}
archiv
> echo ${FOO#*.}
tar.gz
> echo ${FOO##*.}
gz
Skript, který ke všem souborům *.txt v adresáři vytvoří kopie s příponou
.bak, tak lze napsat bez volání dalších programů.
for f in *.txt
do
cp "${f}" "${f%.txt}.bak"
done
Nahrazení podřetězce
| výraz | význam |
|---|---|
${var/vzor/nahrada} | nahradí první výskyt |
${var//vzor/nahrada} | nahradí všechny výskyty |
${var/#vzor/nahrada} | nahradí, jen shoduje-li se vzor se začátkem |
${var/%vzor/nahrada} | nahradí, jen shoduje-li se vzor s koncem |
> CESTA=/usr/local/bin
> echo ${CESTA/\//:}
:usr/local/bin
> echo ${CESTA//\//:}
:usr:local:bin
> VETA="ahoj svete"
> echo ${VETA/svete/lidi}
ahoj lidi
Vynecháme-li náhradu, vzor se odstraní.
> echo ${VETA// /}
ahojsvete
Velikost písmen
> FOO=ahoj
> echo ${FOO^} # prvni znak velky
Ahoj
> echo ${FOO^^} # vse velke
AHOJ
> BAR=AHOJ
> echo ${BAR,,} # vse male
ahoj
Hodí se například při zpracování odpovědí uživatele, kde nechceme rozlišovat
ano a ANO:
read -p "Pokracovat [a/n]? " odpoved
case "${odpoved,,}" in
a | ano) echo "pokracujeme" ;;
*) echo "koncime" ;;
esac
Nepřímá expanze
Výraz ${!var} se expanduje na hodnotu proměnné, jejíž jméno je uloženo
v var.
> HODNOTA=42
> JMENO=HODNOTA
> echo ${JMENO}
HODNOTA
> echo ${!JMENO}
42
Toto je odpověď na problém zmíněný v úvodu kapitoly: expanze se neopakuje, chceme-li druhou úroveň, musíme si o ni říct.
Expanze a pole
Expanze parametrů má několik tvarů specifických pro pole.
| výraz | význam |
|---|---|
${pole[i]} | prvek na indexu i |
${pole[@]} | všechny prvky (v uvozovkách jako samostatná slova) |
${pole[*]} | všechny prvky jako jeden řetězec oddělený prvním znakem IFS |
${#pole[@]} | počet prvků |
${#pole[i]} | délka prvku na indexu i |
${!pole[@]} | seznam použitých indexů |
> pole=(prvni druhy treti)
> echo ${#pole[@]}
3
> echo ${!pole[@]}
0 1 2
> echo ${pole[@]: -1}
treti
Operátory pro odstranění vzoru a nahrazení se u ${pole[@]} aplikují na každý
prvek zvlášť, což se hodí pro hromadné úpravy.
> soubory=(a.txt b.txt c.txt)
> echo ${soubory[@]%.txt}
a b c
Shrnutí
Uvedené operátory pokrývají velkou část toho, k čemu se ve skriptech běžně
sahá po sed, cut, tr, basename a dirname. Kompletní seznam je
v dokumentaci
Bash Manuál
a v [practical].
Ještě jednou upozorníme na dvě věci. Za prvé, vzory jsou žolíky, ne regulární výrazy. Za druhé, celý výraz je stále expanzí parametru, takže se na jeho výsledek vztahuje dělení slov — i tady tedy platí, že expanzi obvykle píšeme do uvozovek.
Aritmetická expanze
Výraz
$(( term ))
se expanduje na výsledek vyhodnocení aritmetického výrazu term. Na rozdíl od
expr a od tvaru $[ … ] použitého v kapitole Aritmetika
jde o vestavěnou konstrukci, která nespouští žádný další proces. Tvar
$(( … )) je dnes doporučovaný; $[ … ] je zastaralý a v nových skriptech se
mu vyhýbáme,1 i když jej ve starším kódu potkáme. Uvnitř výrazu jsou povoleny mezery.
> echo $(( 2 + 3 ))
5
> echo $(( 10 / 3 ))
3
> echo $(( 10 % 3 ))
1
Proměnné uvnitř
Uvnitř $(( )) se na jména proměnných dívá shell jako na proměnné i bez $.
Obě následující varianty jsou tedy správné, druhá je čitelnější.
var1=10
var2=20
vysledek=$(( ${var1} * ${var2} ))
vysledek=$(( var1 * var2 ))
Prázdná nebo nenastavená proměnná se vyhodnotí jako 0. To je pohodlné, ale snadno to zamaskuje překlep ve jméně proměnné.
> echo $(( NEEXISTUJE + 5 ))
5
Operátory
K dispozici jsou operátory známé z jazyka C, včetně priority a závorek:
- aritmetické
+ - * / % **(mocnina) - porovnání
< <= > >= == != - logické
&& || ! - bitové
& | ^ ~ << >> - přiřazovací
= += -= *= /= %= - inkrement a dekrement
++ -- - ternární operátor
podmínka ? a : b
> echo $(( 2 ** 10 ))
1024
> echo $(( 1 << 8 ))
256
> x=5
> echo $(( x > 3 ? 100 : 200 ))
100
Porovnání vrací 1 pro pravdu a 0 pro nepravdu. Pozor, je to opačně než u návratových hodnot příkazů (viz Návratové hodnoty).
Aritmetika jako příkaz
Kromě expanze existuje i tvar (( term )), který je příkazem. Výraz se
vyhodnotí a příkaz skončí s návratovou hodnotou 0, je-li výsledek nenulový,
a s hodnotou 1, je-li výsledek nula. To je právě obrácení zmíněné výše, které
umožňuje psát podmínky přirozeně.
if (( pocet > 10 ))
then
echo "vice nez deset"
fi
# pocitadlo v cyklu
i=0
while (( i < 5 ))
do
echo ${i}
(( i++ ))
done
Ve srovnání s [ "${a}" -gt "${b}" ] je tento zápis čitelnější a nemusíme
řešit uvozovky, protože uvnitř (( )) neprobíhá dělení slov. Jde ovšem
o rozšíření Bashe, které v jiných shellech nemusí být k dispozici (viz
Větvení).
Konstrukce (( )) se objevuje také v hlavičce céčkového cyklu for, který jsme
viděli v kapitole Cykly:
for (( i=1; i <= 10; i++ ))
do
echo ${i}
done
Číselné soustavy
Číslo začínající nulou je považováno za osmičkové, číslo začínající 0x za
šestnáctkové. Obecně lze soustavu zapsat jako zaklad#cislo.
> echo $(( 010 ))
8
> echo $(( 0xff ))
255
> echo $(( 2#1010 ))
10
Osmičková interpretace je zdrojem chyb při práci s daty, která používají nuly zleva — třeba s měsíci nebo dny v datu.
> MESIC=09
> echo $(( MESIC + 1 ))
bash: 09: value too great for base (error token is "09")
Řešením je vynutit desítkovou soustavu zápisem 10#:
> echo $(( 10#${MESIC} + 1 ))
10
Aritmetika je celočíselná
Stejně jako u expr platí, že $(( )) počítá pouze s celými čísly a dělení
ořezává. Pro výpočty s desetinnými čísly je nutné použít bc, jak je popsáno
v kapitole Aritmetika.
-
$[ … ]je v dokumentaci Bashe uveden jako zastaralý a v budoucích verzích může zmizet. V ukázkách ve zbytku textu se ještě vyskytuje. ↩
Substituce příkazů a procesů
Substituce příkazů
Substituci příkazů jsme popsali v kapitole Podshell: výraz
$(command) se nahradí standardním výstupem příkazu command. Zde doplníme
několik podrobností, které z pohledu expanzí stojí za zmínku.
Koncové znaky nového řádku se odstraňují. Shell z výstupu odřízne všechny znaky nového řádku na konci, jinak by se s výsledkem obtížně pracovalo.
> echo "[$(echo ahoj)]"
[ahoj]
Vnitřní znaky nového řádku ovšem zůstávají, a bez uvozovek je pak rozdělí dělení slov.
> touch a.txt b.txt c.txt
> for f in $(ls); do echo "[${f}]"; done
[a.txt]
[b.txt]
[c.txt]
> for f in "$(ls)"; do echo "[${f}]"; done
[a.txt
b.txt
c.txt]
Substituce lze vnořovat. To je hlavní praktický důvod, proč dáváme přednost
$( ) před zpětnými apostrofy: u zpětných apostrofů bychom museli vnitřní
výskyty escapovat.
> echo $(basename $(dirname /a/b/c.txt))
b
Výsledek prochází dělením slov a expanzí jmen souborů. Platí tedy stejné varování jako u proměnných: obsahuje-li výstup mezery nebo žolíky, bez uvozovek se rozpadne nebo změní.
> touch "soubor s mezerami.txt"
> for f in $(ls); do echo "[${f}]"; done
[soubor]
[s]
[mezerami.txt]
Toto je zároveň důvod, proč se výstup ls nemá zpracovávat cyklem; místo toho
použijeme expanzi jmen souborů:
for f in *
do
echo "[${f}]"
done
Substituce procesů
Substituce procesů je rozšíření Bashe, které umožňuje předat výstup příkazu tam, kde jiný příkaz očekává jméno souboru.
<(command) # vystup command je dostupny jako soubor ke cteni
>(command) # zapis do "souboru" jde na vstup command
Shell příkaz spustí a výraz nahradí jménem souboru, obvykle cestou
k pojmenované rouře v /dev/fd.
> echo <(ls)
/dev/fd/63
Typickým použitím je porovnání výstupů dvou příkazů. Bez substituce procesů bychom museli vytvářet dočasné soubory.
> diff <(sort soubor1.txt) <(sort soubor2.txt)
> diff <(ls adresar1) <(ls adresar2)
Substituce procesů a cykly while read
V kapitole Vstup pomocí read jsou dvě
varianty čtení souboru po řádcích. Varianta s rourou má nepříjemnou vlastnost,
kterou teď můžeme vysvětlit: každá část roury běží v podshellu,
takže proměnné nastavené v těle cyklu po jeho skončení zaniknou.
# NEFUNGUJE: po cyklu je count opet 1
count=1
cat soubor.txt | while read line
do
count=$(( count + 1 ))
done
echo "precteno $(( count - 1 )) radku"
Řešením je přesměrování ze souboru, které jsme už použili, nebo — potřebujeme-li zpracovat výstup příkazu — právě substituce procesů:
count=1
while read line
do
count=$(( count + 1 ))
done < <(grep 'vzor' soubor.txt)
echo "precteno $(( count - 1 )) radku"
Mezera mezi < a <( je podstatná: první znak je přesměrování standardního
vstupu, <( … ) je substituce procesů. Bez mezery by shell četl výraz jako
přesměrování <<.
Dělení slov a IFS
Dělení slov je fáze, ve které shell rozdělí výsledky předchozích expanzí na jednotlivá slova, tedy na jednotlivé argumenty spouštěného příkazu. Je to zdaleka nejčastější zdroj nesrozumitelného chování skriptů, a proto jí věnujeme samostatnou podkapitolu.
Platí dvě zásadní pravidla:
- Dělení slov se aplikuje pouze na výsledky expanzí — na expanzi parametrů, aritmetickou expanzi a substituci příkazů. Text, který jsme ve skriptu napsali přímo, se nedělí (ten je na slova rozdělen dříve, při parsování řádku).
- Dělení slov se neprovádí uvnitř uvozovek ani apostrofů.
Proměnná IFS
Které znaky jsou oddělovači, určuje proměnná IFS (internal field separator).
Výchozí hodnotou je mezera, tabulátor a znak nového řádku.
Chování se liší podle toho, jestli je oddělovačem bílý znak, nebo ne:
- bílé znaky (mezera, tabulátor, nový řádek) v
IFS: posloupnost několika oddělovačů za sebou se chová jako jeden a oddělovače na začátku a na konci se ignorují; - ostatní znaky: každý výskyt odděluje, takže dva za sebou vytvoří prázdné slovo.
Rozdíl je vidět na následujícím příkladu.
> FOO="a b"
> printf '[%s]\n' ${FOO}
[a]
[b]
> IFS=:
> BAR="a::b"
> printf '[%s]\n' ${BAR}
[a]
[]
[b]
Právě proto se PATH prochází dvojtečkou jako oddělovačem korektně i tehdy,
když některá položka chybí.
Nastavíme-li IFS na prázdný řetězec, dělení slov se neprovede vůbec
a z expanze vznikne jediné slovo.
> IFS=
> FOO="a b c"
> printf '[%s]\n' ${FOO}
[a b c]
Znaková expanze
Do IFS často potřebujeme dostat znak, který se špatně zapisuje, typicky nový
řádek nebo tabulátor. Slouží k tomu zápis $'…' (tzv. ANSI-C quoting):
IFS=$'\n' # deleni pouze na radky
IFS=$'\t' # deleni pouze tabulatorem
IFS=$' \t\n' # vychozi hodnota
Uvnitř $'…' se interpretují sekvence známé z jazyka C: \n, \t, \\, \',
\xHH a další. Podrobněji viz Uvozování.
Měníme-li IFS, je slušné si původní hodnotu uložit a po skončení práce ji
obnovit, jak jsme to dělali v kapitole Cykly.
Alternativou je nastavit IFS jen pro jeden příkaz zápisem IFS=: prikaz,
který proměnnou změní pouze v prostředí tohoto příkazu.
> IFS=: read -r a b c <<< "prvni:druhy:treti"
> echo ${b}
druhy
Proč uvozovky kolem expanzí
Nyní můžeme přesně vysvětlit doporučení, které v textu opakovaně zaznívá. Uvažujme soubor, jehož jméno obsahuje mezeru.
> SOUBOR="muj soubor.txt"
> ls ${SOUBOR}
ls: cannot access 'muj': No such file or directory
ls: cannot access 'soubor.txt': No such file or directory
> ls "${SOUBOR}"
muj soubor.txt
Bez uvozovek se hodnota po expanzi rozdělila na dvě slova, a ls tak dostal dva
argumenty místo jednoho. S uvozovkami se dělení slov neprovede.
Druhý, méně nápadný případ nastane, když je proměnná prázdná. Bez uvozovek zmizí argument úplně; s uvozovkami zůstane prázdný argument.
> PRAZDNA=""
> printf '[%s]\n' ${PRAZDNA}
> printf '[%s]\n' "${PRAZDNA}"
[]
To je důvod, proč se v kapitole Větvení píše
[ -n "${x}" ] a ne [ -n ${x} ]: v druhém případě by test dostal jediný
argument -n, což vyhodnotí jako test neprázdnosti řetězce -n, tedy vždy
pravdivě.
$* a $@
Rozdíl mezi $* a $@, popsaný v kapitole Vstupní
argumenty, je nyní jen důsledkem
pravidel výše. Předpokládejme skript spuštěný jako
./script.sh "prvni argument" druhy.
| zápis | výsledek |
|---|---|
$* | jedno slovo, rozdělené dělením slov → prvni, argument, druhy |
$@ | jednotlivé argumenty, ale rozdělené dělením slov → prvni, argument, druhy |
"$*" | jediné slovo prvni argument druhy (spojené prvním znakem IFS) |
"$@" | tři slova: prvni argument, druhy — beze změny |
echo "--- \$* bez uvozovek"
for p in $*
do
echo "[${p}]"
done
echo "--- \"\$@\" v uvozovkach"
for p in "$@"
do
echo "[${p}]"
done
> ./script.sh "prvni argument" druhy
--- $* bez uvozovek
[prvni]
[argument]
[druhy]
--- "$@" v uvozovkach
[prvni argument]
[druhy]
Prakticky to znamená: argumenty předáváme dál vždy jako "$@". Totéž platí
pro pole, kde "${pole[@]}" zachová prvky tak, jak byly uloženy (viz
Pole).
Kdy uvozovky vynechat
Uvozovky vynecháváme jen tehdy, když dělení slov opravdu chceme — typicky když je v proměnné seznam položek, který má vzniknout více argumentů:
VOLBY="-l -a"
ls ${VOLBY} # zamerne bez uvozovek: dve volby
I tady je ovšem čistším řešením pole:
VOLBY=(-l -a)
ls "${VOLBY[@]}"
Expanze jmen souborů
Poslední expanzí před odstraněním uvozovek je expanze jmen souborů (anglicky
pathname expansion nebo file globbing). Shell v každém slově hledá žolíky
*, ? a [, a pokud je najde, nahradí slovo abecedně setříděným seznamem
jmen souborů, které vzoru odpovídají.
| žolík | expanze |
|---|---|
* | 0 nebo více libovolných znaků |
? | jeden libovolný znak |
[chars] | jeden ze znaků v řetezci chars |
[c1-c2] | jeden ze znaků v rozsahu c1-c2, např. [0-9], [a-z] |
[!chars] | jakýkoliv znak, který není v chars |
Rozsahy a výčty lze kombinovat, takže [a-zA-Z0-9_] je jeden alfanumerický znak
nebo podtržítko.
> ls
a.txt b.txt foto1.jpg foto2.jpg poznamky.md
> echo *.txt
a.txt b.txt
> echo foto?.jpg
foto1.jpg foto2.jpg
> echo [ab].txt
a.txt b.txt
> echo [!ab]*
foto1.jpg foto2.jpg poznamky.md
Zápis {word1,word2} uváděný v některých přehledech žolíků sem nepatří — je to
expanze závorek, která probíhá v jiné fázi a nekontroluje
existenci souborů.
Žolíky nejsou regulární výrazy
Symboly vypadají podobně jako v regulárních výrazech, ale znamenají něco jiného.
Ve vzoru shellu znamená * „libovolné znaky“, kdežto v regulárním výrazu
znamená „opakování předchozího prvku“. Vzor *.txt má tedy jako regulární výraz
zapsaný pro grep tvar .*\.txt.
Tento rozdíl je pravidelným zdrojem chyb, protože v jednom skriptu se obojí
běžně vyskytuje vedle sebe: grep a sed pracují s regulárními výrazy, case,
expanze parametrů a expanze jmen souborů se žolíky.
Tečka na začátku a lomítko
Žolíky mají dvě výjimky.
*ani?neodpovídají tečce na začátku jména souboru, takže skryté soubory se implicitně nezpracují;- žádný žolík neodpovídá lomítku, takže
*nikdy nepřekročí hranici adresáře.
> ls -a
. .. .skryty a.txt
> echo *
a.txt
> echo .*
. .. .skryty
Zápis .* je tedy nebezpečný, protože zahrnuje . a ... Chceme-li skryté
soubory bez těchto dvou položek, použijeme .[!.]* nebo přepínač dotglob
zmíněný níže.
Když se vzor neshoduje s ničím
Ve výchozím nastavení shell vzor, ke kterému neexistuje žádný soubor, nechá beze změny. To je častý zdroj chyb ve skriptech.
> cd /tmp/prazdny
> echo *.txt
*.txt
> for f in *.txt; do echo "zpracovavam ${f}"; done
zpracovavam *.txt
Cyklus tedy proběhl jednou s neexistujícím „souborem“ jménem *.txt. Obranou je
buď test existence uvnitř cyklu,
for f in *.txt
do
[ -e "${f}" ] || continue
echo "zpracovavam ${f}"
done
nebo změna chování shellu pomocí shopt:
shopt -s nullglob # neshodujici se vzor zmizi (cyklus neprobehne ani jednou)
shopt -s failglob # neshodujici se vzor je chyba
Další volby shopt
Příkaz shopt zapíná a vypíná vlastnosti shellu podobně jako set (viz
podkapitola set); volby týkající se expanze jmen souborů
jsou tyto.
| volba | význam |
|---|---|
nullglob | neshodující se vzor se odstraní |
failglob | neshodující se vzor způsobí chybu |
dotglob | * zahrne i soubory začínající tečkou |
nocaseglob | ignoruje velikost písmen |
globstar | zpřístupní vzor ** |
extglob | zapne rozšířené vzory (viz níže) |
> shopt -s nocaseglob
> echo *.JPG
foto1.jpg foto2.jpg
> shopt -u nocaseglob
Volba globstar zpřístupní vzor **, který na rozdíl od * prochází i do
podadresářů:
> shopt -s globstar
> echo **/*.txt
a.txt doc/b.txt doc/hlubsi/c.txt
V jednodušších případech mi lze použít jako pohodlnou náhradu za find.
Znakové třídy POSIX
Uvnitř hranatých závorek lze místo výčtu použít pojmenované třídy znaků. Ty jsou čitelnější a fungují správně i pro jiné jazyky než angličtinu.
| třída | význam |
|---|---|
[[:alpha:]] | písmeno |
[[:digit:]] | číslice |
[[:alnum:]] | písmeno nebo číslice |
[[:space:]] | bílý znak |
[[:upper:]], [[:lower:]] | velké, malé písmeno |
[[:punct:]] | interpunkce |
> echo foo[[:digit:]][[:digit:]][[:digit:]][[:digit:]].txt
foo0001.txt foo0002.txt [...]
Všimněme si dvojích hranatých závorek: vnější patří žolíku, vnitřní jsou částí zápisu třídy.
Rozšířené vzory
Po shopt -s extglob jsou k dispozici konstrukce, které se blíží možnostem
regulárních výrazů. seznam je jeden vzor nebo více vzorů oddělených |.
| vzor | význam |
|---|---|
?(seznam) | nula nebo jeden výskyt |
*(seznam) | nula nebo více výskytů |
+(seznam) | jeden nebo více výskytů |
@(seznam) | právě jeden ze vzorů |
!(seznam) | cokoliv, co neodpovídá žádnému ze vzorů |
> shopt -s extglob
> echo @(a|b).txt
a.txt b.txt
> echo !(*.txt) # vse krome textovych souboru
foto1.jpg foto2.jpg poznamky.md
Tytéž vzory lze použít i v konstrukci case a v expanzi
parametrů.
Potlačení expanze
Expanzi potlačíme uvozovkami, apostrofy nebo zpětným lomítkem. To je nutné například tehdy, když chceme žolík předat jinému programu, aby jej zpracoval sám.
> find . -name "*.txt"
Bez uvozovek by shell vzor *.txt expandoval ještě před spuštěním find, a ten
by dostal jména souborů z aktuálního adresáře, nikoliv vzor. Ve chvíli, kdy
takový příkaz funguje v jednom adresáři a v jiném ne, je příčinou obvykle právě
tohle.
Uvozování
Uvozování lze shellu říct, že určitou část řádku má brát doslova a neprovádět na ní některé expanze. Po provedení všech expanzí shell uvozovací znaky odstraní — to je poslední fáze, odstranění uvozovek. Odstraní se pouze ty uvozovky, které jsme napsali ve skriptu, nikoliv ty, které vznikly expanzí.
Uvozovacích prostředků je několik a liší se tím, co potlačí.
Zpětné lomítko
Zpětné lomítko potlačí zvláštní význam následujícího znaku.
> echo Cena je 100 $
Cena je 100 $
> echo Cena je 100 \$\{X\}
Cena je 100 ${X}
> touch muj\ soubor.txt
Zpětné lomítko na konci řádku ruší znak nového řádku, což umožňuje rozdělit dlouhý příkaz na více řádků.
grep -r 'vzor' \
--include='*.txt' \
/var/log
Apostrofy
Apostrofy potlačí všechny expanze i zvláštní význam všech znaků. Uvnitř apostrofů nelze zapsat apostrof — ani pomocí zpětného lomítka.
> echo '${HOME} a $(date) a *.txt'
${HOME} a $(date) a *.txt
Potřebujeme-li v takovém řetězci apostrof, musíme uvozování přerušit:
> echo 'nelze to'\''udelat jinak'
nelze to'udelat jinak
Uvozovky
Uvozovky potlačí expanzi závorek, vlnovky, dělení slov a expanzi jmen souborů, ale ponechají expanzi parametrů, aritmetickou expanzi a substituci příkazů. Právě proto je to nejčastěji používaná forma.
> echo "domov je ${HOME}, dnes je $(date +%A), soubory: *.txt"
domov je /home/petr, dnes je Wednesday, soubory: *.txt
Uvnitř uvozovek si zvláštní význam zachovávají znaky $, ` a \; ostatní
znaky, včetně * a mezery, jsou brány doslova. Zpětné lomítko uvnitř uvozovek
funguje pouze před těmito znaky a před uvozovkou:
> echo "cena je \${X}"
cena je ${X}
> echo "zpetne lomitko: \d"
zpetne lomitko: \d
Zápis $'…'
Zápis $'…' se chová jako apostrofy, ale navíc interpretuje sekvence se zpětným
lomítkem podobně jako v jazyce C.
| sekvence | znak |
|---|---|
\n | nový řádek |
\t | tabulátor |
\r | návrat vozíku |
\\ | zpětné lomítko |
\' | apostrof |
\nnn | znak s osmičkovým kódem nnn |
\xHH | znak se šestnáctkovým kódem HH |
\uHHHH | znak Unicode |
> echo $'prvni\ndruhy'
prvni
druhy
> IFS=$'\n'
Tuto konstrukci jsme použili v kapitole Cykly při
nastavování IFS; jde tedy o běžný zápis, nikoliv o kuriozitu. Expanze
proměnných uvnitř $'…' neprobíhá.
Shrnutí
| co potlačíme | \znak | '…' | "…" | $'…' |
|---|---|---|---|---|
| expanze závorek | ano | ano | ano | ano |
| expanze vlnovky | ano | ano | ano | ano |
| expanze parametrů | ano | ano | ne | ano |
| aritmetická expanze | ano | ano | ne | ano |
| substituce příkazů | ano | ano | ne | ano |
| dělení slov | ano | ano | ano | ano |
| expanze jmen souborů | ano | ano | ano | ano |
interpretace \n, \t | — | ne | ne | ano |
Praktické pravidlo pro psaní skriptů: expanze proměnných a příkazů píšeme v uvozovkách, pokud výslovně nechceme dělení slov nebo expanzi jmen souborů. Řetězce, ve kterých nemá nic expandovat, píšeme do apostrofů.
# spravne
cp "${zdroj}" "${cil}"
grep -r 'chybova hlaska' "${adresar}"
# spatne: rozpadne se na jmenech s mezerami
cp ${zdroj} ${cil}
Ladění a typické chyby
Většinu chyb spojených s expanzemi lze odhalit tím, že se podíváme, co shell po expanzi doopravdy spouští.
Jak se podívat na výsledek expanze
set -x vypisuje na stderr každý příkaz po expanzi, uvozený promptem
PS4 (výchozí hodnota je +). Podrobněji viz podkapitola
set.
> set -x
> FOO="muj soubor.txt"
+ FOO='muj soubor.txt'
> ls ${FOO}
+ ls muj soubor.txt
ls: cannot access 'muj': No such file or directory
> ls "${FOO}"
+ ls 'muj soubor.txt'
Ve výpisu je vidět přesně to podstatné: v prvním případě dostal ls dva
argumenty, ve druhém jeden. Uvozovky ve výpisu set -x nejsou naše původní
uvozovky, ale shellem doplněné vyznačení hranic argumentů.
printf '%s\n' vypíše každý argument na vlastní řádek, takže je okamžitě
vidět, na kolik slov se expanze rozpadla. Je to spolehlivější než echo, které
argumenty spojí mezerami.
> FOO="a b"
> printf '[%s]\n' ${FOO}
[a]
[b]
> printf '[%s]\n' "${FOO}"
[a b]
declare -p vypíše proměnnou i s uvozovkami a u polí i s indexy, takže je
poznat, co v ní opravdu je.
> pole=("prvni prvek" druhy)
> declare -p pole
declare -a pole=([0]="prvni prvek" [1]="druhy")
Pro vzory je užitečné vyzkoušet si expanzi napřed pomocí echo, než ji použijeme
v destruktivním příkazu:
> echo rm -f *.tmp
rm -f a.tmp b.tmp
Typické chyby
-
Neuvozená proměnná se jménem obsahujícím mezeru.* Nejčastější chyba vůbec.
cp ${zdroj} ${cil} # spatne cp "${zdroj}" "${cil}" # spravne -
Neuvozená proměnná obsahující žolík. Hodnota se po expanzi promění na seznam souborů, které s ní nemají nic společného.
> VZOR='*' > echo ${VZOR} a.txt b.txt foto1.jpg > echo "${VZOR}" * -
Prázdná proměnná zmizí jako argument. Test pak dostane jiný počet argumentů, než čekáme.
if [ -n ${x} ] # spatne: pri prazdnem x je to test [ -n ], vzdy pravdivy if [ -n "${x}" ] # spravne -
rm -rf ${adresar}/*s prázdnou proměnnou. Pokud jeadresarprázdná nebo nenastavená, expanduje se výraz na/*se známým následkem. Obranou je test na začátku skriptu, nebo${adresar:?}, které skript při nenastavené proměnné ukončí (viz Expanze parametrů).rm -rf "${adresar:?adresar neni nastaven}"/*Užitečná je také volba
set -u, po které je použití nenastavené proměnné chybou místo tichého dosazení prázdného řetězce. -
Cyklus přes vzor, který se s ničím neshoduje. Popsáno v kapitole Expanze jmen souborů; řešením je
shopt -s nullglobnebo test existence v těle cyklu. -
Cyklus přes
$(ls). Rozpadne se na jménech s mezerami a expanduje žolíky ve jménech. Místo toho se cyklí přímo přes vzor:for f in $(ls) # spatne for f in * # spravne -
readbez-r. Bez tohoto přepínačereadinterpretuje zpětná lomítka na vstupu jako escapování, takže se ztratí. Ve skriptech proto čteme řádky pomocíwhile IFS= read -r line do echo "${line}" done < soubor.txtPrázdné
IFS=navíc zabrání oříznutí mezer na začátku a na konci řádku. -
Nesprávné pořadí expanzí. Typicky pokus o rozsah s proměnnou mezí (
{1..${N}}) nebo vlnovku v hodnotě proměnné. Obojí je popsáno u příslušných expanzí; společná příčina je, že daná fáze proběhla dřív, než vznikla hodnota, kterou by potřebovala.
Kontrola skriptů
Většinu uvedených chyb umí najít statický analyzátor
ShellCheck, který je v distribucích běžně
k dispozici jako balík shellcheck. Jeho používání vřele doporučuji.
> shellcheck script.sh
In script.sh line 3:
cp ${zdroj} ${cil}
^-------^ SC2086: Double quote to prevent globbing and word splitting.
Přesměrování a roura
Běžící proces má k dispozici standardní proudy: standardní vstup (stdin),
standardní výstup (stdout) a standardní chybový výstup (stderr). Ze
standardního vstupu může číst, do zbylých dvou zapisovat. Proces neví, kam jsou
proudy napojeny, to je zařízeno vně procesu.
Ve výchozím nastavení jsou všechny tři proudy připojeny ke konzoli. Pokud spustíme program, který čte ze standardního vstupu, musíme něco zadat z klávesnice, výstup (i chybový) programu se tiskne do konzole.
Všechny tři proudy můžeme přesměrovat, a to buď do/ze souboru a nebo do/z jiného procesu. Výstup jednoho procesu tak může stát vstupem druhého procesu.
# stdout je presmerovan do souboru file, pokud existuje, je prepsan
command > file
# stdout je presmerovan do souboru file, pokud existuje, je stdout pripojen na konec
command >> file
# stdin je presmerovan ze souboru file
command < file
# stdin je cten z konzole, ale konci po end
command << end
# presmerovani stdout, stderr do souboru file (prepsani, pripojeni)
command &> file
command &>> file
# presmerovani stderr do souboru file (prepsani, pripojeni)
command 2> file
command 2>> file
# presmerovani stdout command1 na stdin command2
command1 | command2
# presmerovani stdout a stderr do roury
command1 |& command2
Ve skriptech se přesměrování stdin pomocí << chová jinak než v konzole.
Vstup se nečte z klávesnice, ale z řádků samotného skriptu, které se vyskytují
za přesměrováním. Tedy například skript
cat << EOF
prvni radek
druhy radek
treti radek
EOF
vytiskne po spuštění
prvni radek
druhy radek
treti radek
Aritmetika
Celočíselnou aritmetiku lze provádět pomocí příkazu expr, pro který má bash
alternativní syntax.
$[term]
Výraz je expandován vyhodnocením term pomocí příkazu expr, přičemž uvnitř
term platí obvyklá pravidla pro expanzi, takže lze používat proměnné.
var1=10
var2=20
var3=30
var4=$[${var1} * (${var3} - ${var2})]
var5=$[${var3} / ${var2}]
echo "hodnota var4 je ${var4}"
echo "hodnota var5 je ${var5}"
po spuštění skript vypíše
hodnota var4 je 100
hodnota var5 je 1
Na hodnotě var5 vidíme, že aritmetika je skutečně celočíselná.
Pro výpočty s desetinnými čísly musíme použít program bc. Tato utilita umí ve
skutečnosti zpracovat i jednoduché programy s proměnnými, cykly apod. Pro naše
účely postačí pouze základní použití.
Typicky se pro výpočet používá podshell a výsledek se uloží do proměnné. Vzor takového použití je
var=$(echo "scale=x; term" | bc)
kde za x musíme dosadit přesnost, tj. na kolik desetinných míst chceme, aby
bc počítalo. V podshellu tedy pomocí roury pošleme bc na vstup výraz, jehož
výsledek chceme spočítat, bc tento výsledek tiskne na standardní výstup,
a tento se tak dostane do proměnné var. Výhodou je, že ve výrazu "term"
dochází k expanzi proměnných.
var2=20
var3=30
var6=$(echo "scale=4; ${var3} / ${var2}" | bc)
echo "vysledek je ${var6}"
po spuštění skript vypíše
vysledek je 1.5000
Pro delší výrazy je lepší využít chování přesměrování << ve skriptu, například
var1=12
var2=8
var3=30
var4=$(bc << EOF
scale = 4
a = ${var1} + ${var2}
${var3} / a
EOF
)
echo "vysledek je ${var4}"
Po spuštění skript vytiskne
vysledek je 1.5000
Proměnná a ve skriptu je proměnná utility bc. Je vidět, že díky expanzi
shellových proměnných můžeme volně použít i proměnné shellu, po expanzi dostane
bc na vstup
scale = 4
a = 12 + 8
30 / a
Bash má pro aritmetiku ještě zápis $(( )), který je dnes doporučovaný
a proti $[ ] i expr nabízí víc operátorů; věnuje se mu podkapitola
Aritmetická expanze. Další alternativou je interní
příkaz let.1
-
Dokumentace:
help let. ↩
Řízení běhu
Kapitola popisuje návratové hodnoty, na kterých je řízení běhu v shellu postaveno, a konstrukce pro větvení a cykly.
Návratové hodnoty programů a příkazů
Příkazy a programy po svém skončení vrací shellu návratovou hodnotu (anglicky
exit code). Pokud program proběhl v pořádku, je návratová hodnota 0, jinak je
nenulová (pro určité typy chyb jsou ustálené návratové hodnoty). Návratová
hodnota naposled provedeného příkazu je uložena v proměnné ?.
> echo 10
10
> echo ${?}
0
> less aiodfjaoidfja.txt
aiodfjaoidfja.txt: No such file or directory
> echo ${?}
1
Jak vracet návratovou hodnotu ze skriptu se dozvíme v kapitole Interakce s okolím.
Větvení
Bash podporuje následující formy příkazu if.
# bez else
if tst
then
commands-true
fi
# s else vetvi
if tst
then
commands-true
else
commands-false
fi
Pokud je na místě tst proměnná, je podmínka pravdivá, pokud je proměnná
neprázdná. Je-li tst příkaz, je spuštěn. Pokud je jeho návratová hodnota 0,
provede se blok commands-true. V opačném případě se ve verzi s else větví
provede commands-false. Existuje syntax i pro žebříkovou verzi konstrukce,
kde je else if nahrazeno elif
# zebrik
if tst
then
commands
elif tst
then
commands
else
commands
fi
Příkladem utility, kterou lze použít na místě tst, je grep. Pokud totiž
nalezne aspoň jednu shodu, její návratová hodnota je 0, jinak je nenulová.
Následující skript otestuje, jestli je v systému uživatel s vybraným jménem.
ouruser='franta'
if grep -q "^${ouruser}:" /etc/passwd
then
echo "uzivatel ${ouruser} existuje"
else
echo "uzivatel ${ouruser} neexistuje"
fi
Pro ověřování obecnějších podmínek lze použít utilitu test, která se používá
ve formě1
[ expression ]
Za otevírací závorkou a před uzavírací závorkou musí být mezery. Pokud test
vyhodnotí expression jako pravdivé, skončí s návratovou hodnotou 0, jinak
s hodnotou nenulovou. Na místo expression lze napsat celou řadu podmínek,
užitečná je tady manuálová stránka programu test. Existují podmínky pro celá
čísla, řetězce, testy existence, práv a jiných vlastností souborů. Ve výrazu
tst v konstrukci if lze použít logických operátorů pro spojky a (&&)
a nebo (||). V následujícím kódu jsou příklady použití několika testů.
#
# numericke porovnavani
#
# tady si je pouzito && k vytvoreni konjunkce
# n1 -ge n2 je test, jestli n1 >= n2
if [ ${a} -ge ${b} ] && [ ${a} -ge ${c} ]
then
echo "nejvetsi je a s hodnotou ${a}"
elif [ ${b} -ge ${c} ]
then
echo "nejvetsi je b s hodnotou ${b}"
else
echo "nejvetsi je c s hodnotou ${c}"
fi
#
# prace s retezci
#
x=cecil
y=josef
z=""
# proc nize expanzi promenne davame do ""?
# aby test chapal expanzi promenne jako retezec,
# ma nevhodny tvar, napriklad obsahuje vice slov, atd.
# alternativou je pouzit modernejsi [[ -n ${x} ]]
# (to potom ovsem neni program test, je to bash builtin)
# delky retezcu
if [ -n "${x}" ]
then
echo "x je neprazdny"
fi
if [ -z "${z}" ]
then
echo "z je prazdny"
fi
# rovnost
if [ "${x}" != "${y}" ]
then
echo "x a y jsou ruzne"
fi
if [ "${x}" = "${y}" ]
then
echo "x a y jsou stejne"
fi
# lexikograficke porovnani
# operator porovnani < je nutne backquotovat, jinak je bran jako presmerovani
if [ "${x}" \< "${y}" ]
then
echo "x je mensi nez y"
fi
#
# prace se soubory
#
# vypise obsah adresare, pokud existuje
# dalsi testy existence a vlastnosti souboru a adresaru jsou analogicke
addr=logs
if [ -d "${addr}" ]
then
ls ${addr}
fi
# porovnani stari souboru
if [ "${file1}" -ot "${file2}" ]
then
echo "soubor ${file1} je starsi nez ${file2}"
fi
Bash obsahuje následující rozšíření, která nemusí být podporována v jiných shellech.
-
dvojí obyčejné závorky na místě
tst(tj. výraz(( expression ))): lze použít některé aritmetické, bitové a logické operátory, kterétestneumí. -
dvojí hranaté závorky na místě
tst(tj. výraz[[ expression ]]): pro řetězcové porovnávání, obsahuje to cotest, navíc lze kontrolovat shodu s regulárním výrazem, řeší problémy s expanzí a mezerami při porovnávání řetězců a cest.
Analogií konstrukce switch z jazyka C je v Bashi case. Umožňuje porovnat
hodnotu s jinými hodnotami (jako řetězce) a v případě shody provést blok
příkazů. Syntax příkazu je následující.
case val in # val je hodnota, kterou porovnavame
pattern1) # hodnoty, se kterymi porovnavame jsou ukonceny )
cmd1 # nasleduje blok prikazu
[...]
cmdk ;; # ktery je ukoncen dvema stredniky
pattern2 | pattern3) # lze uvest vice hodnot oddelenych |, staci shoda s jednou
cmds ;;
*) # * se shoduje se vsim, je to analogie default z C
cmds ;;
esac
Je důležité si uvědomit, že na val i pattern je aplikována expanze.
Konstrukce se používá na místech, kde bychom jinak použili dlouhou if konstrukci
s mnoha elif větvemi, jak v následujícím kódu.
month=June
case ${month} in
January | March | May | July | August | October | December)
echo "${month} has 31 days" ;;
February)
echo "${month} has 28 or 29 days" ;;
April | June | September | November)
echo "${month} has 30 days" ;;
*)
echo "${month} is not a valid month"
esac
Typické použití ve skriptech je při zpracování argumentů, jak uvidíme v kapitole Vstupní argumenty.
-
Existuje program se jménem
[, který se chová jako test. ↩
Cykly
for var in list
do
commands
done
V každé iteraci je do proměnné var přiřazen jeden prvek ze seznamu list
a poté je provedeno tělo commands. List je seznam oddělený mezerami. Ten
můžeme vytvořit přímo, nebo jej získáme jinak, například pomocí expanze jmen.
# list sestaveny primo
for v in monday tuesday wednesday
do
echo ${v}
done
# list ziskany expanzi
# pro kazdou (viditelnou) polozku v adresari addr zavolame utilitku file
addr=logs
for f in ${addr}/*
do
file ${f}
done
Při tvorbě seznamu a jeho zpracování je nutné být opatrný, pokud některé položky
obsahují mezery. Takové položky lze v ručně vytvořeném seznamu vyznačit pomocí
uvozovek nebo apostrofů. Pokud je seznam vytvořen strojově, tuto možnost nemáme.
Dělení řetězce na seznam položek se řídí obsahem proměnné IFS (podrobně viz
Dělení slov a IFS). Její hodnotou
je řetězec znaků, které chápeme jako oddělovače položek. Výchozí oddělovače jsou
mezera, nový řádek a tabulátor. V následujícím příkladu změníme tuto proměnnou
tak, abychom mohli přistoupit k jednotlivým adresářům v proměnné PATH.
IFS=:
for dir in ${PATH}
do
echo -n ${dir}
echo ": $(ls ${dir} | wc -l) polozek"
done
Při nastavování IFS můžeme narazit na problém, jak tam přidat některý
z výchozích znaků. K tomu lze použít znakovou expanzi shellu
$'znak'
Výraz $'\n' tak například expanduje na znak nového řádku; další sekvence jsou
v podkapitole Uvozování.
Seznam položek lze produkovat i pomocí příkazu. Na místě list v cyklu můžeme
dát spuštění nějakého programu v podshellu a v cyklu zpracovat jeho standardní
výstup. V následujícím příkladu zpracujeme obsah souboru /etc/passwd.
# zapamatujeme si puvodni IFS
OLD_IFS=${IFS}
# nova hodnota IFS, soubor prochazime po radcich
IFS=$'\n'
for entry in $(cat /etc/passwd)
do
# tady nastavime IFS nastavime na dvojtecku
# nemusime mit strach, ze pokazime IFS pro vnejsi cyklus
# tam se pamatuje puvodni nastaveni na novy radek
# tj. vyznam ma pouze hodnota IFS na zacatku cyklu
IFS=:
for value in ${entry}
do
echo " ${value}"
done
done
# tady IFS obsahuje dvojtecku, obnovime puvodni hodnoty
IFS=${OLD_IFS}
Další typ cyklu připomíná jazyk C, nebudeme jej tedy blíže komentovat, čtenáři jistě stačí následující příklad
for (( i=1; i <= 10; i++ ))
do
echo ${i}
done
Cyklus while funguje také podobně jako v jazyku C. Dokud platí podmínka,
opakuje se tělo cyklu. Pro podmínky platí podobné zásady jako pro podmínky
konstrukce if. U cyklu navíc můžeme místo podmínky napsat posloupnost řádků,
z nichž pouze poslední je interpretován jako podmínka. Zbylé řádky se spustí
vždy na začátku iterace, před vyhodnocením podmínky.
# cisla 10...0 se vypisi pred podminkou a uvnitr cyklu,
# cislo -1 se vypise jenom pred podminkou
var=10
while echo "hodnota pred podminkou: ${var}"
[ ${var} -ge 0 ]
do
echo "hodnota uvnitr cyklu: ${var}"
var=$[${var} - 1]
done
Konstrukce until je analogická while s tím, že cyklus končí (nikoliv
pokračuje) v momentě, kdy je podmínka pravdivá.
Příkazem break lze okamžitě ukončit vykonávání cyklu, v jehož těle se příkaz
nachází. Příkazu break lze volitelně předat numerický argument (přirozené
číslo), a ten potom přeruší některý z vnějších cyklů v kódu obsahujícím vnořené
cykly. Argument 1 označuje aktuální cyklus, argument 2 cyklus obsahující aktuální
cyklus atd.
for v1 in prvni druha treti
do
for v2 in 10 20 30 40 50
do
for v3 in x y z
do
break # prerusi cyklus s promennou v3
break 1 # prerusi cyklus s promennou v3
break 2 # prerusi cyklus s promennou v2
break 3 # prerusi cyklus s promennou v1
done
done
done
Příkazem continue lze přerušit vykonávání aktuální iterace cyklu, jako bychom
ve skriptu skočili na konec těla cyklu. Podobně jako break lze continue
předat numerický argument, určující kterého cyklu se příkaz týká.
Standardní výstup cyklu (tedy vše, co se během cyklu vytiskne) lze přesměrovat
do souboru nebo rourou. Přesměrování se píše za klíčové slovo done na konci
těla cyklu.
# vypise adresare setrizene podle poctu polozek
IFS=:
for dir in ${PATH}
do
echo -n ${dir}
echo " $(ls ${dir} | wc -l)"
done | sort -r -k 2 # tady presmerujeme rourou do prikazu sort
Interakce s okolím
Skript komunikuje s okolím návratovou hodnotou, vstupními argumenty, standardními proudy a signály. Tato kapitola popisuje všechny tyto mechanismy.
Návratová hodnota
Pokud skript neukončíme předčasně, je jeho návratovou hodnotou návratová hodnota posledního provedeného příkazu. Předčasně můžeme skript ukončit pomocí příkazu
exit value
Za value dosadíme číslo v rozsahu 0 až 255 (0 označuje úspěšné ukončení,
ostatní chybu). Pokud je value větší než 255, vrací se zbytek po dělení
value číslem 256.
Vstupní argumenty
Při spouštění skriptu mu můžeme předat parametry a volby (dohromady je označujeme argumenty) stejně jako libovolnému jinému příkazu.
> ./script.sh parameter1 parametr2 [...]
Uvnitř skriptu k nim můžeme přistupovat. K jednotlivým argumentům lze
přistoupit pomocí ${1}, ${2}, … Hodnota ${0} je jméno skriptu. Počet
argumentů je dostupný pomocí ${#},1 přičemž jméno skriptu se do počtu
nepočítá. Výraz ${i} (i zde je číslo) je prázdný řetězec, pokud je počet
argumentů menší než i.
Výrazy $* a $@ obsahují všechny argumenty. Liší se pouze jsou-li uvedeny
v uvozovkách: výraz $* obsahuje všechny argumenty jako jedno slovo tak, jak
byly zapsány při spuštění skriptu, kdežto $@ sice také obsahuje všechny
argumenty v jednom řetězci, ale každý z nich bere jako samostatné slovo a lze je
tak procházet pomocí for. Obě poslední hodnoty se typicky používají
v uvozovkách, abychom zabránili expanzi jejich obsahu, čímž by došlo k
rozdělení argumentů s mezerami na samostatná slova. (Bez uvozovek oba cykly
v následujícím příkladu vypíší argumenty po podslovech oddělených mezerami.)
Proč tomu tak je, vysvětluje podkapitola
Dělení slov a IFS.
echo "skript ${0}: pocet predanych argumentu ${#}"
# pomoci if muzeme kontrolovat potrebny pocet argumentu, rekneme, ze chceme aspon 3
if [ ${#} -le 2 ]
then
echo "chyba, chceme aspon 3 argumenty"
exit 1
fi
echo 'vypis argumentu pomoci $*'
for param in "$*"
do
echo ${param}
done
echo 'vypis argumentu pomoci $@'
for param in "$@"
do
echo ${param}
done
Pro procházení argumentů lze použít příkaz shift. Tento příkaz odstraní první
argument, a ostatním argumentům zmenší pořadové číslo o 1 (tj. druhý argument se
stane prvním, třetí druhým atd.). Počet argumentů se také zmenší o 1. Argument
${0} zůstane stejný.
# vypiseme vsechny argumenty a jejich poradi
count=1
while [ -n "${1}" ] #proc uvozovky? ${1} chapeme jako jeden retezec, i kdyz obsahuje mezery
do
echo "parametr ${count} je ${1}"
count=$[${count} + 1]
shift
done
Argumenty můžeme posunout o více míst, příkazu shift lze předat numerický
argument, který určuje, o kolik míst chceme argumenty posunout.
Volby a parametry
Argumenty skriptu rozdělujeme na volby a parametry. Volby typicky začínají pomlčkou, pak následuje jeden znak a za ním případně mezerou oddělená hodnota. Parametry jsou zbytek argumentů. Ve volání
> grep -A 3 -E 'printf' soubor1.c soubor2.c
máme volby -A 3 a -E, první volba má hodnotu 3. Zbytek argumentů ('printf' soubor1.c soubor2.c) jsou parametry. Konvence při zpracování argumentů je
oddělení voleb od parametrů pomocí -- (dvou pomlček). Argumenty z příkladu výše
bychom tedy napsali.
-A 3 -E -- 'printf' soubor1.c soubor2.c
Ve skriptu pak můžeme volby výhodně zpracovat pomocí shift a konstrukce
case.
#predpokladame, ze volby a parametry jsou oddeleny pomoci --
# zpracujeme volby
while [ -n "$1" ]
do
case "$1" in
-a) echo "volba -a" ;;
-b) echo "volba -b, s hodnotou ${2}"
shift ;; # potrebujeme se posunout az za hodnotu
-c) echo "volba -c" ;;
--) shift # nasleduje break, musime se posunout za --
break ;;
*) echo "nepodporovana volba ${1}" ;;
esac
shift
done
# zpracujeme parametry
count=1
for param in "$@"
do
echo "parametr ${count} je ${param}"
count=$[${count} + 1]
shift
done
Ukázka spuštění skriptu
> ./script.sh -b 3 -c -d -a -- prvni druhy
volba -b, s hodnotou 3
volba -c
nepodporovana volba -d
volba -a
parametr 1 je prvni
parametr 2 je druhy
getopt
Pro automatické předzpracování argumentů do požadovaného formátu (rozdělení
voleb a parametrů pomocí -- a rozdělení zhluknutých voleb, kdy pomocí -ab
myslíme -a -b na jednotlivé volby) lze použít příkaz getopt. Syntax
getopt optstring arguments
optstring je řetězec sestávající se z písmen voleb, pokud má volba hodnotu,
píšeme za její písmeno dvojtečku. Program vypíše argumenty ve správném tvaru na
standardní výstup.
> getopt ab:c -ac -b 3 -d prvni druhy
getopt: invalid option -- 'd'
-a -c -b 3 -- prvni druhy
Volby, které nejsou specifikované v optstring, utilita vynechá, ale upozorní na
ně chybovou hláškou. Tu lze potlačit s volbou -q nebo zapsáním dvojtečky jako
prvního symbolu optstring. Pro použití ve skriptu musíme umět nahradit
aktuální argumenty pomocí argumentů získaných utilitou getopt. To se provede
řádkem2
set -- $(getopt :ab:c "$@")
getopts
Poslední možností zpracování voleb je příkaz getopts, který je vylepšením
getopt. Tento příkaz při každém zavolání najde jednu volbu a přiřadí ji do
zvolené proměnné (a případnou hodnotu do proměnné OPTARG). Pokud volbu úspěšně
najde, vrací 0, jinak vrací nenulovou hodnotu. Hodí se tak pro využití přímo
v podmínce cyklu while. Při úspěšném nalezení volby inkrementuje getopts
proměnnou OPTIND o 1. Tato proměnná má výchozí hodnotu 1, po skončení cyklu
tedy obsahuje počet načtených voleb zvětšený o 1. Víme tedy, o kolik se
v argumentech musíme posunout, abychom se dostali k parametrům. Při nalezení
volby nespecifikované v optstring nastaví getopts proměnnou s načtenou volbou
na otazník (?).
while getopts :ab:c opt
# opt je promenna, do ktere prirazujeme volby
# dvojtecka na zacatku optstring potlacuje chybove hlasky
do
case "$opt" in
a) echo "volba a" ;;
b) echo "volba b s hodnotou ${OPTARG}" ;;
c) echo "volba c" ;;
*) echo "neznama volba ${opt}" ;;
esac
done
shift $[${OPTIND} - 1]
count=1
for param in "$@"
do
echo "parametr ${count} je ${param}"
count=$[${count} + 1]
shift
done
Příklad spuštění skriptu
> ./script.sh -b 3 -c -d -a -- param1 param2
volba b s hodnotou 3
volba c
neznama volba ?
volba a
parametr 1 je param1
parametr 2 je param2
-
Symbol
#tady není začátkem komentáře, i když obarvení kódu v naši příkladech ukazuje něco jiného. Je to chyba v algoritmu obarvování kódu. ↩
Vstup pomocí read
Příkaz read je určen ke čtení standardního vstupu (nebo jiného file
descriptoru, viz příští kapitola). Je-li použit bez
dalších argumentů, přečte řádek a uloží jej do proměnné REPLY a skončí úspěšně
(vrátí 0). Pokud se řádek přečíst nepodaří, skončí neúspěšně (vrátí nenulovou
hodnotu). Příkaz akceptuje několik zajímavých argumentů, které lze najít
v dokumentaci,1 některé z nich si ukážeme v následujícím příkladu
# zakladni pouziti
# neuspech lze otestovat pomoci Ctrl-d
if read
then
echo "vlozeno: '${REPLY}'"
else
echo "neuspesny read"
fi
# -p vytiskne prompt
if read -p "Vlozte vstup: "
then
echo "Vlozeno: '${REPLY}'"
else
echo "Neuspesny read"
fi
# parametry jsou promenne, do ktery se ulozi jednotliva vlozena slova, v
# ychozi oddelovac je mezera, lze jej nastavit prepinacem -d
# promenna REPLY je prazdna
if read -p "Vlozte vstup: " var1 var2
then
echo "REPLY: ${REPLY}"
echo "var1: ${var1}"
echo "var2: ${var2}"
else
echo "neuspesny read"
fi
# prepinac -s zabrani tisteni vstupu pri zadavani (jako u hesel)
if read -s -p "Vlozte vstup: " var1 var2
then
echo ""
echo "var1: ${var1}"
echo "var2: ${var2}"
else
echo "neuspesny read"
fi
# prepinac '-n nchar' ukonci cteni po nchar znacich (nebo klasickym zpusobem)
if read -n 1 -p "Chcete pokracovat [Y/N]? " var1
then
case ${var1} in
Y | y) echo
echo "pokracujeme" ;;
N | n) echo
echo "nepokracujeme" ;;
*) echo
echo "${var1} je nespravne zadani" ;;
esac
else
echo "neuspesny read"
fi
# pomoci '-t secs' muzeme dat uzivateli casovy limit na odpoved
if read -t 5 -p "Vstup: " var1
then
echo ${var1}
else
echo
echo "neuspesny read nebo to uzivatel nestihl"
fi
V případě, že po uživateli skriptu potřebujeme nějaké rozhodnutí, je příjemnou
alternativou příkazu read příkaz select. Bližší informace lze nalézt
v [practical] (kapitola 10), případně v manuálu
Bash Manuál.
Příkazem read můžeme číst i ze souboru (po řádcích v cyklu) tak, že soubor
přesměrujeme na standardní vstup cyklu. Čtení není úspěšné, přečte-li read
konec souboru.
# presmerovani pomoci <
count=1
while read line
do
echo "radek ${count}: ${line}"
count=$[${count} + 1]
done < soubor.txt
# alternativne lze rourou do while presmerovat vystup cat
# pozor: telo cyklu pak bezi v podshellu, promenne po cyklu zaniknou
count=1
cat soubor.txt | while read line
do
echo "radek ${count}: ${line}"
count=$[${count} + 1]
done
-
help read↩
File descriptory a přesměrování
File descriptor (FD) je přirozené číslo unikátně identifikující otevřený soubor procesu (v případě skriptu je to proces shellu skript provádějící).1 Deskriptory 0, 1 a 2 jsou rezervovány pro standardní vstup, standardní výstup a standardní chybový výstup. Ve skriptu máme ještě přístup k FD s čísly 3 až 8. Abychom mohli FD použít, musíme jej přesměrovat do/ze souboru či jiného FD. Eventuálně ovšem posloupnost přesměrování musí končit v souboru nebo některém standardním FD.
Při přesměrování z/do FD jej identifikujeme jeho číslem, před nějž vložíme znak
&. Tedy například
echo "go to the fd" >&3 # presmerujeme vystup do FD 3
grep 'pattern' <&4 # grep cte z FD 4
Přesměrování FD, které platí po zbytek skriptu provedeme pomocí příkazu exec.
Při přesměrování do/z souboru je argumentem exec výraz, který je analogický
běžnému přesměrování, jenom na místě příkazu je FD. Například
exec 3>test.txt
Lze přesměrovat i FD do jiného FD, například
exec 3>&1 # presmeruje FD 3 na standardni vystup
Pokud chceme FD zavřít, přesměrujeme jej do &-, například
exec 3>&- # zavreme FD 3.
Další příklad ukazuje, jak dočasně pro část skriptu přesměrovat stdout/stdin
do/z souboru, a potom jej zpět obnovit. (Pomáhá dívat se na FD jako na proměnné,
kterým přesměrováním přiřazujeme vstupní nebo výstupní místo).
#
# presmerovani stdout
#
exec 3>&1 # v deskriptoru 3 si "pamatujeme" stdout
exec 1>foo.txt # stdout presmerujeme do souboru
echo "toto skonci v souboru foo.txt"
exec 1>&3 # tady "obnovime" puvodni stdout
echo "toto se objevi v konzoli"
#
# presmerovani stdin
#
exec 6<&0 # uchovame si stdin
exec 0<bar.txt # presmerujeme do stdin soubor
# cyklus ted cte ze souboru
count=1
while read line
do
echo "radek ${count}: ${line}"
count=$[${count} + 1]
done
exec 0<&6 # obnovime stdin z FD 6
# otestujeme puvodni hodnotu stdin
read -p "vstup: " var
echo "zadal jsi: ${var}"
-
Seznam otevřených souborů procesu lze zobrazit pomocí
lsof. ↩
Signály
Procesy v běžícím systému mezi sebou mohou komunikovat pomocí signálů. Těch je
definováno více než 30 a mají různý význam. Některé už známe: klávesová zkratka
ctrl-c pošle aktivnímu procesu signál SIGINT, zkratka ctrl-z signál
SIGSTOP. Signály obvykle generuje systém nebo běžící procesy. Většinu signálů,
které proces přijme, může tzv. odchytit a nějak na ně reagovat, případně je
ignorovat.1 Mezi signály, které nejde zachytit nebo ignorovat patří SIGSTOP
a SIGKILL (sloužící k pozastavení a bezpodmínečnému ukončení procesu).
Dokumentaci k signálům a jejich významu lze nalézt na manuálové stránce
signal(7).2
K zachycení signálu ve skriptu slouží příkaz trap. Pomocí
trap commands signals
se po přijetí některého ze signálů ze signals provedou příkazy commands. Část
commands se typicky píše v uvozovkách, více příkazů za sebou oddělených
středníkem, v případě nutnosti více řádků lze použít zpětná lomítka na koncích
řádku (podobně jako víceřádková makra v C). V případě delší části commands je
ovšem lepší si vytvořit pro reakci na signál funkci.
V následujícím příkladu je skript, který odchytává SIGINT a po zmáčknutí
ctrl-c neskončí.
trap "echo ' I have trapped Ctrl-C'" SIGINT
count=1
while [ ${count} -le 90 ]
do
echo "loop: ${count}"
sleep 1
count=$[${count} + 1]
done
Mimo signálů můžeme pomocí trap odchytávat i ukončení skriptu, stačí na místě
signálu předat EXIT. Typické využití je pro úklid po práci skriptu, například
smazání dočasných souborů atd.
Past na signál lze modifikovat opakovaným použitím trap, kterým se nastaví na
signál nová reakce. Pokud místo commands uvedeme --, odchytávání signálu je
zrušeno (a skript se vrací k výchozí reakci na signál). Zrušit odchytávání
signálu z předchozího příkladu tak můžeme pomocí
trap -- SIGINT
-
Každý signál má definovanou výchozí reakci, například SIGINT vede k terminaci procesu. ↩
-
man 7 signal↩
Pokročilejší témata
Funkce
Funkce je blok kódu ve skriptu, který má přiřazeno jméno a lze jej opakovaně použít.
function name {
commands
}
Funkci používáme tak, jako bychom používali příkaz se stejným jménem jako funkce. Funkce musí být před použitím definována, jinak dojde k chybě. Jména funkcí musí být ve skriptu unikátní, dvojí definování funkce stejného jména vede k zapomenutí první definice (bez chybového hlášení). Pokud vytvoříme funkci se stejným jménem jako jiný příkaz, bude tento příkaz ve zbytku skriptu nepřístupný, místo něj se bude volat funkce.
# priklad volani funkce, skript vypise dvakrat 'Toto je funkce foo'
function foo {
echo "Toto je funkce foo"
}
foo
foo
Na funkci se v mnoha ohledech dá dívat jako na miniskript:
1. Funkce jako jiný příkaz vrací návratovou hodnotu. Je to buď návratová
hodnota posledního příkazu provedeného v těle funkce, nebo hodnota, která je
vrácena příkazem return. Pro návratové hodnoty funkcí platí stejná pravidla,
jako pro návratové hodnoty skriptu.
2. Můžeme přesměrovat standardní výstup z funkce do proměnné pomocí syntaxe podobné subshellu.
function foo {
echo "testovaci vystup"
}
var=$(foo)
echo ${var} # toto vypise 'testovaci vystup'
3. Funkci můžeme předat argumenty a zpracovat je uvnitř jako ve skriptu.
(Jméno funkce je uloženo v ${0}, další argumenty jsou ${1} …, atd.)
function foo {
count=1
while [ -n "${1}" ]
do
echo "parametr ${count} je ${1}"
count=$[${count} + 1]
shift
done
}
echo "prvni volani"
foo -a -b param1 param2
echo "druhe volani"
foo 120 130
4. Proměnné ve skriptu jsou globální. Pokud bychom v předchozím kódu nahradili za poslední 4 řádky následující
echo "prvni volani"
foo -a -b param1 param2
echo "hodnota count je ${count}"
echo "druhe volani"
foo 120 130
echo "hodnota count je ${count}"
vypíše skript nejdříve jako hodnotu count číslo 5 a posléze číslo 3. Pracovat
uvnitř funkcí s globálními proměnnými není příliš vhodné (z podobných důvodů,
jako musíme být s globálními proměnnými opatrní v běžném programování). Lokální
proměnné vytváříme pomocí slovíčka local, které použijeme při vytvoření
proměnné. Ta je tak platná pouze ve funkci, překryje případnou globální proměnnou
stejného jména.
temp=1
function foo {
local temp=10 # uvnitr funkce je promenna temp lokalni
temp=$[${temp} + 2]
}
foo
echo "hodnota temp je ${temp}" # tady se vypise hodnota 1
5. Do funkce lze přesměrovat standardní vstup. Například
function foo {
local line
while read line
do
echo ${line}
done
}
foo < blah.txt
vypíše obsah souboru blah.txt.
Pole
Bash podporuje numericky indexovaná pole, indexuje se od 0. Níže vidíme syntax vytvoření pole a přístupu k jednotlivým prvkům
# vytvoreni pole vyctem
name=(element1 element2 ...)
# vytvoreni pole pomoci explicitni deklarace
declare -a anotherArray
# pristup k prvku na indexu index
${name[index]}
Všechna existující pole lze vypsat pomocí1
foo=(ahoj svete blah)
declare -a
vypíše
[...]
declare -a foo=([0]="ahoj" [1]="svete" [2]="blah")
[...]
Čtenář může experimenty ověřit, že
-
K jednoduché proměnné lze přistupovat jako k poli, hodnota proměnné je na indexu 0, na ostatních indexech je prázdný řetězec;
declare -aovšem jednoduché proměnné nevypisuje. -
Pole lze vytvořit i přímým vložením hodnoty na nějaký index, tj. např.
foo[3]=trivytvoří pole, které má na indexu 3 hodnotutri;declare -avypisuje pouze hodnoty na indexech, ke kterým bylo přistupováno.
Ke všem prvkům pole najednou lze přistoupit pomocí speciálních indexů * nebo
@. V případě * je z hodnot vytvořen řetězec, kde jsou hodnoty odděleny
pomocí prvního znaku IFS. V případě @ jsou prvky pole odděleny mezerami.
Je-li ovšem @ použito v uvozovkách, pamatuje si, že je seznamem položek bez
ohledu na oddělovače.
foo=("jedna jj" dva tri)
IFS=:
A=(${foo[*]})
B=(${foo[@]})
C=("${foo[*]}")
D=("${foo[@]}")
declare -a
Vypíšeme
[..]
declare -a A=([0]="jedna jj" [1]="dva" [2]="tri")
declare -a B=([0]="jedna jj" [1]="dva" [2]="tri")
declare -a C=([0]="jedna jj:dva:tri")
declare -a D=([0]="jedna jj" [1]="dva" [2]="tri")
[..]
Na první pohled je záhadný řádek, kde bylo vytvořeno A. Souvisejícím výkladem
je podkapitola Dělení slov a IFS. Expanze shellu tady
slovo jedna jj:dva:tri rozdělila ve fázi dělení slov pomocí znaků IFS. Pokud
je ovšem slovo v uvozovkách, tato expanze se potlačí a projeví se rozdíl, jako
v našem příkladu u C a D. Upozorníme také, že pokud provedeme
foo=("jedna jj" dva tri)
# hodnota IFS je defaultni, obsahuje tedy mezeru
A=(${foo[*]})
B=(${foo[@]})
declare -a
dostaneme výše zmíněným expanzním mechanismem
[..]
declare -a A=([0]="jedna" [1]="jj" [2]="dva" [3]="tri")
declare -a B=([0]="jedna" [1]="jj" [2]="dva" [3]="tri")
[..]
To typicky není vyžadováno, v základu tedy doporučuji používat uvozovky.
Lze pracovat i s asociativními poli, kde se indexuje řetězci.
# asociativnimu poli patri prepinac -A u declare
declare -A bar
bar[ahoj]=10
bar[svete]=20
declare -A
s výsledkem
[..]
declare -A bar=([ahoj]="10" [svete]="20" )
[..]
-
Builtin
declarelze použít k deklarování či výpisu proměnných vybraných vlastností, dokumentacehelp declare. ↩
Misc
Drobnosti, které se nevešly jinam.
set
set je velmi komplikovaný builtin (dělá mnoho věcí). Spustíme-li jej bez
argumentů, vypíše seznam všech proměnných s hodnotami a funkcí. Jinak set umí
zapínat a vypínat některé vlastnosti shellu a nastavovat poziční argumenty.
set -o vlastnost # zapnuti
set +o vlastnost # vypnuti
#pripadne zkracene
set -zkratka # zapnuti
set +zkratka # vypnuti
vlastnost nahradíme jménem vlastnosti, zkratka je jednopísmenná zkratka
vlastnosti. Tabulku všech vlastností (se jménem, zkratkou a funkcí) lze nalézt
v dokumentaci a literatuře. Seznam nastavených vlastností je uložen v proměnné
- (zobrazit tedy lze echo ${-}).1
Jako příklad uvedeme vlastnost xtrace (zkratka x), která je defaultně vypnutá.
Po zapnutí shell zobrazí (spolu s promptem PS4) přečtené části skriptu, po
přečtení a po expanzi.
set -x
IFS=:
for dir in ${PATH}
do
echo -n ${dir}
echo " $(ls ${dir} | wc -l)"
done | sort -r -k 2
vede k
+ IFS=:
+ sort -r -k 2
+ for dir in ${PATH}
+ echo -n /usr/local/bin
++ wc -l
++ ls /usr/local/bin
+ echo ' 11'
+ for dir in ${PATH}
+ echo -n /usr/bin
++ wc -l
++ ls /usr/bin
+ echo ' 2886'
+ for dir in ${PATH}
+ echo -n /bin
++ wc -l
++ ls /bin
+ echo ' 166'
+ for dir in ${PATH}
+ echo -n /usr/bin/X11
++ wc -l
++ ls /usr/bin/X11
+ echo ' 2886'
+ for dir in ${PATH}
+ echo -n /usr/games
++ wc -l
++ ls /usr/games
+ echo ' 1'
/usr/bin/X11 2886
/usr/bin 2886
/bin 166
/usr/local/bin 11
/usr/games 1
(Čtenář vypozoruje, že zapnuté vlastnosti jsou předány do podshellu a tak
příkazy v podshellu pozná podle dvou +. Lze vypozorovat i něco o pořadí ve
vztahu k přesměrování.)
Pomocí
set -- arg1 arg2 .. argn
jsou pozičním proměnným 1, 2, …, až n (pro vstupní argumenty
skriptu/funkce) nastaveny hodnoty arg1 až argn a proměnné # je nastavena
hodnota n. Funguje i pro argumenty začínající znakem -.2
-
Další vlastnosti lze měnit pomocí
shopt. ↩ -
Znak
-je bez použití--jako prvního argumentu použit jako oddělovač přepínačů pro set a argumentů, které se mají přiřadit pozičním proměnným. ↩
source
source file args
. file args # alternativni jmeno
vloží obsah souboru file na místo spuštění source. Soubor file (pokud není
zadán plnou cestou, absolutní nebo relativní) je hledán v adresářích v proměnné
PATH a poté v aktuálním adresáři. Volitelné argumenty args jsou předány
souboru file jako poziční argumenty.
Příkaz je výhodné použít pro rozdělení skriptu do více souborů. (Rozdíl oproti
spuštění souboru file jako skriptu je v tom, že se nevytváří podshell. To má
důsledky například při práci s proměnnými).
Úkoly
Cílem je napsat skript.
-
[zápočtová úloha]
Vypište seznam všech souborů a podadresářů v aktuálním adresáři s informacemi o typu (soubor, adresář, symbolický odkaz) a právech (čtení, zápis, spuštění) pro spouštějícího uživatele, ve tvaru:
jméno_souboru typ práva. Řešte bez použití programuls.Upravte skript tak, aby
- při zadání argumentu
-a, zpracoval i skryté soubory a adresáře; - přijímal jako argument adresář, jehož obsah má vypsat. Při zadání neexistujícího adresáře skončí skript s chybou.
- při zadání argumentu
-
Naprogramujte „hádání“ (celého) čísla, které si uživatel myslí, z intervalu zadaného až dvěma argumenty, pomocí série dotazů „je číslo menší/větší než X?“ s odpověďmi ano/ne. Při jednom argumentu je levá mez intervalu 0, při žádném navíc pravá mez 100.
-
Implementujte zjednodušenou verzi programu
seq: výpis posloupnosti (celých) čísel oddělených mezerou od čísla zadaného jako první argument do čísla zadaného jako třetí argument, obojí včetně, s přírůstkem zadaným jako druhý argument – kladným, pokud je počáteční číslo menší nebo rovno koncovému, jinak záporným. Při dvou argumentech je chybějící přírůstek roven 1, při jednom je i chybějící počáteční číslo rovno 1. -
[zápočtová úloha]
-
Do aktuálního adresáře vygenerujte 1000 souborů se jmény
fooNNNN.txt, kdeNNNNnahraďte pořadovými čísly 0001 až 1000. Do každého souboru zapište náhodně vytvořené datum (ne nutně reálně existující, např. 30 únor je OK) ve tvaruDD/MM/YYYY. (Dny, měsíce a roky v obvyklém rozsahu.) (Nápověda:${RANDOM}.) -
Uvažujme adresář obsahující soubory ve tvaru
*NNNN.txt(symbol*představuje libovolnou předponu), každý z nich obsahuje datum ve tvaruDD/MM/YYYY. Vytvořte skript s tímto adresářem jako argumentem, který přesune soubory do podadresářů adresáře s cestami ve tvaruYYYY/MM/DDtak, aby si obsah souboru a jeho umístění odpovídaly. Každý podadresář bude existovat, jen pokud v něm bude alespoň jeden soubor.
-
-
Vypište řádek ze souboru
/etc/passwd, který se nachází nad řádkem odpovídajícím uživateli, který skript spustil. -
Vytvořte skript, který bude obálkou pro příkaz
mv. Bude brát stejné parametry a fungovat jakomv, pouze jméno cílového adresáře bude očekávat jako první parametr. -
Vytvořte skript se dvěma jmény (
min.shamax.sh), který bude dostávat jako parametry sérii čísel a spočítá jejich minimum nebo maximum, podle toho, se kterým jménem byl zavolán. (dvě jména = dva soubory se stejným obsahem, případně soubor a link, nápověda:basename) -
[zápočtová úloha]
Vytvořte skript, jehož prvním argumentem je adresář, zbytek argumentů pak tvoří přepínače pro příkaz
find. Skript příkazfindzavolá a soubory, které najde, interaktivně zkopíruje do adresáře předaného skriptu jako první argument. Před kopií každého souboru se skript uživatele zeptá, zda kopii provést. Možné odpovědi jsouano— soubor je zkopírován,ne— implicitní odpověď, soubor není zkopírován,stop— soubor není zkopírován a skript skončí.
Pokud skript neskončí pomocí
stop(ale projitím všech souborů), vypíše počet zkopírovaných souborů. -
[zápočtová úloha]
Vytvořte skript, jehož argumenty budou adresář
Aa souborS, obsahující na každém řádku koncovku jmen souborů (např.txt,jpeg). Skript v aktuálním adresáři vytvoří archiv souborů z adresářeA, které mají koncovky v souboruS. Archiv pojmenujeX-DDMMYYYY-HHMM.zip, kdeDDMMYYYYje datum aHHMMje čas spuštění skriptu. -
Vytvořte skript, který zjistí, jestli je aktuální uživatel
root. -
Napište skripty
min.shamax.shz úlohy 7 znovu tak, aby nespouštěly žádný externí program — tedy pouze s použitím expanze parametrů, aritmetické expanze$(( ))a vestavěných konstrukcí. -
Napište skript, který dostane jako argumenty jména souborů a u každého vypíše zvlášť adresář, jméno bez přípony a příponu, ve tvaru
adresář | jméno | přípona. Řešte bez použitíbasename,dirname,sedacut. Otestujte skript i na jménech obsahujících mezery a na jménech s více tečkami (archiv.tar.gz). -
Vytvořte skript, který v zadaném adresáři přejmenuje všechny soubory
*.txttak, že mezery ve jméně nahradí podtržítky. Skript musí korektně fungovat i v adresáři, kde žádný takový soubor není (nesmí se pokusit zpracovat soubor jménem*.txt), a nesmí přepsat existující soubor. -
Vezměte skript z úlohy 4b a ověřte jej příkazem
shellcheck. Opravte všechna hlášení SC2086 (chybějící uvozovky) a vysvětlete u každého z nich, jaký konkrétní vstup by chybu vyvolal.
Úkoly jsou převzaty (a případně drobně upraveny) z literatury a z webu http://outrata.inf.upol.cz/courses/shell/seminars.html.
Literatura
[bible] Richard Blum, Christine Bresnahan. Linux Command Line and Shell Scripting Bible. Wiley, 2015.
[examples] Ellie Quigley. Unix shells by example. Prentice Hall, 2015.
[practical] Mark G. Sobell. A practical guide to Linux commands, editors, and shell programming. Prentice Hall, 2013.
[forst] Libor Forst. Shell v příkladech aneb aby váš unix skvěle shell. Matfyzpress, 2010.
[bashmanual] Bash Manuál