Bash scripting

proměnné, podmínky, cykly, funkce, set -e.

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.txtsoubor.txt je $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 cron nebo systemd 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:

TestPravda 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, $@.
  • local drží 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 echo a 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š $adresar a 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 toho false | echo ahoj „projde" jako úspěch, protože poslední echo uspěl.

💡 set -euo pipefail je 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 $soubor místo rm "$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 = Karel není přiřazení — bash to bere jako spuštění příkazu name. Přiřazení je name=Karel, bez mezer.
  • Spoléhání na návratový kód bez set -e. Bez set -euo pipefail skript po selhání příkazu běží klidně dál a napáchá škody „naslepo". Napiš ho na druhý řádek.
  • rm -rf "$DIR/" když je $DIR prázdné. Rozvine se na rm -rf /maže celý systém. Prázdná/nenastavená proměnná je bomba. Chraň se set -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) nebo find ... -print0 s while 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í

  1. 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?
  2. Proč uvozovat. Vytvoř proměnnou x="a b c" a zkus echo $x vs echo "$x". Pak si zkus, co udělá touch $x vs touch "$x" (v prázdné složce, pak ls). Vysvětli rozdíl vlastními slovy.
  3. Skript s argumentem a kontrolou. Napiš skript, který dostane jako $1 název souboru a vypíše „existuje" / „neexistuje" podle [[ -f ... ]]. Když $1 chybí, ať skončí s exit 1 a nápovědou.
  4. Cyklus přes soubory. Napiš skript, který projde všechny *.txt v aktuální složce a u každého vypíše jeho jméno a počet řádků (wc -l). Proč použít glob *.txt a ne $(ls *.txt)?
  5. Bezpečnostní pás. Vezmi kterýkoli předchozí skript, přidej set -euo pipefail a 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
  1. První skript — obsah: #!/usr/bin/env bash a echo "Ahoj, dnes je $(date)". Pak chmod +x ahoj.sh (spustitelný bit) a ./ahoj.sh (spuštění z aktuální složky). Potřeboval jsi tedy chmod (práva) a ./ prefix (aby shell soubor našel). Pozor: bez chmod +xPermission denied, bez ./command not found.
  2. Proč uvozovatecho $x vypíše a b c (bash rozdělí na tři slova a echo je spojí mezerami — vypadá stejně, ale prošla tři slova), echo "$x" pošle jedno slovo. U touch $x vzniknou tři soubory a, b, c; touch "$x" vytvoří jeden soubor jménem a b c. Rozdíl: neuvozená proměnná se rozseká podle mezer (word splitting). Pozor: přesně tohle rozbíjí rm, cp, mv s reálnými názvy souborů.
  3. Argument + kontrola — kostra: if [[ -z "$1" ]]; then echo "Použití: $0 <soubor>" >&2; exit 1; fi a pak if [[ -f "$1" ]]; then echo "existuje"; else echo "neexistuje"; fi. -z "$1" chytí chybějící argument, -f testuje běžný soubor. Pozor: $1 uvozuj ("$1"), jinak prázdný argument test rozbije.
  4. Cyklus přes souboryfor f in *.txt; do echo "$f: $(wc -l < "$f") řádků"; done. Glob *.txt rozbalí 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.
  5. Bezpečnostní pás — s set -euo pipefail skript při použití nedefinované proměnné (překlep) okamžitě spadne s hláškou unbound variable — chybu odhalíš hned. Bez -u by 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 -u patří 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.txtrm $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í.