set -euo pipefail en tête de script est devenu un réflexe quasi universel, copié d’un script à l’autre sans toujours comprendre ce que chacune des trois options attrape réellement, ni surtout ce qu’elles n’attrapent jamais. Ce faux sentiment de sécurité explique pourquoi un script qui commence par cette ligne continue parfois de s’exécuter après une erreur qu’on pensait couverte.
-e : arrêt sur erreur, avec des exceptions documentées mais oubliées
set -e arrête le script dès qu’une commande retourne un code de sortie non nul, mais cette règle a des exceptions précises que bash documente explicitement : une commande dans une condition (if, while, until), dans une chaîne && ou ||, ou niée par !, ne déclenche jamais l’arrêt, quel que soit son code de sortie.
set -e
if grep -q "pattern" file.txt; then
echo "trouvé"
fi
# grep qui échoue (pattern absent) n'arrête jamais le script :
# il est à l'intérieur d'une condition if
Cette exception est délibérée et documentée : sans elle, tester une commande dans un if deviendrait impossible, puisque l’échec attendu de ce test arrêterait systématiquement le script.
-u : les variables non définies devenues des erreurs
set -u transforme l’utilisation d’une variable jamais définie en erreur explicite plutôt qu’en substitution silencieuse par une chaîne vide, un comportement par défaut de bash qui masque des fautes de frappe ($FOOO au lieu de $FOO) pendant des années sans jamais les signaler.
set -u
echo "$UNDEFINED_VAR"
# bash: UNDEFINED_VAR: unbound variable
# Sans -u, cette ligne afficherait juste une ligne vide
Cette option révèle une classe entière de bugs de script qui, sans elle, produisent un comportement silencieusement incorrect (une variable vide traitée comme valide) plutôt qu’une erreur explicite au bon endroit.
pipefail : le code de sortie réel d’un pipeline
Sans pipefail, le code de sortie d’un pipeline (cmd1 | cmd2) est toujours celui de la dernière commande, cmd2, indépendamment de l’échec de cmd1. Un grep qui échoue au début d’un pipeline dont la dernière commande réussit laisse croire que tout le pipeline a réussi.
set -o pipefail
false | echo "toujours affiché"
# Sans pipefail : code de sortie 0 (celui de echo)
# Avec pipefail : code de sortie 1 (celui de false, la commande en échec)
pipefail capture le code de sortie de la première commande en échec dans le pipeline, pas seulement de la dernière, une correction indispensable pour tout script qui enchaîne des commandes via des pipes et vérifie ensuite le succès global.
Le piège qui survit même avec les trois options actives
Une fonction appelée à l’intérieur d’une condition (if my_function; then) désactive set -e pour toute la durée de son exécution, y compris pour des commandes internes à la fonction qui n’ont rien à voir avec la condition testée. Une erreur survenant à l’intérieur de cette fonction, sur une ligne sans rapport avec le test conditionnel voulu, passe silencieusement inaperçue, exactement le même mécanisme que l’exception de set -e dans un if simple, mais bien moins visible une fois masqué derrière un appel de fonction.
set -euo pipefail
my_function() {
rm important_file # échoue si le fichier n'existe pas
echo "toujours affiché malgré l'échec de rm ci-dessus"
}
if my_function; then
echo "succès"
fi
À retenir
set -e n’arrête jamais un script à l’intérieur d’un if, d’une chaîne &&/||, ou d’une fonction appelée en contexte conditionnel, des exceptions documentées mais rarement présentes à l’esprit au moment d’écrire le script. -u révèle les fautes de frappe de variable comme des erreurs explicites plutôt que des substitutions silencieuses ; pipefail capture le code de sortie de la première commande en échec d’un pipeline, pas seulement de la dernière. Ces trois options réduisent réellement une classe d’échecs silencieux, mais ne remplacent jamais une vérification explicite du code de sortie sur les points critiques d’un script, en particulier tout ce qui s’exécute à l’intérieur d’une fonction ou d’une condition, un des réflexes qui séparent un script réellement fiable d’un script qui se contente d’avoir la bonne ligne en en-tête, un détail qui compte à chaque étape d’une industrialisation CI/CD qui s’appuie sur des scripts shell.