Skript je obyčejný textový soubor s příkazy, které bys jinak psal do terminálu ručně — jen je shell přečte a odbaví jeden po druhém sám. Místo abys deset příkazů opisoval pokaždé znovu (a udělal překlep), napíšeš je jednou do souboru a spustíš ho jedním jménem. Bash scripting je tak most mezi „umím pár příkazů" a „mám zautomatizované věci, které za mě dělá stroj". Až pochopíš pár pravidel okolo proměnných a uvozovek, přestaneš se skriptů bát — a přestanou tě kousat.
Pár pojmů na úvod
- Shell = program, který čte tvoje příkazy a spouští je.
bash(Bourne Again SHell) je nejběžnější; skript pro bash se často říká „bash skript". - Skript = textový soubor s posloupností příkazů + trocha logiky (podmínky, cykly). Nic zkompilovaného — čte se řádek po řádku, shodně s tím, cos psal ručně.
- Shebang = první řádek
#!/usr/bin/env bash, který systému říká, čím ten soubor spustit. - Proměnná = pojmenované místo pro hodnotu (
name=Karel). Odkážeš se na ni přes$name. - Argument = to, co skriptu předáš při spuštění (
./skript.sh soubor.txt→soubor.txtje$1). - Exit kód = číslo, kterým příkaz hlásí, jak dopadl:
0= úspěch, cokoli jiného = chyba.
Proč vůbec skriptovat
Představ si, že po každém buildu projektu děláš pořád tohle: smažeš starou složku, zkopíruješ nové soubory, nastavíš práva, restartuješ službu. Pět příkazů, každý den, ručně. Dřív nebo později v jednom uděláš překlep — nebo si nevzpomeneš na pořadí. Skript to udělá vždycky stejně a bez chyby.
Skriptuj, kdykoli:
- děláš tu samou věc podruhé potřetí — opakování je signál „tohle patří do skriptu",
- chceš spustit něco pravidelně (zálohu, úklid logů) přes
cronnebosystemd timer, - chceš řetězec kroků, který si zapamatuje sám stroj a ne tvoje hlava.
💡 Pravidlo palce: uděláš-li stejnou sekvenci příkazů potřetí, napiš na ni skript. Ušetří to čas a chyby — stroj se v opisování nesplete.
První skript — shebang, práva, spuštění
Vytvoř soubor pozdrav.sh:
#!/usr/bin/env bash
echo "Ahoj, tady tvůj první skript!"
echo "Dneska je $(date +%A)."
První řádek #!/usr/bin/env bash je shebang (#! + cesta k interpretu). Říká systému: „tenhle
soubor spusť bashem". Proč /usr/bin/env bash a ne rovnou /bin/bash? Protože env najde bash
v PATH ať je nainstalovaný kdekoli — funguje to přenositelně napříč distry i macOS.
Samotný soubor ale ještě není spustitelný. Musíš mu dát právo na spuštění:
chmod +x pozdrav.sh # přidá "executable" bit
./pozdrav.sh # spustí ho
Proč ./ před jménem? Protože z bezpečnosti shell nehledá skripty v aktuální složce automaticky
(jinak by ti stačilo mít v pracovní složce zlomyslný ls). ./pozdrav.sh znamená „spusť soubor
pozdrav.sh z téhle složky".
🚩 Když zapomeneš
chmod +x, dostanešPermission denied. Když zapomeneš./, dostanešcommand not found. Obojí jsou úplně běžné začátečnické zádrhely — teď víš proč.
Proměnné — pozor na mezery
Proměnná se přiřazuje jmeno=hodnota. Kolem = nesmí být mezery — to je nejčastější začátečnická
chyba vůbec:
name=Karel # ✅ správně
name = Karel # ❌ CHYBA: bash to čte jako "spusť příkaz 'name' s argumenty '=' a 'Karel'"
Hodnotu proměnné vytáhneš přes $name nebo ${name}:
#!/usr/bin/env bash
name=Karel
echo "Ahoj $name" # Ahoj Karel
echo "Ahoj ${name}!" # složené závorky = jasná hranice jména
echo "Soubor: ${name}_zaloha.txt" # bez závorek by bash hledal proměnnou "name_zaloha"
Složené závorky ${name} použij vždy, když za jménem hned pokračuje text — jinak by shell nevěděl,
kde jméno proměnné končí.
⚠️ VŽDYCKY uvozuj proměnné: "$var"
Tohle je asi nejdůležitější pravidlo celé kapitoly. Když proměnnou nedáš do dvojitých uvozovek, bash ji rozdělí podle mezer — a jakmile v ní mezera je (nebo je prázdná), skript se rozbije:
soubor="muj dokument.txt"
rm $soubor # ❌ bash to vidí jako: rm muj dokument.txt → maže DVA soubory!
rm "$soubor" # ✅ maže jeden soubor s mezerou v názvu
Stejný problém nastane, když je proměnná prázdná:
cesta=""
ls $cesta # ❌ z "ls" se stane holé "ls" → vypíše celou aktuální složku (ne to, cos čekal)
ls "$cesta" # ✅ ls dostane jeden (prázdný) argument
🚩 Zapamatuj si to jako mantru: každá
$proměnnápatří do"uvozovek", dokud nemáš výslovný důvod je vynechat. Ušetří ti to hodiny hledání, proč skript maže/kopíruje špatné věci.
Argumenty — co skriptu předáš
Když skript spustíš s něčím za jménem, dostane to jako poziční argumenty:
#!/usr/bin/env bash
echo "První argument: $1"
echo "Druhý argument: $2"
echo "Počet argumentů: $#"
echo "Všechny naráz: $@"
Spuštění:
./argy.sh jablko hruska
# První argument: jablko
# Druhý argument: hruska
# Počet argumentů: 2
# Všechny naráz: jablko hruska
$1,$2,$3… = jednotlivé argumenty podle pořadí,$#= kolik jich přišlo (hodí se na kontrolu),$@= všechny argumenty (v cyklu je uvozuj"$@", ať se zachovají mezery),$0= jméno samotného skriptu.
Typická kontrola na začátku skriptu — „dostal jsem, co potřebuju?":
#!/usr/bin/env bash
if [[ $# -lt 1 ]]; then
echo "Použití: $0 <soubor>" >&2
exit 1
fi
echo "Zpracovávám soubor: $1"
Command substitution — výstup příkazu do proměnné
Chceš do proměnné dostat výsledek jiného příkazu? Použij $(prikaz):
#!/usr/bin/env bash
dnes=$(date +%Y-%m-%d)
pocet=$(ls -1 | wc -l)
echo "Dnes je $dnes a v téhle složce je $pocet položek."
$(...) znamená „spusť to uvnitř a dosaď sem jeho výstup". Dá se i vnořovat a kombinovat:
zaloha="zaloha_$(date +%Y%m%d_%H%M).tar.gz"
echo "Vytvořím soubor: $zaloha"
💡 Uvidíš i starší zápis s obrácenými apostrofy
`prikaz`. Dělá totéž, ale nejde vnořovat a hůř se čte — používej$(...).
Podmínky — if [[ ... ]]
Rozhodování ve skriptu:
#!/usr/bin/env bash
soubor="config.txt"
if [[ -f "$soubor" ]]; then
echo "Soubor existuje, čtu ho."
elif [[ -d "$soubor" ]]; then
echo "To je složka, ne soubor!"
else
echo "Soubor neexistuje."
fi
[[ ]] vs staré [ ]
Uvidíš dvě varianty testu. Používej [[ ]] (bashové, novější) — je bezpečnější:
[ ]je starý POSIX test (vlastně samostatný příkaz). Neuvozená prázdná proměnná v něm způsobí syntaktickou chybu.[[ ]]je bashová vestavěná konstrukce. Nerozpadá se na prázdných proměnných, umí&&,||a porovnávání vzorů. Prostě lepší.
Nejběžnější testy:
| Test | Pravda když… |
|---|---|
-f "$x" | $x je běžný soubor |
-d "$x" | $x je složka (directory) |
-e "$x" | $x existuje (soubor nebo složka) |
-z "$x" | řetězec je prázdný (zero length) |
-n "$x" | řetězec není prázdný |
"$a" == "$b" | řetězce se rovnají |
"$a" != "$b" | řetězce se liší |
$a -eq $b | čísla se rovnají (-lt -gt -le -ge pro porovnání) |
Pozor na rozdíl: == je pro text, -eq pro čísla. "10" == "10.0" je nepravda (jiný text),
ale 10 -eq 10 pravda.
vek=18
if [[ $vek -ge 18 ]]; then
echo "Dospělý."
fi
if [[ -z "$1" ]]; then
echo "Nedostal jsem žádný argument." >&2
exit 1
fi
Cykly — for a while
for projde seznam položek:
#!/usr/bin/env bash
for barva in červená zelená modrá; do
echo "Barva: $barva"
done
Nejčastěji se prochází soubory. Nikdy neparsuj ls (viz Failure modes) — použij zástupné znaky
(glob), ty umí bash sám a poradí si i s mezerami v názvech:
for f in *.txt; do
echo "Zpracovávám: $f"
done
while opakuje, dokud platí podmínka. Skvělé na čtení souboru řádek po řádku:
#!/usr/bin/env bash
while read -r radek; do
echo "Řádek: $radek"
done < seznam.txt
(read -r čte řádek „tak jak je" — bez -r by bash žral zpětná lomítka.)
Počítadlo přes while:
i=1
while [[ $i -le 5 ]]; do
echo "Krok $i"
i=$((i + 1)) # $(( )) = aritmetika
done
Funkce — pojmenované kousky logiky
Když se ti stejný blok opakuje, dej mu jméno:
#!/usr/bin/env bash
pozdrav() {
local jmeno="$1" # local = proměnná žije jen uvnitř funkce
echo "Ahoj, $jmeno!"
}
pozdrav "Karel"
pozdrav "Anna"
- Argumenty funkce se berou stejně jako u skriptu:
$1,$2,$@. localdrží proměnnou uvnitř funkce, ať ti nezasahuje do zbytku skriptu — používej ho.- Funkce vrací exit kód (ne hodnotu). „Výsledek" buď vypiš přes
echoa chyť command substitution, nebo nastav globální proměnnou.
je_sude() {
if [[ $(( $1 % 2 )) -eq 0 ]]; then
return 0 # 0 = úspěch/pravda
else
return 1 # nenula = neúspěch/nepravda
fi
}
if je_sude 4; then
echo "4 je sudé."
fi
Exit kódy — jak příkaz hlásí (ne)úspěch
Každý příkaz po doběhnutí nastaví návratový kód. Poslední si přečteš z $?:
ls /neexistuje
echo $? # vypíše nenulu (třeba 2) → příkaz selhal
ls /home
echo $? # 0 → úspěch
Konvence je pevná: 0 = úspěch, cokoli nenulového = nějaká chyba. Tvůj skript by to měl dodržet
taky — na konci nebo při chybě zavolej exit N:
if [[ ! -f "$1" ]]; then
echo "Soubor $1 neexistuje." >&2
exit 1 # skript končí chybou
fi
# ... práce ...
exit 0 # explicitní „vše OK"
Díky tomu tvůj skript umí spolupracovat s dalšími (./muj.sh && echo hotovo proběhne jen při úspěchu).
⚠️ set -euo pipefail — bezpečnostní pás skriptu
Ve výchozím stavu je bash překvapivě shovívavý: když příkaz selže, skript klidně běží dál jako by
se nic nestalo. To je recept na katastrofu — představ si skript, kterému selže cd /cilova/slozka
a on pak vesele mazání spustí v úplně jiné složce.
Proto na začátek každého vážného skriptu (hned pod shebang) patří:
#!/usr/bin/env bash
set -euo pipefail
Co ty tři přepínače dělají:
-e(errexit) — skonči hned, jakmile nějaký příkaz selže (vrátí nenulu). Konec „běží dál po chybě". Nejdůležitější z celé trojice.-u(nounset) — chybu vyhodí i použití neexistující proměnné. Když napíšeš$adresara měl jsi$adresář(překlep), skript spadne místo aby tiše pracoval s prázdnem. Chytí ti tím spoustu tichých chyb.-o pipefail— v rouře (a | b | c) selže celá roura, když selže kterýkoli článek, ne jen ten poslední. Bez tohofalse | echo ahoj„projde" jako úspěch, protože posledníechouspěl.
💡
set -euo pipefailje průmyslový standard „bezpečného bashe". Ber ho jako bezpečnostní pás: nepřidává funkce, jen tě zastaví dřív, než malér naroste. Napiš ho na druhý řádek každého skriptu.
Malá výjimka: s -e občas potřebuješ příkaz, který smí selhat. Přidej || true:
grep "hledám" soubor.txt || true # grep vrací nenulu když nic nenajde — a to je tady OK
Poskládáme to dohromady
Malý, ale realistický skript — zálohuje složku a hlídá vstup:
#!/usr/bin/env bash
set -euo pipefail
# Použití: ./zaloha.sh <zdrojová_složka> <cílová_složka>
zdroj="${1:-}" # ${1:-} = když $1 chybí, dosaď prázdno (jinak by -u shodil skript)
cil="${2:-}"
if [[ -z "$zdroj" || -z "$cil" ]]; then
echo "Použití: $0 <zdrojová_složka> <cílová_složka>" >&2
exit 1
fi
if [[ ! -d "$zdroj" ]]; then
echo "Chyba: '$zdroj' není složka." >&2
exit 1
fi
nazev="zaloha_$(date +%Y%m%d_%H%M%S).tar.gz"
mkdir -p "$cil"
tar -czf "$cil/$nazev" "$zdroj"
echo "Hotovo: $cil/$nazev"
Všimni si všech pravidel z kapitoly: shebang, set -euo pipefail, uvozené proměnné, kontrola
argumentů, command substitution na jméno souboru, smysluplný exit 1 při chybě.
Failure modes — časté omyly, které kousají
- Neuvozené proměnné.
rm $soubormístorm "$soubor". Jakmile je v hodnotě mezera nebo je prázdná, bash ji rozseká a skript pracuje s úplně jinými soubory. Vždy"$var". - Mezery kolem
=.name = Karelnení přiřazení — bash to bere jako spuštění příkazuname. Přiřazení jename=Karel, bez mezer. - Spoléhání na návratový kód bez
set -e. Bezset -euo pipefailskript po selhání příkazu běží klidně dál a napáchá škody „naslepo". Napiš ho na druhý řádek. rm -rf "$DIR/"když je$DIRprázdné. Rozvine se narm -rf /→ maže celý systém. Prázdná/nenastavená proměnná je bomba. Chraň seset -u, defaultem"${DIR:?musí být nastaveno}", a kontrolou[[ -n "$DIR" ]]před mazáním.- Parsování výstupu
ls.for f in $(ls)se rozbije na názvech s mezerami a speciálních znacích. Používej globy (for f in *.txt) nebofind ... -print0swhile read -r -d ''. - Zapomenutý
chmod +x/./.Permission denied= chybí spustitelný bit;command not found= chybí./před jménem. Nejsou to chyby ve skriptu, ale ve spuštění.
🛠️ Cvičení
- První spustitelný skript. Napiš
ahoj.sh, který vypíše „Ahoj" a aktuální datum (přes command substitution). Nastav mu práva a spusť ho. Jaké dva příkazy jsi potřeboval kromě napsání souboru? - Proč uvozovat. Vytvoř proměnnou
x="a b c"a zkusecho $xvsecho "$x". Pak si zkus, co udělátouch $xvstouch "$x"(v prázdné složce, pakls). Vysvětli rozdíl vlastními slovy. - Skript s argumentem a kontrolou. Napiš skript, který dostane jako
$1název souboru a vypíše „existuje" / „neexistuje" podle[[ -f ... ]]. Když$1chybí, ať skončí sexit 1a nápovědou. - Cyklus přes soubory. Napiš skript, který projde všechny
*.txtv aktuální složce a u každého vypíše jeho jméno a počet řádků (wc -l). Proč použít glob*.txta ne$(ls *.txt)? - Bezpečnostní pás. Vezmi kterýkoli předchozí skript, přidej
set -euo pipefaila schválně tam napiš překlep v názvu proměnné. Co se stane teď a co by se stalo bez-u?
Náčrt řešení — rozbal, až si cvičení zkusíš sám
- První skript — obsah:
#!/usr/bin/env bashaecho "Ahoj, dnes je $(date)". Pakchmod +x ahoj.sh(spustitelný bit) a./ahoj.sh(spuštění z aktuální složky). Potřeboval jsi tedychmod(práva) a./prefix (aby shell soubor našel). Pozor: bezchmod +x→Permission denied, bez./→command not found. - Proč uvozovat —
echo $xvypíšea b c(bash rozdělí na tři slova aechoje spojí mezerami — vypadá stejně, ale prošla tři slova),echo "$x"pošle jedno slovo. Utouch $xvzniknou tři souborya,b,c;touch "$x"vytvoří jeden soubor jménema b c. Rozdíl: neuvozená proměnná se rozseká podle mezer (word splitting). Pozor: přesně tohle rozbíjírm,cp,mvs reálnými názvy souborů. - Argument + kontrola — kostra:
if [[ -z "$1" ]]; then echo "Použití: $0 <soubor>" >&2; exit 1; fia pakif [[ -f "$1" ]]; then echo "existuje"; else echo "neexistuje"; fi.-z "$1"chytí chybějící argument,-ftestuje běžný soubor. Pozor:$1uvozuj ("$1"), jinak prázdný argument test rozbije. - Cyklus přes soubory —
for f in *.txt; do echo "$f: $(wc -l < "$f") řádků"; done. Glob*.txtrozbalí bash sám a správně i pro názvy s mezerami;$(ls *.txt)se přes word splitting rozpadne na názvech s mezerami a speciálních znacích. Pozor:wc -l < "$f"(přesměrování) dá jen číslo bez názvu souboru navíc. - Bezpečnostní pás — s
set -euo pipefailskript při použití nedefinované proměnné (překlep) okamžitě spadne s hláškouunbound variable— chybu odhalíš hned. Bez-uby bash překlep tiše nahradil prázdným řetězcem a pracoval by s prázdnem (např. smazal/zkopíroval špatnou věc). Pozor: právě proto-upatří do každého vážného skriptu.
🧠 Otázky & odpovědi
Proč musím proměnné dávat do dvojitých uvozovek?
Protože bez uvozovek bash proměnnou rozdělí podle mezer (tzv. word splitting) a rozbalí v ní
zástupné znaky. Jakmile je v hodnotě mezera — třeba název souboru muj dokument.txt — rm $soubor
se rozpadne na rm muj dokument.txt a maže dva špatné soubory. A když je proměnná prázdná, úplně
zmizí a příkaz dostane jiné argumenty, než čekáš. Uvozené "$soubor" projde vždy jako jedna
hodnota. Pravidlo: každá proměnná patří do uvozovek, dokud nemáš výslovný důvod je vynechat.
K čemu je set -euo pipefail a proč ho psát do každého skriptu?
Ve výchozím stavu bash po selhání příkazu běží dál, jako by se nic nestalo — a napáchá škody
naslepo. set -e ho zastaví hned při první chybě. set -u shodí skript při použití neexistující
proměnné (chytí překlepy v názvech, které by jinak tiše nahradilo prázdno). set -o pipefail zajistí,
že roura selže, i když selže její prostřední článek, ne jen poslední. Dohromady je to bezpečnostní
pás: nepřidává funkce, jen tě zastaví dřív, než se malér rozroste. Patří na druhý řádek pod shebang.
Jaký je rozdíl mezi hranatými závorkami a dvojitými hranatými?
[ ] je starý POSIX test — technicky samostatný příkaz, který je přísný na uvozování a na prázdných
proměnných se rozpadne se syntaktickou chybou. [[ ]] je novější bashová vestavěná konstrukce:
nerozbije se na prázdných proměnných, umí logické && a || uvnitř a porovnávání vzorů. Pro bash
skripty používej [[ ]] — je bezpečnější a čitelnější. A pamatuj: == porovnává text, -eq čísla.
Proč se nemá parsovat výstup příkazu ls v cyklu?
Protože ls vypisuje jména oddělená konci řádků, ale bash je pak přes word splitting seká podle
mezer — takže for f in $(ls) se rozbije na každém názvu, který obsahuje mezeru, a splete se
i na speciálních znacích. Místo toho použij globy, které bash rozbaluje sám a bezpečně:
for f in *.txt. Když potřebuješ rekurzivně nebo složitěji, sáhni po find ... -print0 ve dvojici
s while read -r -d ''. Zkratka: na soubory glob nebo find, nikdy ne parsování ls.
Co se stane při rm -rf $DIR když je proměnná prázdná?
Katastrofa. rm -rf "$DIR/" se při prázdném $DIR rozvine na rm -rf / — tedy pokus smazat celý
systém. Prázdná nebo nenastavená proměnná je v mazacích příkazech časovaná bomba. Chraň se několika
způsoby: set -u shodí skript při nenastavené proměnné; zápis "${DIR:?DIR musí být nastaveno}"
vyhodí chybu, když je prázdná; a před mazáním přidej explicitní if [[ -n "$DIR" ]]. U ničivých
příkazů se vždy vyplatí být paranoidní — jeden překlep se tu neodpouští.
