Aller au contenu
Administration Linux medium

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

12 min de lecture

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 backslash dans un nom de fichier disparaît silencieusement. Quelques lignes de protection suffisent à transformer un script fragile en script fiable en production.

  • 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 silencieusement échouer 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 :

#!/bin/bash
echo "Fichier source :"
read src
echo "Fichier destination :"
read dst
mv $src $dst
echo "Déplacement effectué."

Quatre problèmes identifiables :

ProblèmeConséquence
read sans -rLes backslashes sont interprétés : \n devient un saut de ligne
$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

La suite corrige ces quatre problèmes.

Placez toujours cette ligne juste après le shebang :

Fenêtre de terminal
set -euo pipefail

Sans -e, un échec passe inaperçu :

#!/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 et n’affiche pas le message trompeur.

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

Sans -u, la variable vide serait substituée silencieusement — potentiellement dangereux dans rm -rf "$dir/".

-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 le 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 du premier échec.

read -r préserve les backslashes 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.

Validez chaque entrée avant toute opération destructive :

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'."

Tests courants :

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 un signal ou un pseudo-signal est reçu.

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.

trap ... EXIT s’exécute toujours, quelle que soit la cause de la sortie (succès, erreur, Ctrl+C) :

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 un lien symbolique avant vous vers un fichier sensible.

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 :

Fenêtre de terminal
tmpfile=$(mktemp)
trap 'rm -f "$tmpfile"' EXIT
grep "ERR\|WARN" /var/log/syslog > "$tmpfile"
echo "$(wc -l < "$tmpfile") alertes trouvées"

Ne jamais coder un mot de passe ou une clé directement dans le script :

Fenêtre de terminal
# À ne pas faire
DB_PASSWORD="monsecret"
mysql -u root -p"$DB_PASSWORD"

Avec une variable d’environnement — définie avant l’appel :

Fenêtre de terminal
# Dans le script : on lit la variable d'environnement
mysql -u root -p"$DB_PASSWORD"
Fenêtre de terminal
# Avant d'exécuter le script
export DB_PASSWORD="monsecret"
./mon_script.sh

Avec un fichier de configuration à accès restreint :

Fenêtre de terminal
# config.conf — contient les secrets, jamais versionné
DB_PASSWORD="monsecret"
Fenêtre de terminal
chmod 600 config.conf
source config.conf
mysql -u root -p"$DB_PASSWORD"

Voici deplacement_robuste.sh, version corrigée de deplacement.sh :

#!/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 -.

shellcheck analyse statiquement un script et signale les mauvaises pratiques avant l’exécution :

Fenêtre de terminal
sudo apt install shellcheck # Debian/Ubuntu
shellcheck deplacement.sh
In deplacement.sh line 7:
read src
^-- SC2162: read without -r will mangle backslashes.
In deplacement.sh line 13:
mv $src $dst
^---^ SC2086: Double quote to prevent globbing and word splitting.

Chaque code (SC2162, SC2086) pointe vers une explication détaillée sur shellcheck.net/wiki/SCxxxx.

shfmt reformate le code source de manière cohérente :

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
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 pas-e non activé ou trap placé après l’erreurMettre set -euo pipefail et trap en tête de script
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.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn