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
- Depuració: veure què fa realment l'script
shellcheckseriosament- El mode estricte esmicolat
- On
set -eno et salva trapi els pseudosenyals- Neteja garantida i Ctrl+C
- Un gestor
ERRque serveixi d'alguna cosa - Idempotència
- Reintents, temporals segurs i bloqueig
- Aplicació:
copia_tramontana.sh
- 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.
shellcheck seriosament
shellcheck seriosamentJa 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: 1Al 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"
- 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àssicrm -rf "$DIR/"convertit enrm -rf /perquèDIRno 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 -lretorna 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: 1Sobre 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".
- On
set -e no et salva
set -e no et salvaAquest é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
preparatcp 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 )).
trap i els pseudosenyals
trap i els pseudosenyalstrap 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.
- 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.
- Un gestor
ERR que serveixi d'alguna cosa
ERR que serveixi d'alguna cosaBash 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 mainEn 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.
- 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.confDespré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
1Tres 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.
- 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 4sLes 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.
- Aplicació:
copia_tramontana.sh
copia_tramontana.shPromè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 0operador@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: 75Prova 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 -eho atura tot. No actua en condicions, ni després de&&/||, ni dins de funcions fetes servir com a condició, ni alocal x=$(cmd): continua comprovant l'important a mà. - No capturar
$?a la primera línia del gestorEXIT. 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. mktempsensetrap ... 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 -xambPS4abans 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..."
fiExercici 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: 1L'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/esLa 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 faltaEls 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 -xi unPS4que diu fitxer, línia i funció, acotant zones ambset -x/set +x, i comproves la sintaxi sense executar ambbash -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 onset -eno actua, amb el cas de la funció feta servir com a condició demostrat i arreglat de dues maneres. Fas servirtrapambEXIT,ERRiINT, capturant$?a la primera línia del gestor, amb cometes simples, sabent queEXITes dispara passi el que passi tret d'amb SIGKILL. - Escrius un
traca_error()que aprofitaBASH_COMMAND,BASH_LINENOiFUNCNAMEper donar una traça de debò, i dissenyes per a la idempotència:mkdir -p,ln -sfn, comprovar abans d'afegir, escriure a.parciali reanomenar al final. - Reintentes amb retrocés exponencial, crees temporals amb
mktempen comptes de noms predictibles amb$$, i bloqueges ambflocksobre un descriptor dins del mateix script. - I
copia_tramontana.shexisteix per fi: amb bloqueig, temporals que es netegen sols, escriptura atòmica, suma de verificació, permisos 640 sobre un arxiu que contédb_passwordi 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
- Què és Linux?
- Història de Linux
- Distribucions de Linux
- Instal·lant Linux
- Primer Contacte amb el Sistema
- Estructura del Sistema de Fitxers de Linux
Mòdul 2: Comandes Bàsiques de Linux
- Introducció a la Línia de Comandes
- Obtenir Ajuda i Documentació del Sistema
- Navegant pel Sistema de Fitxers
- Operacions amb Fitxers i Directoris
- Visualització i Edició de Fitxers
- Enllaços Durs i Simbòlics
- Permisos i Propietat dels Fitxers
Mòdul 3: Habilitats Avançades en la Línia de Comandes
- L'Entorn del Shell: Variables, Àlies i Historial
- Ús de Comodins i Expressions Regulars
- Cerca de Fitxers i Contingut: find, locate i grep
- Canonades i Redirecció
- Processament de Text: cut, sort, uniq, sed i awk
- Gestió de Processos
- Programació de Tasques amb Cron
- Comandes de Xarxa
Mòdul 4: Scripting en Shell
- Introducció al Scripting en Shell
- Variables i Tipus de Dades
- Entrada, Sortida i Arguments d'un Script
- Estructures de Control
- Funcions i Biblioteques
- Depuració i Gestió d'Errors
- Scripts de Producció: Bones Pràctiques
Mòdul 5: Administració del Sistema
- Gestió d'Usuaris i Grups
- sudo i Permisos Especials
- Gestió de Paquets
- Gestió de Discs
- systemd i la Gestió de Serveis
- Registres del Sistema: journald i syslog
- Monitoratge del Sistema i Optimització del Rendiment
- Còpies de Seguretat i Restauració
Mòdul 6: Xarxes i Seguretat
- Configuració de Xarxes
- SSH i Accés Remot
- Tallafocs i Seguretat Perimetral
- Sistemes de Detecció d'Intrusions
- Gestió de Secrets i Certificats TLS
- Assegurant Sistemes Linux
Mòdul 7: Temes Avançats
- El Procés d'Arrencada i la Recuperació del Sistema
- Diagnòstic Avançat: strace, perf i eBPF
- Optimització del Nucli de Linux
- Virtualització amb Linux
- Contenidors de Linux i Docker
- Automatització amb Ansible
- Alta Disponibilitat i Balanceig de Càrrega
