Scripting Bash : Plongée dans les Codes de Sortie et le Statut

Comprenez les codes de sortie Bash, inspectez $? en toute sécurité, définissez des statuts avec exit et construisez un flux de contrôle fiable.

Scripting Bash : Plongée dans les Codes de Sortie et le Statut

Les codes de sortie Bash sont la façon dont les commandes indiquent à votre script ce qui s'est passé. 0 signifie succès, et un statut non nul signifie que la commande a échoué ou a produit un résultat que votre script doit gérer.

Ce guide vous montre comment lire $?, définir des statuts avec exit et utiliser les codes de sortie pour construire un flux de contrôle plus sûr dans l'automatisation Bash.

Comprendre les Codes de Sortie

Chaque commande, fonction ou script exécuté dans Bash renvoie un code de sortie à la fin. Il s'agit d'une valeur entière qui signale le résultat de l'exécution. Par convention :

  • 0 (Zéro) : Indique un succès. La commande s'est terminée sans erreur.
  • Non nul (Tout autre entier) : Indique un échec ou une erreur. Différentes valeurs non nulles peuvent parfois signifier des types d'erreurs spécifiques.

Cette simple convention 0 vs non nul est fondamentale pour le fonctionnement de Bash et pour la construction de logique conditionnelle dans vos scripts.

Récupérer le Dernier Code de Sortie : $?

Bash fournit un paramètre spécial, $?, qui contient le code de sortie de la commande de premier plan la plus récemment exécutée. Vous pouvez vérifier sa valeur immédiatement après n'importe quelle commande pour déterminer son résultat.

# Exemple 1 : Commande réussie
ls /tmp
echo "Code de sortie pour 'ls /tmp' : $?"

# Exemple 2 : Commande échouée (répertoire inexistant)
ls /repertoire_inexistant
echo "Code de sortie pour 'ls /repertoire_inexistant' : $?"

# Exemple 3 : Grep trouve une correspondance (succès)
grep "root" /etc/passwd
echo "Code de sortie pour 'grep root /etc/passwd' : $?"

# Exemple 4 : Grep ne trouve pas de correspondance (échec, mais attendu)
grep "utilisateur_inexistant" /etc/passwd
echo "Code de sortie pour 'grep utilisateur_inexistant /etc/passwd' : $?"

Sortie (peut varier légèrement selon votre système et le contenu de /etc/passwd) :

ls /tmp
# ... (liste des fichiers dans /tmp)
Code de sortie pour 'ls /tmp' : 0
ls /repertoire_inexistant
ls: impossible d'accéder à '/repertoire_inexistant': Aucun fichier ou dossier de ce type
Code de sortie pour 'ls /repertoire_inexistant' : 2
grep "root" /etc/passwd
root:x:0:0:root:/root:/bin/bash
Code de sortie pour 'grep root /etc/passwd' : 0
grep "utilisateur_inexistant" /etc/passwd
Code de sortie pour 'grep utilisateur_inexistant /etc/passwd' : 1

Remarquez que grep renvoie 0 pour une correspondance et 1 pour aucune correspondance. Les deux sont des résultats valides dans le contexte de grep, mais pour la logique conditionnelle, 0 signifie la découverte réussie du motif.

Définir Explicitement les Codes de Sortie avec exit

Lorsque vous écrivez vos propres scripts ou fonctions, vous pouvez définir explicitement leur code de sortie en utilisant la commande exit suivie d'une valeur entière. Ceci est crucial pour communiquer le résultat du script aux processus appelants, aux scripts parents ou aux pipelines CI/CD.

#!/bin/bash

# script_succes.sh
echo "Ce script se terminera avec succès (0)"
exit 0
#!/bin/bash

# script_echec.sh
echo "Ce script se terminera avec un échec (1)"
exit 1
# Tester les scripts
./script_succes.sh
echo "Statut de script_succes.sh : $?"

./script_echec.sh
echo "Statut de script_echec.sh : $?"

Sortie :

Ce script se terminera avec succès (0)
Statut de script_succes.sh : 0
Ce script se terminera avec un échec (1)
Statut de script_echec.sh : 1

Astuce : Si exit est appelé sans argument, le statut de sortie du script sera le statut de sortie de la dernière commande exécutée avant que exit ne soit appelé.

Exploiter les Codes de Sortie pour le Flux de Contrôle

Les codes de sortie sont la base de l'exécution conditionnelle dans Bash, vous permettant de créer des scripts dynamiques et réactifs.

Instructions Conditionnelles (if/else)

L'instruction if dans Bash évalue le code de sortie d'une commande. Si la commande se termine avec 0 (succès), le bloc if est exécuté. Sinon, le bloc else (s'il est présent) est exécuté.

#!/bin/bash

FICHIER="/chemin/vers/mon/fichier_important.txt"

if [ -f "$FICHIER" ]; then # La commande test `[` renvoie 0 si le fichier existe
    echo "Le fichier '$FICHIER' existe. Poursuite du traitement..."
    # Ajoutez la logique de traitement du fichier ici
    # Exemple : cat "$FICHIER"
    exit 0
else
    echo "Erreur : Le fichier '$FICHIER' n'existe pas."
    echo "Abandon du script."
    exit 1
fi

Opérateurs Logiques (&&, ||)

Bash fournit des opérateurs logiques de court-circuit puissants qui dépendent des codes de sortie :

  • commande1 && commande2 : commande2 est exécutée uniquement si commande1 se termine avec 0 (succès).
  • commande1 || commande2 : commande2 est exécutée uniquement si commande1 se termine avec une valeur non nulle (échec).

Ceux-ci sont extrêmement utiles pour les commandes séquentielles et les mécanismes de repli.

#!/bin/bash

REP_LOG="/var/log/mon_app"

# Créer le répertoire seulement s'il n'existe pas
mkdir -p "$REP_LOG" && echo "Répertoire de logs '$REP_LOG' assuré."

# Essayer de démarrer un service, s'il échoue, essayer une commande de repli
systemctl start mon_service || { echo "Échec du démarrage de mon_service. Tentative de repli..."; ./lancer_repli.sh; }

# Une commande qui doit réussir pour que le script continue
copier_donnees_vers_emplacement_sauvegarde && echo "Sauvegarde des données réussie." || { echo "Échec de la sauvegarde des données !"; exit 1; }

echo "Script terminé avec succès."
exit 0

set -e : Quitter en Cas d'Erreur

L'option set -e est un outil puissant pour rendre vos scripts plus robustes. Lorsque set -e est actif, Bash quittera immédiatement le script si une commande se termine avec un statut non nul. Cela évite les échecs silencieux et les erreurs en cascade.

#!/bin/bash
set -e # Quitter immédiatement si une commande se termine avec un statut non nul

echo "Démarrage du script..."

# Cette commande réussira
ls /tmp

echo "Première commande réussie."

# Cette commande échouera, et à cause de 'set -e', le script quittera ici
ls /chemin_inexistant

echo "Cette ligne ne sera jamais atteinte si la commande précédente a échoué."

exit 0 # Cette ligne ne sera atteinte que si toutes les commandes précédentes ont réussi

Sortie (si /chemin_inexistant n'existe pas) :

Démarrage du script...
# ... (sortie de ls /tmp)
Première commande réussie.
ls: impossible d'accéder à '/chemin_inexistant': Aucun fichier ou dossier de ce type

Le script se termine après la commande ls échouée, et le message "Cette ligne ne sera jamais atteinte" n'est pas affiché.

Avertissement : set -e a des exceptions, et certaines commandes renvoient légitimement un non nul pour des résultats attendus. Par exemple, grep renvoie 1 lorsqu'il ne trouve aucune correspondance. Préférez un if grep -q "motif" fichier; then ... fi explicite lorsque le résultat vous importe.

Scénarios Courants de Codes de Sortie et Bonnes Pratiques

Bien que 0 pour le succès et non nul pour l'échec soit la règle générale, certains codes non nuls ont des significations courantes, en particulier pour les commandes système et les commandes internes :

  • 0 : Succès.
  • 1 : Erreur générale, fourre-tout pour divers problèmes.
  • 2 : Mauvaise utilisation des commandes internes du shell ou arguments de commande incorrects.
  • 126 : La commande invoquée ne peut pas être exécutée (par exemple, problème de permissions, pas un exécutable).
  • 127 : Commande non trouvée (par exemple, faute de frappe dans le nom de la commande, pas dans le PATH).
  • 128 + N : La commande a été terminée par le signal N. Par exemple, 130 (128 + 2) signifie que la commande a été terminée par SIGINT (Ctrl+C).

Lorsque vous créez vos propres scripts, tenez-vous en à 0 pour le succès. Pour les échecs, 1 est une valeur par défaut sûre pour une erreur générale. Si votre script gère plusieurs conditions d'erreur distinctes, vous pouvez utiliser des valeurs non nulles plus élevées (par exemple, 10, 20, 30) pour les différencier, mais documentez clairement ces codes personnalisés.

Bonnes Pratiques pour un Script Robuste :

  1. Vérifiez Toujours les Commandes Critiques : Ne supposez pas le succès. Utilisez des instructions if ou && pour vérifier les étapes critiques.
  2. Fournissez des Messages d'Erreur Informatifs : Lorsqu'un script échoue, imprimez des messages clairs sur stderr expliquant ce qui a mal tourné et comment le réparer potentiellement. Utilisez >&2 pour rediriger la sortie vers l'erreur standard.
    ma_commande || { echo "Erreur : ma_commande a échoué. Vérifiez les logs." >&2; exit 1; }
    
  3. Nettoyez en Cas d'Échec : Utilisez trap pour garantir que les fichiers temporaires ou les ressources sont nettoyés même si le script se termine prématurément.
    nettoyage() {
        echo "Nettoyage des fichiers temporaires..."
        rm -f /tmp/mon_fichier_temp_$$
    }
    trap nettoyage EXIT
    
  4. Validez les Entrées : Vérifiez les arguments du script ou les variables d'environnement tôt et quittez avec une erreur informative s'ils sont invalides.
  5. Enregistrez le Statut de Sortie : Pour une automatisation complexe, enregistrez le statut de sortie des opérations clés à des fins d'audit et de débogage.

Exemple Concret : Un Extrait de Script de Sauvegarde Robuste

Voici comment vous pourriez combiner ces concepts dans un scénario pratique :

#!/bin/bash
set -e # Quitter immédiatement si une commande se termine avec un statut non nul

SOURCE_SAUVEGARDE="/data/app/config"
DEST_SAUVEGARDE="/mnt/backup/configs"
HORODATAGE=$(date +%Y%m%d%H%M%S)
FICHIER_LOG="/var/log/sauvegarde_config_${HORODATAGE}.log"

# --- Fonctions ---
log_message() {
    echo "$(date +%Y-%m-%d_%H:%M:%S) - $1" | tee -a "$FICHIER_LOG"
}

nettoyage() {
    log_message "Nettoyage initié."
    if [ -n "${REP_TEMP:-}" ] && [ -d "$REP_TEMP" ]; then
        rm -rf "$REP_TEMP"
        log_message "Répertoire temporaire supprimé : $REP_TEMP"
    fi
}

# --- Trap pour la sortie et les signaux ---
trap 'nettoyage' EXIT
trap 'log_message "Script interrompu (SIGINT). Sortie."; exit 130' INT
trap 'log_message "Script terminé (SIGTERM). Sortie."; exit 143' TERM

# --- Logique Principale du Script ---
log_message "Démarrage de la sauvegarde de configuration."

# 1. Vérifier si le répertoire source existe
if [ ! -d "$SOURCE_SAUVEGARDE" ]; then
    log_message "Erreur : La source de sauvegarde '$SOURCE_SAUVEGARDE' n'existe pas." >&2
    exit 2 # Code d'erreur personnalisé pour source invalide
fi

# 2. Assurer que la destination de sauvegarde existe
mkdir -p "$DEST_SAUVEGARDE" || {
    log_message "Erreur : Échec de la création/validation de la destination de sauvegarde '$DEST_SAUVEGARDE'." >&2
    exit 3 # Code d'erreur personnalisé pour problème de destination
}

# 3. Créer un répertoire temporaire pour la compression
REP_TEMP=$(mktemp -d)
log_message "Répertoire temporaire créé : $REP_TEMP"

# 4. Copier les données dans le répertoire temporaire
cp -r "$SOURCE_SAUVEGARDE" "$REP_TEMP/" || {
    log_message "Erreur : Échec de la copie des données de '$SOURCE_SAUVEGARDE' vers '$REP_TEMP'." >&2
    exit 4 # Code d'erreur personnalisé pour échec de copie
}
log_message "Données copiées vers l'emplacement temporaire."

# 5. Compresser les données
NOM_ARCHIVE="sauvegarde_config_${HORODATAGE}.tar.gz"
tar -czf "$REP_TEMP/$NOM_ARCHIVE" -C "$REP_TEMP" "$(basename "$SOURCE_SAUVEGARDE")" || {
    log_message "Erreur : Échec de la compression des données." >&2
    exit 5 # Code d'erreur personnalisé pour échec de compression
}
log_message "Données compressées dans $NOM_ARCHIVE."

# 6. Déplacer l'archive vers la destination finale
mv "$REP_TEMP/$NOM_ARCHIVE" "$DEST_SAUVEGARDE/" || {
    log_message "Erreur : Échec du déplacement de l'archive vers '$DEST_SAUVEGARDE'." >&2
    exit 6 # Code d'erreur personnalisé pour échec de déplacement
}
log_message "Archive déplacée vers '$DEST_SAUVEGARDE/$NOM_ARCHIVE'."

log_message "Sauvegarde terminée avec succès !"
exit 0

À Retenir

Traitez les codes de sortie comme faisant partie de l'interface de votre script. Vérifiez les commandes critiques, renvoyez des statuts non nuls clairs en cas d'échec et documentez les codes personnalisés qu'un autre script ou un travail CI pourrait avoir besoin d'interpréter.