Gestion d'erreurs avancée : pipefail, trap et nettoyage
Notions théoriques
Le piège des pipelines avec set -e
Vous connaissez set -eu depuis le niveau 1. Mais set -e a un angle mort avec les pipelines (|), vus en séance 4 : le code de retour d'une chaîne cmd1 | cmd2 est celui de cmd2 uniquement.
false | true
echo $? # 0 : true a réussi, l'échec de false est invisible
Résultat : set -e ne détecte pas l'échec de false dans cet exemple, puisque true (la dernière commande du pipeline) a réussi. L'option set -o pipefail, annoncée en fin de niveau 1, corrige cela : le pipeline échoue dès que n'importe laquelle de ses commandes échoue.
set -o pipefail
false | true
echo $? # 1 : l'échec de false est maintenant détecté
La ligne d'en-tête recommandée à partir de maintenant devient donc :
set -euo pipefail
Les angles morts restants de set -e
Même avec set -euo pipefail, certaines situations n'arrêtent pas le script :
- une commande testée dans un
if,while, ou après&&/||— c'est voulu, sinon aucun test ne serait possible ; - une commande suivie explicitement de
|| true, qui neutralise volontairement son échec.
# Ne s'arrête PAS, même sous set -e : c'est le comportement attendu
if grep -q "erreur" fichier.log; then
echo "Erreur trouvée"
fi
# Échec volontairement ignoré
rm -f fichier_optionnel.tmp || true
set -e reste un filet de sécurité pour les échecs inattendus, pas un substitut à une gestion d'erreur réfléchie avec des tests explicites — un point déjà souligné en séance 7 du niveau 1, qui garde toute sa valeur ici.
trap : réagir à un signal
trap exécute des instructions lorsqu'un signal particulier est reçu par le script :
trap 'instructions' SIGNAL
| Signal | Déclenché quand... |
|---|---|
EXIT | le script se termine, quelle qu'en soit la raison (fin normale, exit, erreur) |
INT | l'utilisateur appuie sur Ctrl+C |
TERM | le script reçoit une demande d'arrêt (kill) |
ERR | une commande échoue (utile combiné à set -e) |
Le signal EXIT est le plus utile en pratique : il garantit qu'un bloc de nettoyage s'exécute toujours, même en cas d'erreur imprévue ou d'interruption.
cleanup() {
rm -rf "$TMPDIR"
}
TMPDIR=$(mktemp -d)
trap cleanup EXIT
Placez toujours le trap juste après la création de la ressource à nettoyer : de cette façon, même si le script s'arrête à la ligne suivante (une erreur, par exemple), le nettoyage est déjà armé.
Créer des fichiers temporaires en sécurité
tmp=$(mktemp) # un fichier temporaire unique
tmpdir=$(mktemp -d) # un dossier temporaire unique
mktemp garantit un nom unique (pas de conflit si le script tourne deux fois en même temps) et crée le fichier avec des permissions restreintes. C'est nettement plus sûr qu'un nom fixe comme /tmp/monscript.tmp, qui pourrait déjà exister ou être manipulé par un autre processus.
Empêcher deux exécutions simultanées
Un script planifié (nous verrons cron en séance 7) peut se chevaucher avec sa propre exécution précédente si celle-ci dure trop longtemps. Un verrou l'empêche :
LOCK="/tmp/sauvegarde.lock"
if ! mkdir "$LOCK" 2>/dev/null; then
echo "Une exécution est déjà en cours" >&2
exit 3
fi
trap 'rmdir "$LOCK"' EXIT
mkdir échoue si le dossier existe déjà, et cette opération est atomique (aucun autre processus ne peut créer le même dossier entre le test et la création) — c'est ce qui en fait un verrou fiable, contrairement à un simple test [ -f fichier_verrou ] suivi d'une création, qui laisse une petite fenêtre où deux scripts pourraient passer le test en même temps. flock est une alternative plus robuste encore, à découvrir si le besoin s'en fait sentir.
Journaliser proprement
exec >> "$LOG" 2>&1
Cette ligne, placée en tête de script, redirige toute la sortie standard et d'erreur du reste du script vers le fichier $LOG, sans avoir à répéter >> "$LOG" sur chaque ligne. Pour afficher à la fois à l'écran et dans le fichier, tee -a (ajout) est plus adapté :
echo "Sauvegarde démarrée" | tee -a "$LOG"
logger "message" envoie un message vers le journal système (syslog/journald), consultable ensuite avec journalctl. Une alternative à un fichier de log dédié, utile pour un script destiné à un environnement de production surveillé de façon centralisée.
Localiser une erreur précisément
trap 'echo "Erreur à la ligne $LINENO" >&2' ERR
$LINENO contient le numéro de ligne en cours d'exécution — combiné à un trap ERR, cela permet de savoir exactement où un script s'est arrêté sans avoir à ajouter des echo de débogage partout.
Exemple pratique
#!/bin/bash
# Script : traitement.sh
# Rôle : démontrer trap, mktemp et un verrou d'exécution
set -euo pipefail
log_info() {
echo "[$(date +%H:%M:%S)] $1"
}
cleanup() {
log_info "Nettoyage de $TMPDIR"
rm -rf "$TMPDIR"
}
LOCK="/tmp/traitement.lock"
if ! mkdir "$LOCK" 2>/dev/null; then
echo "Une exécution est déjà en cours" >&2
exit 3
fi
trap 'rmdir "$LOCK"' EXIT
TMPDIR=$(mktemp -d)
trap cleanup EXIT
trap 'log_info "Interrompu par l'"'"'utilisateur"; exit 130' INT
log_info "Travail en cours dans $TMPDIR..."
sleep 10
log_info "Terminé"
Lancez ce script puis appuyez sur Ctrl+C avant la fin des 10 secondes : le message d'interruption s'affiche, et $TMPDIR disparaît quand même — le trap EXIT s'exécute même sur une sortie provoquée par un signal.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Nous rendons sauvegarde.sh réellement robuste face aux erreurs, en reprenant la version structurée en fonctions du niveau 1.
Étape 1 — Passer à set -euo pipefail
Dès qu'un script combine des commandes avec | (ce qui est fréquent dès la séance 4), pipefail devrait accompagner systématiquement set -eu.
Étape 2 — Créer un espace de travail temporaire nettoyé automatiquement
Armez le trap immédiatement après mktemp -d, avant toute autre instruction : si une erreur survient juste après, le nettoyage reste garanti.
Étape 3 — Ajouter un verrou d'exécution
mkdir échoue si le dossier existe déjà, sans laisser de fenêtre où deux processus pourraient croire, tous les deux, avoir obtenu le verrou — contrairement à un test suivi d'une création en deux étapes séparées.
Étape 4 — Rediriger toute la sortie vers un fichier de log
exec >> "$LOG" 2>&1 en une seule ligne évite de répéter >> "$LOG" sur chaque echo du script, et garantit qu'aucun message ne peut être oublié par erreur.