Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Scripts Bash robustes : set -euo pipefail, trap et shellcheck

Par mis à jour le

Améliorer cette page

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).

  • Activer set -euo pipefail pour arrêter le script dès la première erreur
  • Utiliser read -r et 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

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 mv raté et marque l'opération comme réussie
  • un script de déploiement qui lit $CONFIG_FILE vide parce que la variable n'a pas été exportée
  • un pipeline cat log | grep ERR | wc -l qui retourne 0 même si cat a échoué
  • un fichier temporaire prévisible dans /tmp remplacé 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.

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/bash
echo "Fichier source :"
read src
echo "Fichier destination :"
read dst
mv $src $dst
echo "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èmeConséquence
read sans -rL'antislash est retiré : la saisie a\nb devient anb (mesuré)
$src et $dst sans guillemetsUn espace dans le nom scinde l'argument en plusieurs mots
Pas de validationSi le fichier source n'existe pas, mv échoue mais "Déplacement effectué." s'affiche quand même
Pas de set -eUn code de retour non nul ne stoppe pas l'exécution

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 :

Fenêtre de terminal
set -euo pipefail

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/bash
rm /fichier/inexistant
echo "Nettoyage terminé." # s'affiche quand même
rm: cannot remove '/fichier/inexistant': No such file or directory
Nettoyage 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.

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 bash
set -euo pipefail
n=0
((n++)) # code retour 1 (ancienne valeur = 0), le script meurt ici
echo "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 :

Fenêtre de terminal
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

Avec -u, lire une variable jamais définie arrête le script au lieu de la remplacer par une chaîne vide :

Fenêtre de terminal
set -u
echo "Répertoire : $REPERTOIRE"
bash: REPERTOIRE: unbound variable

Sans -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 :

Fenêtre de terminal
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.

read -r préserve les antislashs tels quels. Les guillemets doubles protègent contre le word splitting et le globbing :

Fenêtre de terminal
read -r src # -r : backslashes préservés
mv "$src" "$dst" # guillemets : un seul argument même avec des espaces

Sans 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.

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 :

Fenêtre de terminal
[[ -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 :

TestSignification
-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 exécute une commande quand le script reçoit un signal (INT, TERM) ou un pseudo-signal de Bash (ERR, EXIT, DEBUG).

Fenêtre de terminal
erreur() {
echo "Erreur : $1" >&2
exit 1
}
trap 'erreur "Interruption inattendue à la ligne $LINENO."' ERR

Quand 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.

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é :

Fenêtre de terminal
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
# Le fichier est supprimé même si le script échoue en cours de route

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.

Fenêtre de terminal
tmpfile=$(mktemp) # Crée /tmp/tmp.xK3pQr (nom unique, permissions 600)
tmprepertoire=$(mktemp -d) # Crée un répertoire temporaire

Associé à 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 :

Fenêtre de terminal
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
journalctl -p warning -b --no-pager > "$tmpfile"
echo "$(wc -l < "$tmpfile") lignes d'avertissement trouvées"

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.

Fenêtre de terminal
# À ne pas faire : secret en dur, puis en argument
DB_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 :

Fenêtre de terminal
# config.conf : lisible par son seul propriétaire, jamais versionné
DB_PASSWORD="monsecret"
Fenêtre de terminal
chmod 600 config.conf
source config.conf

Ne 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=sauvegarde
password=monsecret
Fenêtre de terminal
mysql --defaults-extra-file=/etc/sauvegarde/mysql.cnf

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/bash
set -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 0

Le 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 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.

Fenêtre de terminal
shellcheck deplacement.sh

Sur 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) :

Fenêtre de terminal
sudo apt install shfmt
shfmt -i 4 -w mon_script.sh # indentation 4 espaces, réécriture en place
shfmt -d mon_script.sh # affiche les différences sans modifier

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ômeCause probableSolution
Le script s'arrête à une commande bénigne-e interrompt sur tout code non nulProté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 pasCommande testée par if, && ou ||, trap posé après l'erreur, ou échec dans une fonction sans set -EPoser trap en tête de script, ajouter set -E pour les fonctions
mktemp: failed to create file/tmp plein ou monté en readonlydf /tmp puis nettoyer les fichiers orphelins
Script fonctionne en interactif mais pas en cronPATH différent dans cronUtiliser les chemins complets (/bin/mv, /usr/bin/grep)
  • set -euo pipefail est la première ligne après le shebang dans tout script de production.
  • read -r pré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' ERR intercepte les erreurs inattendues ; trap 'cmd' EXIT garantit le nettoyage.
  • mktemp gé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.

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

6 questions
6 min.
70% requis

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

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn