Modul 4 — Skriptování a automatizace

17. Pokročilé Bash skriptování a error handling

Funkce v Bashi, bezpečné zpracování chyb pomocí "set -euo pipefail", trap pro úklid a práce s poli a asociativními poli.

Odhadovaná délka studia: 55 minut · Stav: nehotovo

Technologie: Bash

Úvod a kontext

Předchozí lekce ukázala základní stavební prvky Bash skriptů. Skripty
používané v provozu (deploy skripty, zálohovací úlohy, automatizace
spouštěná z CI pipeline) ale musí být odolnější — musí se bezpečně
chovat, když něco selže uprostřed běhu, musí umět uklidit po sobě i
v případě chyby a musí jít snadno testovat a udržovat. Tato lekce
navazuje na základy a ukazuje techniky, které oddělují "skript, co
funguje, když všechno jde dobře" od skriptu bezpečného pro produkční
použití.

Teorie

Výchozí chování Bashe je nebezpečné

Bez dalšího nastavení Bash ve výchozím stavu:

  • pokračuje v běhu i po selhání příkazu (chyba se jen tiše ztratí,
    pokud výsledek nikdo nekontroluje),
  • nehlásí chybu při použití nedefinované proměnné (typicky kvůli
    překlepu v názvu),
  • v rouře (pipe) bere jako výsledný návratový kód jen kód
    posledního příkazu, i když ten uprostřed selhal.

To vede k tichým chybám — skript "doběhne", ale s neúplným nebo
špatným výsledkem. Proto se v produkčních skriptech doporučuje hned
na začátek přidat:

set -euo pipefail
  • -e — skript se okamžitě ukončí, jakmile libovolný příkaz
    vrátí nenulový návratový kód (s několika výjimkami, např. uvnitř
    podmínky if).
  • -u — použití nedefinované proměnné je chyba, skript se
    ukončí, místo aby ji tiše nahradil prázdným řetězcem.
  • -o pipefail — návratový kód roury (příkaz1 | příkaz2) je
    nenulový, pokud selže kterýkoliv příkaz v ní, ne jen poslední.

Funkce

Funkce umožňují rozdělit skript na pojmenované, znovupoužitelné
bloky. Definují se jednoduše, bez klíčového slova pro parametry —
uvnitř se k argumentům funkce přistupuje stejně jako k argumentům
skriptu ($1, $2, $#):

zaloguj() {
    echo "[$(date +%H:%M:%S)] $1"
}

Funkce může vracet číselný stav přes return N (jde do $?, stejně
jako u skriptu) nebo "vracet" textový výstup přes echo, který
volající zachytí substitucí $(funkce).

trap — reakce na signály a úklid

trap zaregistruje příkaz nebo funkci, která se spustí při zachycení
signálu nebo při ukončení skriptu — bez ohledu na to, jestli skript
skončil úspěchem, chybou, nebo byl přerušen (Ctrl+C). Nejčastější
využití je úklid dočasných souborů, i když skript selže uprostřed:

docasny_soubor=$(mktemp)
trap 'rm -f "$docasny_soubor"' EXIT

EXIT je pseudo-signál, který Bash spustí vždy při ukončení skriptu
(ať už normálním, nebo kvůli set -e). Díky tomu se dočasný soubor
smaže i v případě, že skript spadne na chybě uprostřed zpracování.

Kontrola chyb u konkrétních příkazů

I se zapnutým set -e je někdy žádoucí selhání jednoho konkrétního
příkazu ošetřit ručně (např. abyste vypsali smysluplnější chybovou
hlášku), místo aby skript rovnou spadl:

if ! curl -sf https://api.example.com/health > /tmp/health.json; then
    echo "Chyba: healthcheck endpoint neodpověděl." >&2
    exit 1
fi

Přesměrování >&2 posílá chybovou hlášku na standardní chybový výstup
(stderr), ne na standardní výstup (stdout) — důležitá konvence, díky
které lze skutečný výstup skriptu (stdout) přesměrovat do souboru
odděleně od chybových hlášek.

Pole a asociativní pole

Kromě běžných (indexovaných) polí z předchozí lekce podporuje Bash i
asociativní pole (klíč–hodnota), užitečná např. pro mapování
prostředí na konfiguraci:

declare -A porty
porty[web]=80
porty[api]=8080
porty[db]=5432

for sluzba in "${!porty[@]}"; do
    echo "$sluzba naslouchá na portu ${porty[$sluzba]}"
done

"${!porty[@]}" vrátí seznam klíčů asociativního pole.

Praktický příklad

Robustnější verze zálohovacího skriptu s set -euo pipefail,
funkcemi a úklidem přes trap:

#!/bin/bash
set -euo pipefail

ZDROJ="/var/www/data"
CIL="/backup"

zaloguj() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

uklid() {
    local kod=$?
    if [ -n "${docasny_archiv:-}" ] && [ -f "$docasny_archiv" ]; then
        rm -f "$docasny_archiv"
        zaloguj "Dočasný soubor smazán (úklid při ukončení)."
    fi
    if [ "$kod" -ne 0 ]; then
        zaloguj "Skript skončil s chybou (kód $kod)."
    fi
    exit "$kod"
}
trap uklid EXIT

if [ ! -d "$ZDROJ" ]; then
    zaloguj "Chyba: zdrojový adresář $ZDROJ neexistuje." >&2
    exit 1
fi

docasny_archiv=$(mktemp /tmp/zaloha-XXXXXX.tar.gz)
zaloguj "Vytvářím archiv $docasny_archiv..."
tar -czf "$docasny_archiv" -C "$ZDROJ" .

zaloguj "Přesouvám archiv do $CIL..."
mv "$docasny_archiv" "$CIL/zaloha-$(date +%Y%m%d-%H%M%S).tar.gz"

zaloguj "Záloha dokončena úspěšně."

Klíčové body v tomto skriptu:

  • set -euo pipefail zajistí, že selhání tar nebo mv skript
    okamžitě ukončí, místo aby pokračoval s neúplnou zálohou.
  • trap uklid EXIT zaručí, že se docasny_archiv uklidí (pokud ještě
    existuje) i při chybě uprostřed běhu, a zaloguje výsledný stav.
  • local kod=$? uvnitř funkce zachytí návratový kód posledního
    příkazu předtím, než ho samotné příkazy uvnitř funkce (rm, if)
    přepíší — proto se čte hned na prvním řádku funkce uklid.
  • "${docasny_archiv:-}" je bezpečný zápis i se zapnutým set -u
    pokud proměnná ještě není nastavená (skript spadl dřív, než se
    vytvořila), :- dosadí prázdný řetězec místo chyby.

Shrnutí

Produkční Bash skripty by měly začínat set -euo pipefail, aby
tiché chyby (selhání příkazu, nedefinovaná proměnná, selhání uprostřed
roury) vedly k okamžitému a viditelnému ukončení místo pokračování s
poškozeným stavem. Funkce umožňují rozdělit logiku na znovupoužitelné
bloky, trap ... EXIT zajišťuje spolehlivý úklid (dočasné soubory,
logování výsledku) bez ohledu na to, jak skript skončil, a
asociativní pole (declare -A) umožňují přehledně pracovat s
mapováním klíč–hodnota přímo v shellu.

Kontrolní otázky

  1. Co konkrétně dělají přepínače -e, -u a -o pipefail v příkazu
    set -euo pipefail?
  2. Proč se trap ... EXIT typicky používá pro úklid dočasných
    souborů, a ne jen úklidový kód na konec skriptu?
  3. K čemu slouží přesměrování >&2 a proč je důležité oddělovat
    chybové hlášky od běžného výstupu skriptu?
  4. Upravte ukázkový zálohovací skript tak, aby při chybějícím
    cílovém adresáři $CIL (nikoliv zdrojovém) vypsal srozumitelnou
    chybu na stderr a skončil s návratovým kódem 1 ještě předtím, než
    se vytvoří dočasný archiv.

Lekce na sebe nejsou zamčené — libovolnou lekci můžete otevřít i označit jako hotovou v jakémkoliv pořadí.