Portes cinc lliçons escrivint set -euo pipefail sense saber què fa. Has posat dues vegades un return 0 al final de log() «perquè si no, l'script mor». I informe_reserves.sh crea el directori de l'informe i, si alguna cosa falla a mig escriure'l, deixa un fitxer incomplet que demà algú llegirà com si fos bo. Tot això s'arregla avui, i convé entendre per què importa tant: un script que es trenca aviat i fa soroll és infinitament millor que un que falla a mitges en silenci. El primer et desperta a les quatre de la matinada amb un missatge clar; el segon et deixa una còpia truncada que descobriràs el dia que la necessitis. Aquesta lliçó va d'aconseguir el primer: veure què fa el teu script, fer que s'aturi quan cal i garantir que deixa les coses netes passi el que passi. Al final escriurem per fi copia_tramontana.sh.

Contingut

  1. Depuració: veure què fa realment l'script
  2. shellcheck seriosament
  3. El mode estricte esmicolat
  4. On set -e no et salva
  5. trap i els pseudosenyals
  6. Neteja garantida i Ctrl+C
  7. Un gestor ERR que serveixi d'alguna cosa
  8. Idempotència
  9. Reintents, temporals segurs i bloqueig
  10. Aplicació: copia_tramontana.sh

  1. Depuració: veure què fa realment l'script

Eina Què fa
bash -n script.sh Comprova la sintaxi sense executar res
bash -x script.sh Traça cada ordre ja expandida, a stderr
set -x / set +x / PS4 Activa i desactiva la traça per zones / dona format al prefix
set -v Mostra les línies abans d'expandir-les

bash -x és l'eina principal, i la seva sortida per defecte (+ ordre) és poc útil tan bon punt hi ha funcions. Amb un PS4 decent canvia completament:

operador@srv-tramontana:~$ PS4='+ ${BASH_SOURCE##*/}:${LINENO}:${FUNCNAME[0]:-main}: ' \
>     bash -x ~/scripts/purgar_releases.sh -n 2>&1 | head -5
+ purgar_releases.sh:12:main: DRY_RUN=0
+ purgar_releases.sh:20:main: readlink -f /opt/tramontana/app
+ purgar_releases.sh:20:main: basename /opt/tramontana/releases/3.2.1
+ purgar_releases.sh:20:main: actiu=3.2.1
+ purgar_releases.sh:24:main: llista+=(3.1.0)

Cada línia diu ara fitxer, número de línia i funció, i mostra l'ordre després d'expandir variables: veus basename /opt/tramontana/releases/3.2.1, no basename "$(readlink -f "$BASE/app")". Aquella diferència és justament el que busques quan una variable no val el que et pensaves. Traçar un script sencer genera centenars de línies, així que per acotar envolta només la zona sospitosa amb set -x … set +x. Exporta a més el PS4 al teu ~/.bashrc per no escriure'l cada vegada. set -v es fa servir molt menys, però és el complement de -x quan el problema és a l'expansió mateixa: -v ensenya el que vas escriure i -x el que Bash va entendre.

  1. shellcheck seriosament

Ja el vas instal·lar a 04-01. Aquests són els avisos que més veuràs, amb el seu perquè:

Codi Què diu Per què importa de debò
SC2086 / SC2046 «Entrecometa per evitar globbing i divisió de paraules», per a variables i per a $(ordre) rm $f amb f="informe agost.txt" esborra dos fitxers equivocats; amb f="*" ho esborra tot
SC2164 «Fes servir cd ... || exit» Si el cd falla, el que vingui després s'executa al directori equivocat
SC2155 «Declara i assigna per separat» local x=$(cmd) retorna el codi de local, sempre 0: la fallada de cmd es perd

SC2155 mereix demostració, perquè és subtil i apareix a tot arreu:

operador@srv-tramontana:~$ f() { local v=$(false); echo "dins: $?"; }; f
dins: 0
operador@srv-tramontana:~$ g() { local v; v=$(false); echo "dins: $?"; }; g
dins: 1

Al primer l'error de false desapareix i, amb set -e, l'script continua alegrement amb una variable buida; al segon es detecta. La regla: declara primer, assigna després.

Silenciar un avís és legítim quan saps què fas, però sempre amb justificació escrita, i la directiva afecta només la línia següent (si la poses al principi del fitxer silencia l'avís a tot l'script, cosa que gairebé mai no és el que vols):

# Volem que OPCIONS es divideixi en paraules: són opcions separades.
# shellcheck disable=SC2086
rsync $OPCIONS "$origen" "$desti"

  1. El mode estricte esmicolat

set -euo pipefail són tres ajustos independents, i convé saber exactament què fa cadascun.

  • set -e (errexit): si una ordre retorna un codi diferent de 0, l'script acaba immediatament amb aquell codi. Converteix les fallades silencioses en aturades sorolloses.
  • set -u (nounset): fer servir una variable no definida és un error fatal en comptes d'expandir-se a la cadena buida. Evita el clàssic rm -rf "$DIR/" convertit en rm -rf / perquè DIR no existia.
  • set -o pipefail: una canonada retorna el codi de l'últim tram que ha fallat, no el de l'últim tram; sense això, cat inexistent | wc -l retorna 0 i el teu script creu que tot ha anat bé.
operador@srv-tramontana:~$ bash -c 'cat /noexisteix | wc -l; echo "codi: $?"'
cat: /noexisteix: No such file or directory
0
codi: 0
operador@srv-tramontana:~$ bash -c 'set -o pipefail
> cat /noexisteix | wc -l; echo "codi: $?"'
cat: /noexisteix: No such file or directory
0
codi: 1

Sobre set -u, dos detalls pràctics. Amb Bash 4.4 i posteriors —inclòs el 5.2 del teu Ubuntu 24.04— "$@" i "${matriu[@]}" buits ja no disparen l'error, però ${matriu[0]} d'una matriu buida sí. I la sortida d'emergència per a qualsevol variable que legítimament pugui no existir és ${VAR:-}, que ja fas servir des de 04-02: [[ -n ${TRAMONTANA_VERBOSE:-} ]] && echo "mode detallat".

  1. On set -e no et salva

Aquest és el material que gairebé ningú no explica i que fa que la gent confiï de més en el mode estricte. set -e es desactiva en cinc contextos, i en tots ells una fallada passa desapercebuda:

set -e
# 1. A la condició d'un if, while o until: és la seva feina, no una fallada.
if grep -q alguna_cosa fitxer_inexistent; then :; fi  # no avorta (correcte)
# 2. A l'esquerra de && o ||, i en qualsevol llista d'ordres.
false && echo "res"                                   # no avorta
comprovar_alguna_cosa || echo "avisat"                # no avorta
# 3. Amb ! al davant.
! false                                               # no avorta
# 4. DINS d'una funció cridada en una condició: es desactiva sencera.
preparar() { cp /noexisteix /tmp/x; echo "encara soc aquí"; }
if preparar; then :; fi                               # imprimeix "encara soc aquí"
# 5. En assignacions tipus local/declare/export amb substitució d'ordres.
la_meva_fn() { local v=$(false); echo "no avorta"; }

El cas 4 és el més traïdor i mereix veure's funcionant:

operador@srv-tramontana:~$ bash -c 'set -e
> preparar() { cp /noexisteix /tmp/x; echo "CONTINUO EXECUTANT"; return 0; }
> if preparar; then echo "preparat"; fi'
cp: cannot stat '/noexisteix': No such file or directory
CONTINUO EXECUTANT
preparat

cp va fallar, set -e estava actiu, i la funció va continuar fins al final i va declarar èxit, perquè en fer-la servir com a condició Bash desactiva errexit a tot el seu cos. S'arregla comprovant a dins (cp ... || return 1) o no fent servir la funció com a condició: cridar-la solta i deixar que set -e faci la seva feina. La conclusió pràctica: set -e és una xarxa de seguretat, no un pla. Comprova explícitament el que importa —amb || morir, amb if— i deixa que set -e atrapi el que s'escapi. I suma-hi el parany de 04-02: (( i++ )) amb i a 0 retorna 1 i sí que avorta; fes servir (( ++i )) o i=$(( i + 1 )).

  1. trap i els pseudosenyals

trap instal·la un gestor que s'executa en arribar un senyal. La seva sintaxi és trap 'ordres' SENYAL... i, a més dels senyals de 03-06, admet quatre pseudosenyals:

Pseudosenyal Es dispara…
EXIT En acabar l'script, pel motiu que sigui (inclòs set -e)
ERR Cada vegada que una ordre falla, amb les mateixes exclusions que set -e
DEBUG / RETURN Abans de cada ordre (alenteix molt) / en tornar d'una funció

EXIT és l'important: és l'única manera de garantir que una cosa passi passi el que passi, perquè es dispara amb un exit normal, amb una fallada de set -e, amb un Ctrl+C i amb un kill; només SIGKILL se'l salta, i contra això no hi ha defensa. Tres detalls que eviten sorpreses: les cometes del gestor decideixen quan s'expandeixen les variables (simples, en el moment del dispar, que gairebé sempre és el que vols); trap - EXIT desinstal·la un gestor; i trap -p llista els instal·lats.

  1. Neteja garantida i Ctrl+C

El patró que converteix un script en una cosa segura d'interrompre:

TEMPORAL=""
netejar() {
    local codi=$?                      # cal capturar-lo a la PRIMERA línia
    [[ -n $TEMPORAL && -d $TEMPORAL ]] && rm -rf -- "$TEMPORAL"
    if (( codi == 0 )); then log "acabat correctament"
    else error "acabat amb codi $codi"; fi
    exit "$codi"                       # conserva el codi original
}
trap netejar EXIT
trap 'error "execució interrompuda"; exit 130' INT TERM
TEMPORAL=$(mktemp -d) || morir 73 "no puc crear el directori temporal"

Quatre decisions deliberades. local codi=$? va a la primera línia, perquè qualsevol ordre anterior el sobreescriuria. La comprovació [[ -n $TEMPORAL && -d ... ]] protegeix del cas en què l'script mori abans de crear el temporal: sense ella, un rm -rf "$TEMPORAL"/* amb la variable buida seria catastròfic. exit "$codi" preserva el codi original en comptes de retornar el del rm. I el gestor d'INT surt amb 130, que és 128 + 2 (SIGINT), la convenció de 04-01; aquell exit 130 dispara al seu torn l'EXIT, així que la neteja s'executa igualment: els gestors s'encadenen, no es trepitgen.

  1. Un gestor ERR que serveixi d'alguna cosa

Bash publica quatre variables durant un gestor ERR, i amb elles es construeix una traça gairebé tan útil com la d'un llenguatge amb excepcions:

Variable Contingut
$? / BASH_COMMAND El codi de la fallada / el text de l'ordre que fallava
BASH_LINENO[0] / FUNCNAME[@] La línia del context actual / la pila de funcions
traca_error() {
    local codi=$?
    printf '[%s] ERROR %d a %s, línia %s\n' \
        "$(date '+%F %T')" "$codi" "${BASH_SOURCE[1]##*/}" "${BASH_LINENO[0]}" >&2
    printf '  ordre : %s\n' "$BASH_COMMAND" >&2
    printf '  pila  : %s\n' "${FUNCNAME[*]:1}" >&2   # :1 salta traca_error
    return "$codi"
}
trap traca_error ERR
operador@srv-tramontana:~$ ~/scripts/copia_tramontana.sh
[2026-08-18 13:07:22] ERROR 1 a copia_tramontana.sh, línia 61
  ordre : cp -a -- /home/operador/dades/reserves.csv /srv/.../copia.9kQ2
  pila  : copiar_origens main

En quatre línies saps el codi, el fitxer, la línia, l'ordre exacta ja expandida i la cadena de crides. Compara-ho amb el cp: Permission denied a seques que hauries tingut i entendràs per què aquesta funció mereix viure a lib/comuns.sh.

  1. Idempotència

Un script és idempotent si executar-lo dues vegades deixa el sistema igual que executar-lo una. És l'objectiu de disseny més valuós del mòdul, perquè significa que el pots rellançar després d'una fallada sense pensar-hi. El contraexemple canònic, que en Luis va escriure el mes passat:

# NO idempotent: cada execució afegeix una altra línia
echo "max_connexions=200" >> /etc/tramontana/app.conf

Després de quatre execucions, grep -c '^max_connexions=' /etc/tramontana/app.conf retorna 4. L'aplicació llegeix l'última i funciona, així que ningú no se n'assabenta... fins que algú edita la primera i no passa res. La versió idempotent comprova abans d'actuar:

fixar_opcio() {                     # fixar_opcio FITXER CLAU VALOR
    local fitxer="$1" clau="$2" valor="$3"
    if grep -q "^${clau}=" "$fitxer"; then
        # Ja existeix: se substitueix, amb còpia datada com mana el Mòdul 2.
        sudo sed -i.bak-"$(date +%F)" "s|^${clau}=.*|${clau}=${valor}|" "$fitxer"
    else
        printf '%s=%s\n' "$clau" "$valor" | sudo tee -a "$fitxer" >/dev/null
    fi
}

operador@srv-tramontana:~$ fixar_opcio /etc/tramontana/app.conf max_connexions 200
operador@srv-tramontana:~$ fixar_opcio /etc/tramontana/app.conf max_connexions 200
operador@srv-tramontana:~$ grep -c '^max_connexions=' /etc/tramontana/app.conf
1

Tres idees transferibles: mkdir -p en comptes de mkdir (no falla si existeix), ln -sfn en comptes de ln -s (reemplaça l'enllaç en comptes d'imbricar-lo a dins) i comprovar l'estat desitjat abans d'aplicar el canvi. És el model amb què treballa Ansible, i per això la seva sortida distingeix ok de changed; hi tornarem a 07-06.

  1. Reintents, temporals segurs i bloqueig

Reintents amb retrocés exponencial

Una operació de xarxa no falla: falla de vegades. Reintentar immediatament empitjora les coses, així que s'espera cada vegada més:

# reintentar INTENTS ESPERA_INICIAL ORDRE...
#   Retorna: 0 si l'ordre acaba bé; 1 si s'esgoten els intents.
reintentar() {
    local intents="$1" espera="$2" n=1; shift 2
    until "$@"; do
        (( n >= intents )) && { error "fallada després de $n intents: $*"; return 1; }
        log "l'intent $n ha fallat; reintent d'aquí a ${espera}s"
        sleep "$espera"
        espera=$(( espera * 2 )); (( ++n ))
    done
}
operador@srv-tramontana:~$ reintentar 4 2 curl -sf http://10.0.2.15:8080/salut
[2026-08-18 13:11:02] l'intent 1 ha fallat; reintent d'aquí a 2s
[2026-08-18 13:11:04] l'intent 2 ha fallat; reintent d'aquí a 4s

Les esperes van 2, 4, 8, 16: això és el retrocés exponencial. I no reintentis mai operacions no idempotents —crear una reserva, enviar un correu— sense un identificador que eviti duplicar-les.

Temporals segurs

TMP="/tmp/copia_$$" és un forat de seguretat real: el PID és predictible, així que un atacant pot crear per endavant un enllaç simbòlic amb aquell nom apuntant a /etc/tramontana/app.conf, i el teu script, si corre amb privilegis, sobreescriurà el fitxer apuntat. TMP=$(mktemp -d) || morir 73 "sense temporal" ho evita perquè crea el fitxer de manera atòmica (amb O_EXCL, que falla si ja existeix), amb nom aleatori i permisos 600 —700 per a directoris—. Fes-lo servir sempre, juntament amb el trap ... EXIT de l'apartat 6.

Bloqueig dins de l'script

A 03-07 vas posar flock -n davant de l'ordre a la línia de crontab. Millor que el bloqueig visqui dins de l'script, perquè protegeixi també quan el llancis a mà:

exec 9>"$BLOQUEIG" || morir 73 "no puc obrir el bloqueig" seguit de flock -n 9 || morir 75 "ja hi ha una altra còpia en marxa". exec 9>fitxer obre el descriptor 9 per a tot l'script i flock -n 9 intenta bloquejar-lo sense esperar. El bloqueig s'allibera sol quan el procés mor, fins i tot a cop de kill -9, perquè el manté el nucli i no el fitxer. No esborris el fitxer de bloqueig en acabar: obre una cursa en què dos processos podrien bloquejar fitxers diferents amb el mateix nom.

  1. Aplicació: copia_tramontana.sh

Promès des del Mòdul 2, amb tot el d'aquesta lliçó:

#!/usr/bin/env bash
#
# copia_tramontana.sh - Còpia de seguretat de la configuració, les dades
#                       i el release actiu de Tramontana Reserves.
# Autor : Operador de sistemes <operador@srv-tramontana>  Data: 2026-08-18
# Ús    : copia_tramontana.sh [-h] [-v] [-n] [-d DESTI]
# Sortida: 0 ok | 2 ús | 66 origen il·legible | 69 falta una dependència
#          73 no es pot escriure | 75 ja hi ha una altra còpia | 130 interromput

set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=lib/comuns.sh
source "${SCRIPT_DIR}/lib/comuns.sh"
readonly BLOQUEIG="/var/lock/tramontana-copia.lock"
readonly ENLLAC_APP="/opt/tramontana/app"
readonly ORIGENS=(/home/operador/dades/reserves.csv /etc/tramontana/app.conf)
DESTI="${TRAMONTANA_DESTI:-/srv/tramontana/backups/enviaments}"
DRY_RUN=0; TEMPORAL=""

netejar() {                       # el gestor de l'apartat 6, tal qual
    local codi=$?
    [[ -n $TEMPORAL && -d $TEMPORAL ]] && rm -rf -- "$TEMPORAL"
    (( codi == 0 )) && log "còpia acabada correctament"
    exit "$codi"
}
trap netejar EXIT
trap 'error "execució interrompuda"; exit 130' INT TERM
trap 'error "fallada a la línia ${BASH_LINENO[0]}: ${BASH_COMMAND}"' ERR
# getopts ":hvnd:" com a 04-03: -v posa TRAMONTANA_VERBOSE=1, -n DRY_RUN=1,
# -d fixa DESTI, i qualsevol altra cosa acaba en 'morir 2'.
requereix_comanda tar gzip sha256sum || morir 69 "falten dependències"
mkdir -p "$DESTI" || morir 73 "no puc crear $DESTI"          # idempotent

# El bloqueig va abans de tocar res: si hi ha una altra còpia, sortim sense embrutar.
exec 9>"$BLOQUEIG" || morir 73 "no puc obrir $BLOQUEIG"
flock -n 9 || morir 75 "ja hi ha una altra còpia en marxa; abandono"
release=$(basename "$(readlink -f "$ENLLAC_APP")")
data=$(date +%F)
arxiu="${DESTI}/tramontana-${data}.tar.gz"
log "release actiu: $release, destí: $arxiu"
if (( DRY_RUN )); then
    printf 'Es copiarien %s i el release %s -> %s\n' \
        "${ORIGENS[*]}" "$release" "$arxiu"
    exit 0
fi
TEMPORAL=$(mktemp -d "${DESTI}/.copia.XXXXXXXX") || morir 73 "sense temporal"
for origen in "${ORIGENS[@]}"; do
    [[ -r $origen ]] || morir 66 "no puc llegir $origen"
    cp -a -- "$origen" "$TEMPORAL/"
    log "copiat $origen ($(formatar_bytes "$(stat -c %s "$origen")"))"
done
cp -a -- "/opt/tramontana/releases/${release}" "${TEMPORAL}/release-${release}"
printf 'release=%s\ndata=%s\nhost=%s\n' "$release" "$data" "$(hostname)" \
    > "${TEMPORAL}/MANIFEST"
# S'escriu com a .parcial i es reanomena al final: el fitxer definitiu només
# apareix si tar ha acabat bé, i mv dins del mateix sistema de fitxers és
# atòmic, igual que el 'ln -sfn' del desplegament.
tar -czf "${arxiu}.parcial" -C "$TEMPORAL" .
mv -- "${arxiu}.parcial" "$arxiu"
# app.conf conté db_password: 640 i grup tramontana, mai llegible per altres.
# És la fallada exacta que va obligar a rotar la contrasenya al juliol.
sha256sum "$arxiu" > "${arxiu}.sha256"
chmod 640 "$arxiu" "${arxiu}.sha256"
tar -tzf "$arxiu" >/dev/null || morir 1 "l'arxiu generat no és llegible"
log "còpia verificada: $(formatar_bytes "$(stat -c %s "$arxiu")")"
printf '%s\n' "$arxiu"            # stdout: la ruta, per encadenar
exit 0
operador@srv-tramontana:~$ ~/scripts/copia_tramontana.sh -v
[2026-08-18 13:20:04] release actiu: 3.2.1, destí: .../tramontana-2026-08-18.tar.gz
[2026-08-18 13:20:04] copiat /home/operador/dades/reserves.csv (1.8 KiB)
[2026-08-18 13:20:04] copiat /etc/tramontana/app.conf (412 B)
[2026-08-18 13:20:11] còpia verificada: 31.4 MiB
/srv/tramontana/backups/enviaments/tramontana-2026-08-18.tar.gz
operador@srv-tramontana:~$ ~/scripts/copia_tramontana.sh &   # i alhora:
operador@srv-tramontana:~$ ~/scripts/copia_tramontana.sh; echo "codi: $?"
[2026-08-18 13:20:14] ERROR: ja hi ha una altra còpia en marxa; abandono
codi: 75

Prova també el camí de fallada, que és on l'script demostra el seu valor: interromp-lo amb Ctrl+C a mitges i comprova que no queda cap .copia.XXXXXXXX ni cap .parcial al destí. La neteja que no has provat és neteja que no funciona.

Errors Comuns i Consells

  • Creure que set -e ho atura tot. No actua en condicions, ni després de &&/||, ni dins de funcions fetes servir com a condició, ni a local x=$(cmd): continua comprovant l'important a mà.
  • No capturar $? a la primera línia del gestor EXIT. Qualsevol ordre anterior el sobreescriu i l'script retorna 0 encara que hagi fallat.
  • Fer servir cometes dobles al gestor de trap. Les variables s'expandeixen en instal·lar la trampa, no en disparar-la: cometes simples tret que sàpigues per què. I no esborris el fitxer de bloqueig en acabar, perquè obre una cursa entre processos: el bloqueig l'allibera el nucli en morir el procés.
  • mktemp sense trap ... EXIT. Cada execució fallida deixa brossa: van sempre junts. Evita també els noms predictibles a /tmp, perquè $$ no és aleatori: és un atac d'enllaços simbòlics esperant l'ocasió.
  • Scripts no idempotents. Si rellançar-lo duplica una línia o suma dues vegades, no pots reintentar després d'una fallada, que és justament quan més falta fa.
  • Consell: escriu el fitxer definitiu amb un nom temporal i reanomena'l al final; és la manera barata que mai no existeixi un fitxer a mitges. I quan una cosa no funcioni, bash -x amb PS4 abans de rellegir el codi: cinc segons de traça estalvien vint minuts de suposicions.
  • Consell: prova el camí d'error, no només el d'èxit: treu permisos, omple el disc, mata el procés a mitges. És l'única manera de saber si les teves trampes funcionen.

Exercicis

Exercici 1. Aquest script sembla correcte i falla en silenci. Explica per què i arregla'l de dues maneres.

#!/usr/bin/env bash
set -euo pipefail
preparar_desti() {
    mkdir -p /srv/tramontana/backups/nou
    cp /etc/tramontana/no_existeix.conf /srv/tramontana/backups/nou/
    echo "destí preparat"
}
if preparar_desti; then
    echo "començant la còpia..."
fi

Exercici 2. Afegeix a lib/comuns.sh una funció amb_neteja: ha de crear un directori temporal, publicar-lo a TRAMONTANA_TMP, instal·lar el trap de neteja i retornar 0. Escriu un cas de prova que verifiqui que, després d'un script que mor per set -e, el directori ja no existeix.

Exercici 3. Fes idempotent aquest fragment de desplegament d'en Luis i explica quin problema resol cada canvi: mkdir /opt/tramontana/releases/3.3.0, tar -xzf /tmp/app-3.3.0.tar.gz -C /opt/tramontana/releases/3.3.0, ln -s /opt/tramontana/releases/3.3.0 /opt/tramontana/app i echo "3.3.0" >> /opt/tramontana/HISTORIAL.

Solucions

Solució 1. La fallada és el cas 4 de l'apartat 4: preparar_desti es fa servir com a condició d'un if, i això desactiva set -e a tota la funció. El cp falla, imprimeix el seu error, l'execució continua, s'imprimeix «destí preparat», la funció retorna 0 —el codi de l'echo— i l'if dona el resultat per bo. L'script anuncia que comença la còpia amb el destí incomplet.

# Arranjament A: la funció comprova i surt explícitament.
preparar_desti() {
    mkdir -p /srv/tramontana/backups/nou || return 1
    cp /etc/tramontana/no_existeix.conf /srv/tramontana/backups/nou/ || return 1
    echo "destí preparat"
}
# Arranjament B: no fer-la servir com a condició; que set -e faci la seva feina.
preparar_desti; echo "començant la còpia..."
operador@srv-tramontana:~$ bash /tmp/prep_a.sh; echo "codi: $?"
cp: cannot stat '/etc/tramontana/no_existeix.conf': No such file or directory
codi: 1

L'arranjament A és preferible quan la funció pot fallar de maneres que vols distingir; el B, quan qualsevol fallada ha d'avortar. El que no val és l'original, que promet comprovar i no comprova.

Solució 2.

# amb_neteja [PLANTILLA] -> crea un temporal, el publica a TRAMONTANA_TMP
#   i instal·la el trap EXIT que l'esborra. Retorna: 0 si s'ha creat; 73 si no.
amb_neteja() {
    TRAMONTANA_TMP=$(mktemp -d "${1:-/tmp/tramontana.XXXXXXXX}") || return 73
    # Cometes SIMPLES: la variable es llegeix en disparar-se el trap, no ara.
    trap 'rm -rf -- "${TRAMONTANA_TMP:-}"' EXIT
    return 0
}
# Prova: un script fill que crea el temporal i mor per set -e.
desat=$(bash -c '
    set -euo pipefail
    source '"$SCRIPT_DIR"'/lib/comuns.sh
    amb_neteja; printf "%s\n" "$TRAMONTANA_TMP"
    false                      # mor aquí; el trap EXIT es dispara
' 2>/dev/null) || true
[[ -d $desat ]] && resultat="encara existeix" || resultat="esborrat"
comprovar "temporal netejat després de la fallada" "esborrat" "$resultat"
operador@srv-tramontana:~$ ~/scripts/test_comuns.sh | tail -2
  ok      temporal netejat després de la fallada
0 fallada/es

La prova importa perquè la funció depèn de dues subtileses fàcils de trencar: les cometes simples del trap i el fet que EXIT es dispari també quan qui mata l'script és set -e. Un test ho fixa; un comentari no.

Solució 3.

versio="3.3.0"; desti="/opt/tramontana/releases/${versio}"
mkdir -p "$desti"                        # -p no falla si ja existeix
# Comprovar el paquet ABANS d'extreure, com mana el curs des de 02-04.
tar -tzf "/tmp/app-${versio}.tar.gz" >/dev/null || morir 65 "paquet corrupte"
# Netegem abans d'extreure perquè no quedin restes d'un intent anterior
# amb fitxers que ja no formen part del release.
rm -rf -- "${desti:?}"/*
tar -xzf "/tmp/app-${versio}.tar.gz" -C "$desti"
# ln -sfn: -f reemplaça l'enllaç existent, -n evita que, en existir ja
# 'app' com a enllaç a un directori, el nou es creï DINS d'ell. I
# relatiu, com tot el curs: es fa des de /opt/tramontana.
ln -sfn "releases/${versio}" /opt/tramontana/app
grep -qxF "$versio" /opt/tramontana/HISTORIAL 2>/dev/null \
    || printf '%s\n' "$versio" >> /opt/tramontana/HISTORIAL   # només si falta

Els quatre canvis. mkdir -p evita que la segona execució mori a la primera línia. El tar -tzf previ detecta un paquet truncat abans d'haver tocat res. ln -sfn arregla l'error clàssic: ln -s sobre un enllaç que ja apunta a un directori crea el nou enllaç dins d'aquell directori, deixant /opt/tramontana/releases/3.2.1/3.3.0, i el desplegament no canvia res mentre tu creus que sí. I el grep -qxF —-x línia completa, -F text literal— converteix el >> en condicional, evitant un HISTORIAL amb la mateixa versió repetida quatre vegades.

Conclusió

Els teus scripts ja no fallen en silenci, i per primera vegada els pots interrompre sense por.

  • Depures amb bash -x i un PS4 que diu fitxer, línia i funció, acotant zones amb set -x/set +x, i comproves la sintaxi sense executar amb bash -n. Entens SC2086, SC2046, SC2164 i SC2155, i saps silenciar un avís justificant-lo a la línia anterior.
  • Coneixes el mode estricte peça a peça —-e, -u, pipefail— i, sobretot, els cinc contextos on set -e no actua, amb el cas de la funció feta servir com a condició demostrat i arreglat de dues maneres. Fas servir trap amb EXIT, ERR i INT, capturant $? a la primera línia del gestor, amb cometes simples, sabent que EXIT es dispara passi el que passi tret d'amb SIGKILL.
  • Escrius un traca_error() que aprofita BASH_COMMAND, BASH_LINENO i FUNCNAME per donar una traça de debò, i dissenyes per a la idempotència: mkdir -p, ln -sfn, comprovar abans d'afegir, escriure a .parcial i reanomenar al final.
  • Reintentes amb retrocés exponencial, crees temporals amb mktemp en comptes de noms predictibles amb $$, i bloqueges amb flock sobre un descriptor dins del mateix script.
  • I copia_tramontana.sh existeix per fi: amb bloqueig, temporals que es netegen sols, escriptura atòmica, suma de verificació, permisos 640 sobre un arxiu que conté db_password i codis de sortida documentats.

Queda una última pregunta, la que separa un script que funciona d'un que es desplega: què passa quan això ho executi cron a les 4:20 sense ningú al davant? On va el registre? Com s'avisa la Marta només si hi ha alguna cosa a fer? D'on surt la contrasenya de la base de dades si no pot ser al fitxer? I què passa si una execució triga més que l'interval entre execucions? A Scripts de Producció: Bones Pràctiques tanquem el mòdul amb aquella llista de comprovació completa, la plantilla canònica amb main "$@", la precedència de configuració implementada de debò, el principi de «silenci si tot va bé», les proves abans de producció, i el projecte integrador: desplegar.sh, amb validació, còpia prèvia, canvi atòmic de l'enllaç, comprovació per curl i reversió automàtica si el 8080 no respon.

Curs de Linux: De Principiant a Administrador de Sistemes

Mòdul 1: Introducció a Linux

Mòdul 2: Comandes Bàsiques de Linux

Mòdul 3: Habilitats Avançades en la Línia de Comandes

Mòdul 4: Scripting en Shell

Mòdul 5: Administració del Sistema

Mòdul 6: Xarxes i Seguretat

Mòdul 7: Temes Avançats

Mòdul 8: Projectes Pràctics

© Copyright 2026. Tots els drets reservats