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

Déboguer un script Bash : bash -x, set -x, shellcheck

Par mis à jour le

Améliorer cette page

Déboguer un script Bash commence toujours par la même question : est-ce un problème de syntaxe, de logique ou d'environnement ? Bash propose plusieurs niveaux de diagnostic, de la validation syntaxique (bash -n) à la trace ligne par ligne (bash -x), en passant par l'analyse statique (shellcheck) et les pièges réactifs (trap ERR). Choisir le bon outil pour le bon niveau d'erreur fait gagner un temps considérable. Les sorties de cette page ont été mesurées sur Bash 5.2.37 et shellcheck 0.10 (Debian 13).

  • Valider la syntaxe d'un script sans l'exécuter avec bash -n
  • Activer une trace d'exécution ligne par ligne avec bash -x et set -x/set +x
  • Personnaliser le préfixe de trace avec PS4 pour y inclure le nom du fichier et la ligne
  • Rediriger la trace vers un fichier séparé avec BASH_XTRACEFD
  • Réagir aux erreurs avec trap ERR et inspecter chaque instruction avec trap DEBUG
  • Analyser statiquement un script avec shellcheck
  • Identifier les pièges classiques : (( )) avec set -e, SIGPIPE, set -u et variables vides

Le débogage Bash intervient dans plusieurs situations fréquentes :

  • un script fonctionnel en local échoue silencieusement en production, la trace bash -x révèle où il dévie
  • un script présente une erreur de syntaxe après modification, bash -n l'identifie sans exécuter le code
  • un script de déploiement continue après une commande en échec sans signal apparent, trap ERR intercepte la rupture
  • un collègue vous remet un script non documenté à corriger, shellcheck liste les problèmes automatiquement
  • un script écrit à la hâte doit être corrigé vite, par exemple sous la contrainte de temps d'un examen pratique

bash -n lit le script, vérifie la syntaxe et s'arrête. Aucune commande n'est exécutée, et le code de retour vaut 2 en cas d'erreur (mesuré), ce qui le rend utilisable dans un contrôle automatique.

# Script avec une erreur de syntaxe (fi manquant)
$ cat test.sh
#!/bin/bash
if true
then
echo "ok"
# fi manquant
$ bash -n test.sh
test.sh: line 6: syntax error: unexpected end of file
Fenêtre de terminal
# Script syntaxiquement valide
$ bash -n mon_script.sh && echo "Syntaxe OK"
Syntaxe OK

Utilisez bash -n avant toute exécution sur un serveur de production : c'est gratuit et sans risque. Il ne voit en revanche que la syntaxe, pas une commande mal nommée ni une variable vide.

bash -x exécute le script et affiche chaque commande après expansion des variables, juste avant de l'exécuter. La trace part sur la sortie d'erreur, préfixée par +.

Fenêtre de terminal
$ bash -x mon_script.sh
+ a=42
+ b=84
+ echo 'Résultat : 84'
Résultat : 84

Chaque ligne préfixée par + est une commande réellement exécutée, les variables y sont déjà substituées. C'est la première chose à faire quand un script produit un résultat inattendu.

set -x active la trace à partir d'un point, set +x la désactive : seule la section encadrée apparaît dans la sortie :

#!/bin/bash
set -euo pipefail
# Code sans trace...
prepare_env
# Activer la trace uniquement sur la section critique
set -x
resultat=$(traitement_complexe "$1")
set +x
echo "Résultat : $resultat"

Le nombre de + indique le niveau de substitution : chaque $( ) imbriqué en ajoute un. Un appel de fonction n'en ajoute pas, pas plus qu'un sous-shell ( ) écrit explicitement. Mesuré sur ce script de cinq lignes :

Fenêtre de terminal
f() { echo "dans f"; }
f
x=$(echo sub)
( echo "sous-shell" )
y=$(echo $(echo double))
+ f
+ echo 'dans f'
dans f
++ echo sub
+ x=sub
+ echo sous-shell
sous-shell
+++ echo double
++ echo double
+ y=double

Le echo de la fonction reste à un seul +, celui de la substitution passe à ++, et la substitution imbriquée à +++.

Par défaut, PS4='+ '. Vous pouvez l'enrichir avec le nom du fichier et le numéro de ligne, grâce aux variables BASH_SOURCE et LINENO évaluées à chaque ligne de trace :

Fenêtre de terminal
export PS4='[${BASH_SOURCE##*/}:${LINENO}] '
set -x
msg="bonjour"
echo "$msg"
set +x

Sortie :

[demo-debug.sh:35] msg=bonjour
[demo-debug.sh:36] echo bonjour
bonjour
[demo-debug.sh:37] set +x

Chaque ligne de trace indique maintenant le fichier et le numéro de ligne, indispensable quand des fonctions provenant de plusieurs fichiers sourcés sont impliquées.

Quand la trace se mélange aux sorties du script, la lisibilité en souffre. BASH_XTRACEFD désigne le descripteur de fichier qui reçoit la trace : exec {BASH_XTRACEFD}>debug.log en ouvre un, et range son numéro dans la variable elle-même :

#!/bin/bash
# Rediriger la trace vers debug.log
exec {BASH_XTRACEFD}>debug.log
set -x
calcul=$((6 * 7))
resultat=$(date '+%Y%m%d')
set +x
exec {BASH_XTRACEFD}>&- # fermer le descripteur
echo "calcul=$calcul, date=$resultat"
# La sortie standard est propre ; debug.log contient la trace
$ cat debug.log
+ calcul=42
++ date +%Y%m%d
+ resultat=20260929
+ set +x

La trace complète est bien dans debug.log, substitution date comprise, et la sortie standard reste propre. Cette technique est particulièrement utile pour les scripts exécutés en cron ou dans une CI, où la trace ne doit pas parasiter les logs applicatifs.

trap 'commande' ERR exécute commande à chaque fois qu'une commande se termine avec un code non nul, avec ou sans set -e (mesuré). Avec set -e, le gestionnaire s'exécute juste avant la sortie :

#!/bin/bash
set -euo pipefail
gestion_erreur() {
echo "ERREUR à la ligne $1, code de sortie : $2" >&2
echo "Script abandonné." >&2
}
trap 'gestion_erreur $LINENO $?' ERR
commande_qui_peut_echouer

trap 'commande' DEBUG exécute commande avant chaque instruction, utile pour compter les exécutions ou suivre l'état d'une variable :

#!/bin/bash
# Afficher le numéro de ligne avant chaque instruction
trap 'echo " → ligne $LINENO"' DEBUG
x=1
y=2
z=$((x + y))
trap - DEBUG # désactiver
echo "z=$z"

En pratique, trap DEBUG sert surtout à instrumenter une section suspecte, pas à tout tracer : sur un script entier, la sortie devient vite illisible.

shellcheck analyse un script sans l'exécuter et signale les erreurs, les pratiques dangereuses et les incompatibilités POSIX. Il s'installe avec apt install shellcheck sur Debian et Ubuntu, depuis EPEL (paquet ShellCheck) sur RHEL :

Fenêtre de terminal
$ shellcheck mon_script.sh

Voici quatre défauts qu'il détecte, chacun en paire à éviter / correct. Les lignes marquées # à éviter sont des contre-exemples volontaires :

Fenêtre de terminal
# SC2086 : variable non quotée, word splitting et globbing
rm $fichier # à éviter
rm "$fichier" # correct
# SC2046 : résultat de $() non quoté
cp $(ls *.log) /backup/ # à éviter
cp ./*.log /backup/ # correct, ou via un tableau
# SC2068 : tableau développé sans guillemets doubles
cmd ${tableau[@]} # à éviter
cmd "${tableau[@]}" # correct
# SC2155 : déclaration et affectation combinées avec local
local val=$(commande) # à éviter : masque le code de retour
local val; val=$(commande) # correct

Attention à une variante fréquente du troisième cas : $tableau[@], sans accolades, n'est pas un tableau mal quoté mais une autre erreur. shellcheck la classe en SC1087 (accolades manquantes) et SC2128 : Bash ne développe que le premier élément, suivi du texte [@].

(( n++ )) retourne le code de sortie 1 quand le résultat de l'expression vaut 0, ce qui déclenche set -e. Avec n=0, le post-incrément vaut justement 0 :

Fenêtre de terminal
set -e
n=0
(( n++ )) # résultat = 0 → exit code 1 → set -e stoppe le script !
echo "n=$n" # jamais exécuté

La solution est une affectation, dont le code de retour vaut toujours 0, quelle que soit la valeur calculée :

Fenêtre de terminal
set -e
n=0
n=$((n + 1)) # exit code 0 quelle que soit la valeur
echo "n=$n" # n=1

Les tests arithmétiques demandent plus de nuance. Au milieu d'un script, (( a == 0 )) && echo "zéro" n'arrête pas le script quand a vaut 1 : une commande testée par && est exemptée de set -e (mesuré). Le piège se déclenche quand cette ligne est la dernière d'une fonction : la fonction retourne alors 1, et son appel, lui, arrête le script.

Fenêtre de terminal
set -e
f() { (( a == 0 )) && echo "zéro"; } # retourne 1 quand a vaut 1
a=1
f # le script s'arrête ici, code 1
echo "jamais affiché"

La forme if (( a == 0 )); then echo "zéro"; fi évite le problème : un if dont la condition est fausse retourne 0, et la fonction avec lui.

Quand head a lu ce qu'il voulait, il ferme le pipe ; la commande amont qui écrit encore reçoit SIGPIPE et sort avec le code 141. Deux conditions doivent être réunies pour que le script s'arrête, et la mesure le montre : pipefail doit être actif (sans lui, seul le code de head compte), et l'amont doit encore écrire quand head ferme. Cent lignes tiennent dans le tampon du pipe et ne déclenchent rien ; un million, si.

Fenêtre de terminal
set -eo pipefail
for i in {1..100}; do echo "$i"; done | head -5 # continue : l'amont a déjà fini
seq 1 1000000 | head -5 # le script s'arrête, code 141
echo "jamais affiché"

Le défaut est intermittent par nature : il dépend du volume et de la vitesse des deux côtés du pipe. Quand la troncature est voulue, neutralisez explicitement le code de ce seul pipeline :

Fenêtre de terminal
set -eo pipefail
seq 1 1000000 | head -5 || true # code 141 attendu, neutralisé ici seulement

set -u stoppe le script sur toute variable non définie, au lieu de la remplacer par une chaîne vide sans prévenir :

Fenêtre de terminal
set -u
echo "$undefined" # bash: undefined: unbound variable → exit 1

Pour une variable réellement optionnelle, fournissez une valeur par défaut : l'expansion ${VAR:-défaut} ne déclenche pas l'erreur :

Fenêtre de terminal
set -u
nom="${USER_NAME:-anonyme}" # OK même si USER_NAME n'est pas défini
echo "Bonjour $nom"

Un chemin contenant un espace casse une commande dès que la variable n'est pas quotée. La correction consiste à quoter la variable et à laisser le glob hors des guillemets :

Fenêtre de terminal
repertoire="/chemin/avec espaces"
# Problème : word splitting + globbing
cp $repertoire/*.log /backup/
# Correct : guillemets doubles + glob explicite
cp "$repertoire"/*.log /backup/

Le bon outil dépend du symptôme. Ce tableau résume la page : partez de ce que vous observez, et appliquez l'outil de la même ligne avant de passer au suivant.

SymptômeOutil recommandéAction
Erreur de syntaxe au lancementbash -nIdentifier la ligne exacte
Résultat inattendubash -xLire les expansions après +
Erreur dans une section préciseset -x / set +xEncadrer la section
Plusieurs fichiers sourcésPS4 avec BASH_SOURCE:LINENOLocaliser la ligne exacte
Script en CI / sans terminalBASH_XTRACEFDSéparer trace et sortie
Code de style et bugs connusshellcheckAnalyse statique complète
Script s'arrête sans messagetrap ERRAfficher ligne + code de sortie
  • bash -n valide la syntaxe sans exécuter, à utiliser systématiquement avant un déploiement.
  • bash -x trace chaque commande après expansion : c'est le premier réflexe face à un résultat inattendu.
  • set -x / set +x ciblent la trace sur une section précise du script.
  • PS4='[${BASH_SOURCE##*/}:${LINENO}] ' rend la trace exploitable dans un script multi-fichiers.
  • trap 'handler $LINENO $?' ERR capture toutes les erreurs non gérées.
  • shellcheck détecte les problèmes courants avant l'exécution, intégrez-le dans votre workflow.
  • Avec set -e, évitez (( n++ )) quand n=0, utilisez n=$((n + 1)).

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