En tancar 05-02 va quedar identificat el forat que encara té informe-diari.sh: si alguna cosa falla a dins, l'script no se n'assabenta. Bash, per disseny, executa la línia següent passi el que passi amb l'anterior. Si el grep sobre el CSV no troba res, si una variable està mal escrita i s'expandeix a la cadena buida, si el disc s'omple a mitja escriptura, l'script tira endavant i produeix un informe silenciosament fals —el pitjor que pot fer una eina d'operacions, perquè ningú no sospita d'un número equivocat que arriba puntual cada matí—. I a més deixa enrere el temporal de mktemp que a 05-01 vam prometre aprendre a netejar. Aquesta lliçó dona a Bash la disciplina que no porta de sèrie, i et dona a tu les eines per veure què està passant quan alguna cosa no quadra.

Contingut

  1. La fallada silenciosa
  2. set -e: què detecta i què no
  3. set -u: variables no definides
  4. set -o pipefail i PIPESTATUS
  5. set -euo pipefail i la seva crítica honesta
  6. trap: senyals i pseudosenyals
  7. Neteja garantida amb EXIT
  8. morir() i trap ... ERR amb context
  9. Conveni de codis de sortida
  10. Reintents amb retrocés
  11. Depuració: -n, -x, PS4, -v, DEBUG i bisecció
  12. informe-diari.sh endurit

  1. La fallada silenciosa

#!/usr/bin/env bash
cd /srv/veloz/dades/no-existeix    # falla: "No such file or directory"
rm -rf ./*                         # s'executa IGUALMENT, al directori actual

Aquest exemple, amb variants, ha destruït servidors de debò. Bash reporta l'error a stderr i continua. Contra això hi ha tres interruptors.

  1. set -e: què detecta i què no

set -e (o set -o errexit) fa que l'script acabi tan bon punt una ordre retorni un codi diferent de zero: amb ell al principi, l'exemple anterior mor al cd i no arriba mai al rm. L'important és saber què no detecta, perquè les excepcions són moltes i deliberades: una ordre la fallada de la qual forma part d'una decisió no ha de matar l'script.

Situació set -e avorta? Per què
grep patro f sense coincidències Retorna 1 i no està protegit
if grep -q patro f; then No És la condició d'un if
while ! ping -c1 host; do No És la condició d'un bucle
grep -q x f && echo trobat No Tret que falli l'últim de la cadena
`ordre true`
! ordre No Està negada
f() { fals; }; if f; then No La funció sencera perd el -e dins d'un if
fals | wc -l No Només compta l'últim de la canonada (secció 4)
(( comptador++ )) amb comptador a 0 Sorpresa clàssica, ja vista a 04-06
local x=$(fals) No El codi és el de local, no el de la substitució

Les dues últimes files són les que donen més maldecaps. La de local té solució: declara i assigna en línies separades (local data i després data=$(date -d "$1" +%F)), i així la fallada sí que es detecta. I la penúltima —una funció perd set -e al seu interior quan es crida des d'un if o des d'un &&— és la raó principal per la qual hi ha qui desconfia de set -e. No és un error de Bash: és POSIX. Però significa que no pots delegar tota la teva gestió d'errors en -e.

  1. set -u: variables no definides

Sota set -u, un echo "Informe de $CIUTATT" amb la variable mal escrita avorta amb bash: CIUTATT: variable sense assignar. Sense -u, aquesta errada s'expandeix a la cadena buida i produeix rm -rf "$DIR_BASE/" amb DIR_BASE buida, és a dir rm -rf /. Conviu perfectament amb els valors per defecte de 03-06, que són la manera explícita de dir «aquesta pot no existir»:

nivell="${VELOZ_LOG_NIVELL:-info}"    # per defecte si no està definida
desti="${1:?falta el desti}"          # error propi amb missatge clar
[[ -n "${DEBUG:-}" ]] && set -x       # el :- és obligatori sota set -u

Compte amb els arrays: sota -u i en Bash anterior a 4.4, "${arr[@]}" sobre un array buit avorta. L'idioma segur és "${arr[@]:-}" o comprovar abans amb (( ${#arr[@]} > 0 )).

  1. set -o pipefail i PIPESTATUS

A 02-04 va quedar ajornat això: el codi de sortida d'una canonada és el de la seva última ordre.

zcat /var/log/veloz/no-existeix.gz | wc -l
echo $?     # 0  ← èxit! perquè wc -l va funcionar perfectament (va imprimir 0)
set -o pipefail
zcat /var/log/veloz/no-existeix.gz | wc -l
echo $?     # 1  ← ara sí

Una fallada així a informe-diari.sh produeix «0 errors avui» quan la veritat és «no he pogut llegir el fitxer». pipefail ho arregla: la canonada retorna el codi de l'última ordre que ha fallat, o 0 si no n'ha fallat cap. Quan necessites saber quin dels graons ha fallat, l'array PIPESTATUS desa tots els codis, en ordre:

zcat acces.log.2.gz | grep ' 500 ' | wc -l
echo "${PIPESTATUS[@]}"     # 0 1 0  ← el grep no ha trobat res (codi 1)

PIPESTATUS es sobreescriu amb cada ordre, inclòs un echo. Si el faràs servir, copia'l primer: local -a codis=( "${PIPESTATUS[@]}" ).

  1. set -euo pipefail i la seva crítica honesta

La capçalera que veuràs en gairebé tot script seriós és set -euo pipefail just sota el shebang, de vegades acompanyada d'un IFS=$'\n\t' restrictiu (03-06). És un bon valor per defecte i l'hauries de fer servir. Però convé dir amb claredat per què no és màgia:

  • set -e té el forat de les funcions cridades des d'if, així que les funcions crítiques han de comprovar els seus propis errors.
  • Avorta amb un missatge de l'ordre que ha fallat, sense dir en quina línia del teu script ha passat. Això ho arregla el trap ... ERR de la secció 8.
  • Converteix qualsevol codi diferent de zero en fatal, i hi ha ordres on això és normal: grep sense coincidències, diff amb diferències, pgrep sense processos. Cal marcar-les explícitament.

Per a aquests casos, tres idiomes:

grep -c ERROR "$LOG" || true            # "pot retornar 1, és acceptable"
if ! errors=$(grep -c ERROR "$LOG"); then errors=0; fi   # millor: distingeix el cas
set +e; ordre_que_falla_sovint; codi=$?; set -e          # desactivar puntualment

La regla: || true per al cas trivial, if ! quan vols reaccionar, i set +e/set -e acotat a les mínimes línies possibles. Un set +e al principi d'un script llarg és pitjor que no haver posat mai -e, perquè dona falsa sensació de seguretat.

  1. trap: senyals i pseudosenyals

trap 'ordres' SENYAL [SENYAL...] registra codi que s'executa quan arriba un senyal. Les seves variants: trap - SENYAL restaura el comportament per defecte, trap '' SENYAL l'ignora i trap -p llista les trampes actives.

A més dels senyals reals de 05-02, Bash defineix quatre pseudosenyals:

Pseudosenyal Es dispara Ús típic
EXIT En sortir l'script, per la via que sigui Neteja garantida
ERR Quan una ordre retorna codi ≠ 0 (mateixes excepcions que -e) Informar d'on ha fallat
DEBUG Abans de cada ordre Depuració fina
RETURN En tornar d'una funció o d'un source Perfilat, traces

Fes servir cometes simples en el codi de la trampa. trap 'rm -f "$tmp"' EXIT és correcte perquè $tmp es resol en disparar-se; amb cometes dobles s'expandiria en registrar la trampa i capturaries el valor equivocat.

  1. Neteja garantida amb EXIT

Aquí es tanca el que vam prometre a 05-01 i 05-02:

readonly TMPDIR_INFORME=$(mktemp -d)
netejar() {
    local codi=$?                       # capturar ABANS de fer res
    rm -rf "$TMPDIR_INFORME"
    exit "$codi"                        # preservar el codi original
}
trap netejar EXIT

EXIT es dispara amb exit, en acabar l'última línia, quan set -e avorta i quan arriba un senyal capturat. Dos detalls que la gent oblida: el local codi=$? ha de ser la primera línia de la funció, perquè qualsevol ordre anterior el sobreescriu; i l'exit "$codi" final evita que la neteja converteixi una fallada en un èxit aparent. Perquè Ctrl-C també hi passi, afegeix els senyals: trap netejar EXIT INT TERM HUP.

  1. morir() i trap ... ERR amb context

Tot script seriós té una funció per avortar amb missatge. La convenció: missatge a stderr, codi de sortida significatiu.

# morir — Escriu un missatge d'error i acaba. Ús: morir <codi> <missatge...>
morir() {
    local codi="${1:?}"; shift
    printf '%s [ERROR] %s\n' "$(date '+%F %T')" "$*" >&2
    exit "$codi"
}
validar_entorn() {
    [[ -r "$CAMI_CSV" ]] || morir 66 "No puc llegir $CAMI_CSV"
    command -v bc > /dev/null || morir 69 "Falta la utilitat bc"
}

I per a les fallades que no vas preveure, una trampa ERR que digui exactament on ha passat:

trap 'printf "[FATAL] %s:%d a %s(): codi %d\n" "${BASH_SOURCE[0]}" \
      "$LINENO" "${FUNCNAME[0]:-main}" "$?" >&2' ERR

Les tres variables que donen el context són les que converteixen un missatge inútil en un diagnòstic:

Variable Contingut
$LINENO Número de línia actual
${BASH_SOURCE[0]} Fitxer on és aquesta línia (clau amb llibreries, 05-06)
${FUNCNAME[0]} Funció en curs; l'array complet és la pila de crides

FUNCNAME és un array: ${FUNCNAME[1]} és qui ha cridat la funció actual, i recorrent-lo juntament amb BASH_LINENO s'imprimeix una traça de pila completa. Això sí: perquè la trampa ERR s'hereti dins de funcions i subshells cal set -E (o set -o errtrace); sense ell, les funcions no la disparen.

  1. Conveni de codis de sortida

A 03-01 vas veure exit N. Ara el conveni, que importa quan un altre script o cron llegeix el teu resultat:

Codi Significat
0 Èxit
1 Error genèric
2 Ús incorrecte: falten arguments o opció desconeguda
64 EX_USAGE: error a la línia d'ordres
65 EX_DATAERR: les dades d'entrada són incorrectes (CSV corromput)
66 EX_NOINPUT: el fitxer d'entrada no existeix o no es pot llegir
69 EX_UNAVAILABLE: un servei o dependència no està disponible
73 EX_CANTCREAT: no es pot crear el fitxer de sortida
78 EX_CONFIG: error de configuració
124 / 126 timeout ha expirat (05-02) / el fitxer existeix però no és executable
127 / 130 Ordre no trobada (no és al PATH) / Ctrl-C (128 + 2, SIGINT)

Els del rang 64-78 vénen de sysexits.h de BSD; no tothom els fa servir, però són millor conveni que inventar números. I la regla 128 + N per als senyals explica d'un cop d'ull el 137 (128+9, SIGKILL) i el 143 (128+15, SIGTERM). Reserva els codis per sobre de 125 i no facis servir valors més grans de 255: es trunquen mòdul 256, així que exit 256 és exit 0.

  1. Reintents amb retrocés

Les fallades de xarxa són transitòries per naturalesa: reintentar és correcte, però reintentar immediatament i sense límit és un atac contra el teu propi servidor. El patró és retrocés exponencial:

# reintentar — Executa una ordre amb espera creixent. Ús: reintentar <n> <ordre...>
reintentar() {
    local intents="${1:?}" n=1 espera=1; shift
    until "$@"; do
        (( n >= intents )) && { printf 'Fallada després de %d intents\n' "$n" >&2; return 1; }
        sleep "$espera"; (( espera *= 2 )); (( ++n ))
    done
}
reintentar 5 timeout 10 curl -sf http://localhost:8080/envios

Les esperes són 1, 2, 4, 8 segons. Fixa't en la combinació amb timeout de 05-02: sense ell, un intent penjat impedeix arribar al següent. L'ús real amb curl i APIs arriba a 06-04 i 06-05.

  1. Depuració: -n, -x, PS4, -v, DEBUG i bisecció

bash -n informe-diari.sh       # només comprova la SINTAXI, no executa res
bash -x informe-diari.sh       # traça: imprimeix cada ordre ja expandida
bash -v informe-diari.sh       # imprimeix cada línia tal qual, abans d'expandir

bash -n hauria de ser un reflex abans de desar qualsevol script que vagi a cron: detecta el fi que falta sense executar el rm. I set -x / set +x acoten la traça a la part sospitosa, que és el que la fa usable en un script llarg.

La traça surt a stderr precedida d'un +. Aquest prefix és la variable PS4, i enriquir-la converteix una traça il·legible en un diagnòstic:

export PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}(): '
exec 8> ~/veloz-ops/logs/traca.$$
export BASH_XTRACEFD=8       # la traça va al descriptor 8, no a stderr
set -x
acumular_dia "$data"
set +x

La traça resultant té aquesta forma —+ informe-diari.sh:91:acumular_dia(): [[ 2026-08-03 == 2026-08-03 ]]—: cada línia diu fitxer, línia i funció. I BASH_XTRACEFD la desvia a un fitxer perquè no es barregi amb la sortida de l'informe (el descriptor 8 és de la família que veuràs a fons a 05-05). Per la seva banda, trap 'printf "linia %d: total=%s\n" "$LINENO" "${total:-}" >&2' DEBUG inspecciona l'estat abans de cada ordre, útil per caçar on canvia una variable.

Quan res d'això no n'hi ha prou, queda la bisecció: col·locar exit 99 a mitja passada de l'script, veure si el problema apareix, moure l'exit a la meitat de la meitat que correspongui i repetir. Amb cinc o sis execucions localitzes la línia culpable d'un script de mil. I per prevenir en comptes de curar, shellcheck detecta estàticament la majoria d'aquestes fallades abans d'executar res: és el tema de 08-05.

  1. informe-diari.sh endurit

#!/usr/bin/env bash
# informe-diari.sh — Informe diari de repartiment de Veloz Envíos
set -Eeuo pipefail                    # -E: les funcions hereten el trap ERR
readonly CAMI_CSV="${VELOZ_CSV:-/srv/veloz/dades/enviaments.csv}"
TMPDIR_INFORME=$(mktemp -d) || exit 73; readonly TMPDIR_INFORME

netejar() { local c=$?; rm -rf "$TMPDIR_INFORME"; exit "$c"; }
trap netejar EXIT INT TERM
trap 'printf "[FATAL] %s:%d a %s(): codi %d\n" "${BASH_SOURCE[0]}" \
      "$LINENO" "${FUNCNAME[0]:-main}" "$?" >&2' ERR

main() {
    [[ "${1:-}" == --debug ]] && { shift; activar_debug; }
    validar_entorn
    ...
}
main "$@"

Quatre línies de capçalera i dues trampes: l'script ja no menteix. Si el CSV no es pot llegir, mor amb 66 i un missatge; si alguna cosa inesperada falla, diu fitxer, línia i funció; i el temporal s'esborra per les quatre vies possibles de sortida.

Errors Habituals i Consells

  • Creure que set -e ho cobreix tot. Les funcions cridades des d'if el perden. Comprova també a mà.
  • trap amb cometes dobles. Les variables s'expandeixen en registrar la trampa, no en disparar-la.
  • No capturar $? a la primera línia del gestor EXIT, o oblidar l'exit "$codi" final. Una fallada es converteix en èxit aparent i cron no avisa.
  • trap ... ERR sense set -E. Les funcions no el disparen i el diagnòstic no apareix mai.
  • Fer servir PIPESTATUS després d'una altra ordre. Se sobreescriu amb cadascuna; copia'l immediatament.
  • exit 300. Es trunca mòdul 256 i surt amb 44. Mantén-te entre 0 i 125.
  • Consell: escriu la trampa de neteja a la mateixa línia en què crees el temporal. Si ho deixes per «després», el return primerenc que afegeixis d'aquí a un mes s'endurà l'esborrat per davant.

Exercicis

Exercici 1. Escriu una capçalera completa per a un script arxivar-historic.sh que faci servir mode estricte, creï un directori temporal, garanteixi el seu esborrat també davant d'un Ctrl-C, preservi el codi de sortida original i informi de la línia exacta davant de qualsevol fallada imprevista.

Exercici 2. La funció comptar_errors() fa servir zgrep -c ERROR "$LOG" | tr -d ' '. Explica per què sota set -e sense pipefail pot retornar un número fals, i reescriu-la perquè distingeixi «zero errors» de «no he pogut llegir el fitxer», sortint amb 66 en el segon cas.

Exercici 3. Afegeix a informe-diari.sh una opció --debug que activi una traça enriquida (fitxer, línia i funció) abocada a ~/veloz-ops/logs/traca-AAAA-MM-DD.log, sense embrutar ni la sortida de l'informe ni els seus errors.

Solucions

Solució 1.

#!/usr/bin/env bash
set -Eeuo pipefail
TREBALL=$(mktemp -d) || exit 73
readonly TREBALL
netejar() { local c=$?; rm -rf "$TREBALL"; exit "$c"; }
trap netejar EXIT INT TERM HUP
trap 'printf "[FATAL] %s:%d %s(): codi %d\n" "${BASH_SOURCE[0]}" "$LINENO" \
      "${FUNCNAME[0]:-main}" "$?" >&2' ERR

-E és imprescindible perquè la trampa ERR funcioni dins de funcions. INT TERM HUP al costat d'EXIT pot semblar redundant —un senyal capturat acaba disparant EXIT—, però explicitar-los garanteix l'esborrat també si algú redefineix el comportament per defecte.

Solució 2. Sense pipefail, el codi de la canonada és el de tr, que sempre val 0: si zgrep no ha pogut obrir el fitxer, la funció retorna la cadena buida com si fossin zero errors. A més, zgrep -c retorna 1 quan hi ha zero coincidències, cosa que sota set -e mataria l'script sense ser un error real.

comptar_errors() {                 # Ús: comptar_errors <fitxer>
    local log="${1:?}" n
    [[ -r "$log" ]] || morir 66 "No puc llegir $log"
    n=$(grep -c 'ERROR' "$log") || n=0    # el || captura el "sense coincidències"
    printf '%s\n' "$n"
}

La guarda [[ -r ]] separa la fallada real (fitxer il·legible → codi 66) del cas legítim (zero errors → imprimeix 0). És més clar que qualsevol acrobàcia amb PIPESTATUS, i la lliçó és general: comprova les precondicions tu mateix en comptes de deduir-les de codis ambigus.

Solució 3.

activar_debug() {
    local traca=~/veloz-ops/logs/traca-$(date +%F).log
    exec 8>> "$traca"                     # descriptor propi, en mode afegir
    export BASH_XTRACEFD=8
    export PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}(): '
    set -x
    log_info "Traça activada a $traca"
}
[[ "${1:-}" == --debug ]] && { shift; activar_debug; }

BASH_XTRACEFD=8 desvia la traça al descriptor 8, així que ni stdout (l'informe) ni stderr (els avisos) no es contaminen: pots seguir canalitzant l'informe cap a un fitxer mentre llegeixes la traça en un altre terminal amb tail -f. El >> conserva les traces de diverses execucions del mateix dia.

Conclusió

Bash no avorta pel seu compte, així que la robustesa es demana explícitament. set -e mata l'script a la primera fallada, amb excepcions deliberades —condicionals, &&/||, negacions, funcions cridades des d'if— que has de conèixer per no confiar-t'hi; set -u converteix una errada en un error visible en comptes d'en una cadena buida perillosa; set -o pipefail arregla el codi de sortida de les canonades, i PIPESTATUS diu quin graó ha fallat. La línia set -Eeuo pipefail és el punt de partida correcte, però no una garantia: cal marcar amb || true o if ! les ordres la fallada de les quals és normal. trap completa el quadre: EXIT dona la neteja garantida que fa segura la parella mktemp + esborrat, i ERR amb $LINENO, BASH_SOURCE i FUNCNAME converteix un error mut en un diagnòstic amb fitxer, línia i funció. Una morir() amb codis del conveni sysexits i un patró de reintents amb retrocés completen l'arsenal. I per investigar: bash -n valida sintaxi, set -x amb un PS4 enriquit i BASH_XTRACEFD produeix una traça llegible i aïllada, i la bisecció amb exit temporal localitza el que se't resisteixi.

Queda un deute pendent des del Mòdul 2. El toolkit valida rutes, processos i codis, però quan ha de validar text —una data --data 2026-08-03, una IP, un codi HTTP, l'estructura d'una línia d'app.log— segueix recorrent a comparacions fràgils amb globs. A 05-04 arriben per fi les expressions regulars de debò: els tres sabors i quina eina fa servir cadascun, la sintaxi des de zero, i l'operador =~ de Bash amb BASH_REMATCH, que valida i extreu en un sol pas.

Curs de Programació en Bash

Mòdul 1: Introducció a Bash

Mòdul 2: Ordres Bàsiques de Bash

Mòdul 3: Fonaments de Scripting

Mòdul 4: Scripting Intermedi

Mòdul 5: Tècniques Avançades de Scripting

Mòdul 6: Treballar amb Eines Externes

Mòdul 7: Automatització i Programació

Mòdul 8: Bones Pràctiques i Optimització

Mòdul 9: Projectes del Món Real

© Copyright 2026. Tots els drets reservats