Un script qui continue après une erreur ne gère pas les erreurs : il les ignore. Sans set -euo pipefail, un mv qui échoue affiche quand même « OK ». Sans read -r, un antislash saisi disparaît silencieusement. Quelques lignes de protection suffisent à transformer un script fragile en script fiable en production. Chaque comportement décrit ici a été mesuré sur Bash 5.2 (Debian 13).
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Activer
set -euo pipefailpour arrêter le script dès la première erreur - Utiliser
read -ret citer les variables pour éviter le word splitting - Valider les entrées avec des tests
[[ ]]avant chaque action destructive - Intercepter les erreurs imprévues avec
trap - Créer des fichiers temporaires sécurisés avec
mktemp - Analyser et corriger un script avec shellcheck
Dans quel contexte ?
Section intitulée « Dans quel contexte ? »Un script fonctionnel en développement peut échouer silencieusement en production, là où les chemins diffèrent, les variables sont absentes, et personne ne regarde la sortie d'erreur :
- un script de sauvegarde qui continue après un
mvraté et marque l'opération comme réussie - un script de déploiement qui lit
$CONFIG_FILEvide parce que la variable n'a pas été exportée - un pipeline
cat log | grep ERR | wc -lqui retourne 0 même sicata échoué - un fichier temporaire prévisible dans
/tmpremplacé par un lien symbolique avant l'écriture - un mot de passe en clair dans un script versionné sur GitHub
Les techniques de ce guide s'appliquent à tout script destiné à tourner en production, en cron, ou dans une CI, dès que vous n'êtes pas là pour observer la sortie.
Un script sans protections, le problème
Section intitulée « Un script sans protections, le problème »Voici deplacement.sh, un script simple mais truffé de pièges. Il fonctionne sur un nom de fichier sage, et casse sur le premier cas réel :
#!/bin/bashecho "Fichier source :"read src
echo "Fichier destination :"read dst
mv $src $dstecho "Déplacement effectué."Le tableau recense les quatre problèmes de ce script, avec leur conséquence concrète. Chacun se corrige par une protection dédiée, présentée dans les sections suivantes.
| Problème | Conséquence |
|---|---|
read sans -r | L'antislash est retiré : la saisie a\nb devient anb (mesuré) |
$src et $dst sans guillemets | Un espace dans le nom scinde l'argument en plusieurs mots |
| Pas de validation | Si le fichier source n'existe pas, mv échoue mais "Déplacement effectué." s'affiche quand même |
Pas de set -e | Un code de retour non nul ne stoppe pas l'exécution |
set -euo pipefail
Section intitulée « set -euo pipefail »Placez toujours cette ligne juste après le shebang, pour que la première commande soit déjà protégée. Elle combine trois options distinctes :
set -euo pipefail-e : s'arrêter à la première erreur
Section intitulée « -e : s'arrêter à la première erreur »Sans -e, un échec passe inaperçu : le script enchaîne sur la ligne suivante comme si de rien n'était, et affiche un message trompeur :
#!/bin/bashrm /fichier/inexistantecho "Nettoyage terminé." # s'affiche quand mêmerm: cannot remove '/fichier/inexistant': No such file or directoryNettoyage terminé.Avec set -e, le script s'arrête après rm, avec le code de retour de la commande en échec, et n'affiche pas le message trompeur.
Le piège de ((n++)) avec set -e
Section intitulée « Le piège de ((n++)) avec set -e »Sous set -e, un compteur ((n++)) tue le script au premier tour si n vaut 0. C'est le piège le plus vicieux de set -e, parce qu'il frappe un idiome banal et sans le moindre message. La cause : ((expression)) renvoie un code de retour dérivé de la valeur arithmétique, et n++ est un post-incrément, il s'évalue à l'ancienne valeur de n. Partant de n=0, ((n++)) vaut donc 0, ce que le shell traduit en code de retour 1 (0 = « faux »), et set -e arrête tout.
#!/usr/bin/env bashset -euo pipefailn=0((n++)) # code retour 1 (ancienne valeur = 0), le script meurt iciecho "jamais atteint"Le comble : dès que n vaut 1 ou plus, ((n++)) renvoie l'ancienne valeur non nulle, donc un code 0, et tout passe. Le bug ne se déclenche qu'au premier incrément et seulement depuis zéro, ce qui le rend imprévisible en relecture. Trois formes sûres :
n=$((n + 1)) # affectation : toujours code retour 0((++n)) # pré-incrément : évalue la NOUVELLE valeur (1), code 0((n++)) || true # neutralise le code retour-u : interdire les variables non définies
Section intitulée « -u : interdire les variables non définies »Avec -u, lire une variable jamais définie arrête le script au lieu de la remplacer par une chaîne vide :
set -uecho "Répertoire : $REPERTOIRE"bash: REPERTOIRE: unbound variableSans -u, la variable vide serait substituée silencieusement, ce qui devient dangereux dans rm -rf "$dir/" : avec dir absente, la commande vise la racine.
-o pipefail : détecter les erreurs dans les pipelines
Section intitulée « -o pipefail : détecter les erreurs dans les pipelines »Par défaut, seule la dernière commande d'un pipeline détermine son code de retour :
cat fichier_inexistant.txt | grep "motif"echo $? # retourne 1 (grep sans résultats), mais l'échec de cat est ignoréAvec set -o pipefail, le pipeline retourne le code de la dernière commande en échec, la plus à droite, ou 0 si toutes réussissent. Mesuré : (exit 2) | (exit 3) | true rend 3 avec pipefail, et 0 sans. Combiné à -e, un maillon en échec arrête le script, même quand la dernière commande réussit.
Valider et citer les variables
Section intitulée « Valider et citer les variables »read -r et guillemets doubles
Section intitulée « read -r et guillemets doubles »read -r préserve les antislashs tels quels. Les guillemets doubles protègent contre le word splitting et le globbing :
read -r src # -r : backslashes préservésmv "$src" "$dst" # guillemets : un seul argument même avec des espacesSans guillemets, si src="mon rapport.txt" alors mv $src $dst passe trois arguments à mv : mon, rapport.txt et la valeur de $dst. Le déplacement échoue, ou pire, vise un autre fichier.
Tester avant d'agir
Section intitulée « Tester avant d'agir »Validez chaque entrée avant toute opération destructive. La fonction erreur, définie plus bas dans le script complet, affiche le message et sort en échec :
[[ -z "$src" ]] && erreur "Fichier source non spécifié."[[ -f "$src" ]] || erreur "Le fichier '$src' n'existe pas."
dst_dir=$(dirname "$dst")[[ -w "$dst_dir" ]] || erreur "Pas les droits d'écriture dans '$dst_dir'."Les tests les plus utiles pour valider une entrée tiennent en six opérateurs. Ils sont détaillés dans Conditions et tests Bash, avec leurs pièges de quoting :
| Test | Signification |
|---|---|
-z "$var" | Variable vide ou non définie |
-n "$var" | Variable non vide |
-f "$chemin" | Fichier ordinaire existant |
-d "$chemin" | Répertoire existant |
-w "$chemin" | Accessible en écriture |
-r "$chemin" | Accessible en lecture |
trap, intercepter les signaux
Section intitulée « trap, intercepter les signaux »trap exécute une commande quand le script reçoit un signal (INT, TERM) ou un pseudo-signal de Bash (ERR, EXIT, DEBUG).
Intercepter les erreurs inattendues (ERR)
Section intitulée « Intercepter les erreurs inattendues (ERR) »erreur() { echo "Erreur : $1" >&2 exit 1}
trap 'erreur "Interruption inattendue à la ligne $LINENO."' ERRQuand une commande échoue avec set -e actif, trap ERR s'exécute avant la sortie, utile pour afficher un message contextualisé avec le numéro de ligne. Le piège fonctionne aussi sans set -e (mesuré) : le gestionnaire s'exécute, puis le script continue, sauf si le gestionnaire appelle lui-même exit.
Nettoyer à la sortie (EXIT)
Section intitulée « Nettoyer à la sortie (EXIT) »trap ... EXIT s'exécute à chaque sortie du script : succès, erreur, Ctrl+C (SIGINT) ou kill (SIGTERM), deux cas mesurés. Seul un kill -9 (SIGKILL) l'empêche, puisque ce signal ne peut pas être intercepté :
tmpfile=$(mktemp)trap 'rm -f "$tmpfile"' EXIT
# Le fichier est supprimé même si le script échoue en cours de routemktemp, fichiers temporaires sécurisés
Section intitulée « mktemp, fichiers temporaires sécurisés »Ne jamais créer un fichier temporaire avec un nom prévisible (/tmp/monscript.tmp) : un attaquant peut créer avant vous un lien symbolique portant ce nom et pointant vers un fichier sensible, que votre script écrasera avec ses droits.
tmpfile=$(mktemp) # Crée /tmp/tmp.xK3pQr (nom unique, permissions 600)tmprepertoire=$(mktemp -d) # Crée un répertoire temporaireAssocié à trap EXIT, la suppression est garantie même en cas d'erreur. L'exemple extrait les avertissements du journal système du démarrage courant :
tmpfile=$(mktemp)trap 'rm -f "$tmpfile"' EXIT
journalctl -p warning -b --no-pager > "$tmpfile"echo "$(wc -l < "$tmpfile") lignes d'avertissement trouvées"Pas de secrets en dur
Section intitulée « Pas de secrets en dur »Un secret pose deux problèmes distincts : où il est stocké, et comment il est transmis au programme qui l'utilise. Le premier, c'est le fichier versionné ; le second, c'est la ligne de commande, lisible par tous les comptes de la machine pendant l'exécution.
# À ne pas faire : secret en dur, puis en argumentDB_PASSWORD="monsecret"mysql -u sauvegarde -p"$DB_PASSWORD"Stocker le secret hors du script. Une variable d'environnement exportée avant l'appel, ou un fichier de configuration en chmod 600, jamais versionné, gardent le secret hors du dépôt :
# config.conf : lisible par son seul propriétaire, jamais versionnéDB_PASSWORD="monsecret"chmod 600 config.confsource config.confNe pas le passer en argument. Même lu depuis une variable, -p"$DB_PASSWORD" place le secret dans les arguments du processus, que ps affiche à tous les comptes de la machine : c'est mesuré avec curl dans Télécharger des fichiers. Préférez le fichier d'options que la commande sait lire elle-même, comme --defaults-extra-file pour le client MySQL :
# /etc/sauvegarde/mysql.cnf, en chmod 600[client]user=sauvegardepassword=monsecretmysql --defaults-extra-file=/etc/sauvegarde/mysql.cnfLe script complet
Section intitulée « Le script complet »Voici deplacement_robuste.sh, version corrigée de deplacement.sh. Il réunit les quatre protections : options strictes, lectures avec read -r, validations avant l'action, et gestionnaire d'erreur :
#!/bin/bashset -euo pipefail
# ── Gestion des erreurs ────────────────────────────────────────────────────────erreur() { echo "Erreur : $1" >&2 exit 1}
trap 'erreur "Interruption inattendue à la ligne $LINENO."' ERR
# ── Lectures sécurisées ────────────────────────────────────────────────────────echo "Fichier source :"read -r src
echo "Fichier destination :"read -r dst
# ── Validations ────────────────────────────────────────────────────────────────[[ -z "$src" ]] && erreur "Fichier source non spécifié."[[ -z "$dst" ]] && erreur "Fichier destination non spécifié."[[ -f "$src" ]] || erreur "Le fichier '$src' n'existe pas."
dst_dir=$(dirname "$dst")[[ -w "$dst_dir" ]] || erreur "Pas les droits d'écriture dans '$dst_dir'."
# ── Action ────────────────────────────────────────────────────────────────────mv -- "$src" "$dst"echo "OK : '$src' déplacé vers '$dst'."exit 0Le double tiret -- avant les arguments de mv protège contre les noms de fichiers commençant par -, que mv prendrait sinon pour une option. Les guillemets traitent les espaces, le -- traite les tirets : ce sont deux protections distinctes.
shellcheck et shfmt
Section intitulée « shellcheck et shfmt »shellcheck analyse statiquement un script et signale les mauvaises pratiques avant l'exécution. Il s'installe avec apt install shellcheck sur Debian et Ubuntu ; sur RHEL et ses dérivées, il vient d'EPEL, sous le nom de paquet ShellCheck.
shellcheck deplacement.shSur le script fragile du début, shellcheck 0.10 relève les trois défauts de lecture et de quoting, avec leur numéro de ligne réel (sortie abrégée) :
In deplacement.sh line 3:read src^--^ SC2162 (info): read without -r will mangle backslashes.
In deplacement.sh line 6:read dst^--^ SC2162 (info): read without -r will mangle backslashes.
In deplacement.sh line 8:mv $src $dst ^--^ SC2086 (info): Double quote to prevent globbing and word splitting. ^--^ SC2086 (info): Double quote to prevent globbing and word splitting.Chaque code (SC2162, SC2086) pointe vers une explication détaillée sur shellcheck.net/wiki/SCxxxx. L'absence de validation et de set -e, elle, ne relève pas de l'analyse statique : shellcheck ne la signale pas.
shfmt reformate le code source de manière cohérente : indentation, espaces, retours à la ligne. Il est empaqueté dans Debian 13 (version 3.8.0) :
sudo apt install shfmtshfmt -i 4 -w mon_script.sh # indentation 4 espaces, réécriture en placeshfmt -d mon_script.sh # affiche les différences sans modifierDépannage
Section intitulée « Dépannage »Les symptômes ci-dessous sont ceux des options strictes elles-mêmes : un script protégé s'arrête plus souvent, et c'est voulu. Le réflexe est de lire le code de retour et la ligne affichés par le trap ERR.
| Symptôme | Cause probable | Solution |
|---|---|---|
| Le script s'arrête à une commande bénigne | -e interrompt sur tout code non nul | Protéger avec cmd || true ou if cmd; then ... fi |
unbound variable sur une variable initialisée | -u détecte un nom mal orthographié | Vérifier l'orthographe exacte et initialiser : var="" |
trap ERR ne se déclenche pas | Commande testée par if, && ou ||, trap posé après l'erreur, ou échec dans une fonction sans set -E | Poser trap en tête de script, ajouter set -E pour les fonctions |
mktemp: failed to create file | /tmp plein ou monté en readonly | df /tmp puis nettoyer les fichiers orphelins |
| Script fonctionne en interactif mais pas en cron | PATH différent dans cron | Utiliser les chemins complets (/bin/mv, /usr/bin/grep) |
À retenir
Section intitulée « À retenir »set -euo pipefailest la première ligne après le shebang dans tout script de production.read -rpréserve les backslashes ; les guillemets doubles protègent contre le word splitting.- Valider toujours les entrées avant une opération destructive (
mv,rm,cp). trap 'cmd' ERRintercepte les erreurs inattendues ;trap 'cmd' EXITgarantit le nettoyage.mktempgénère un nom unique sécurisé, ne jamais utiliser/tmp/monscript.tmp.- shellcheck détecte la plupart des erreurs avant l'exécution, à lancer avant chaque commit.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Fonctions Bash
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications