Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Ú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í

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í.


  1. 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.)


  1. Alternativy: env, printenv zobrazí 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.


  1. 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í:

  1. expanze závorek — {a,b,c}, {1..10} (viz Expanze závorek)
  2. expanze vlnovky — ~, ~uzivatel (viz Expanze vlnovky)
  3. expanze parametrů a proměnných — ${var}, ${var:-default}, ${var%.txt} (viz Expanze parametrů)
  4. aritmetická expanze — $(( 2 + 3 )) (viz Aritmetická expanze)
  5. substituce příkazů — $(date) (viz Substituce příkazů a procesů)
  6. dělení slov — rozdělení výsledku podle IFS (viz Dělení slov a IFS)
  7. expanze jmen souborů — *.txt, zaloha?.tar (viz Expanze jmen souborů)
  8. 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ázesyntaxpotlačí '…'potlačí "…"
expanze závorek{a,b}anoano
expanze vlnovky~, ~useranoano
expanze parametrů${var}anone
aritmetická expanze$(( … ))anone
substituce příkazů$( … )anone
dělení slovpodle IFSanoano
expanze jmen souborů*, ?, […]anoano

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.


  1. 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ýrazvý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ýrazvý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ýrazvý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ýrazvý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.


  1. $[ … ] 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:

  1. 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).
  2. 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ápisvý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íkexpanze
*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.

volbavýznam
nullglobneshodující se vzor se odstraní
failglobneshodující se vzor způsobí chybu
dotglob* zahrne i soubory začínající tečkou
nocaseglobignoruje velikost písmen
globstarzpřístupní vzor **
extglobzapne 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řídavý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 |.

vzorvý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.

sekvenceznak
\nnový řádek
\ttabulátor
\rnávrat vozíku
\\zpětné lomítko
\'apostrof
\nnnznak s osmičkovým kódem nnn
\xHHznak se šestnáctkovým kódem HH
\uHHHHznak 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ávorekanoanoanoano
expanze vlnovkyanoanoanoano
expanze parametrůanoanoneano
aritmetická expanzeanoanoneano
substituce příkazůanoanoneano
dělení slovanoanoanoano
expanze jmen souborůanoanoanoano
interpretace \n, \t—neneano

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 je adresar prá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 nullglob nebo 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
    
  • read bez -r. Bez tohoto přepínače read interpretuje 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.txt
    

    Prá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


  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é test neumí.

  • dvojí hranaté závorky na místě tst (tj. výraz [[ expression ]]): pro řetězcové porovnávání, obsahuje to co test, 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.


  1. 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

  1. 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. ↩

  2. set vysvětlíme později. ↩

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

  1. 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}"

  1. 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

  1. Každý signál má definovanou výchozí reakci, například SIGINT vede k terminaci procesu. ↩

  2. 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 -a ovš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]=tri vytvoří pole, které má na indexu 3 hodnotu tri; declare -a vypisuje 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" )
[..]

  1. Builtin declare lze použít k deklarování či výpisu proměnných vybraných vlastností, dokumentace help 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


  1. Další vlastnosti lze měnit pomocí shopt. ↩

  2. 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.


  1. [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í programu ls.

    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.
  2. 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.

  3. 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.

  4. [zápočtová úloha]

    1. Do aktuálního adresáře vygenerujte 1000 souborů se jmény fooNNNN.txt, kde NNNN nahraď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 tvaru DD/MM/YYYY. (Dny, měsíce a roky v obvyklém rozsahu.) (Nápověda: ${RANDOM}.)

    2. Uvažujme adresář obsahující soubory ve tvaru *NNNN.txt (symbol * představuje libovolnou předponu), každý z nich obsahuje datum ve tvaru DD/MM/YYYY. Vytvořte skript s tímto adresářem jako argumentem, který přesune soubory do podadresářů adresáře s cestami ve tvaru YYYY/MM/DD tak, 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.

  5. Vypište řádek ze souboru /etc/passwd, který se nachází nad řádkem odpovídajícím uživateli, který skript spustil.

  6. Vytvořte skript, který bude obálkou pro příkaz mv. Bude brát stejné parametry a fungovat jako mv, pouze jméno cílového adresáře bude očekávat jako první parametr.

  7. Vytvořte skript se dvěma jmény (min.sh a max.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)

  8. [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říkaz find zavolá 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 jsou

    • ano — 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ů.

  9. [zápočtová úloha]

    Vytvořte skript, jehož argumenty budou adresář A a soubor S, 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áře A, které mají koncovky v souboru S. Archiv pojmenuje X-DDMMYYYY-HHMM.zip, kde DDMMYYYY je datum a HHMM je čas spuštění skriptu.

  10. Vytvořte skript, který zjistí, jestli je aktuální uživatel root.

  11. Napište skripty min.sh a max.sh z ú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í.

  12. 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, sed a cut. Otestujte skript i na jménech obsahujících mezery a na jménech s více tečkami (archiv.tar.gz).

  13. Vytvořte skript, který v zadaném adresáři přejmenuje všechny soubory *.txt tak, ž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.

  14. 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