Observer les codes de retour
Comprendre et exploiter les codes de retour d’une commande.
Objectifs
À la fin de cette leçon, vous saurez :
- expliquer ce qu'est le code de retour d'un processus et pourquoi il existe ;
- lire le code de la dernière commande avec
echo $?; - relier codes de retour et messages d'erreur (deux canaux, deux publics) ;
- utiliser
&&,||et;en pleine connaissance de cause.
🔗 Pour vous rafraîchir la mémoire : observer les processus · redirections et pipes
Le secret que chaque commande garde jusqu'à sa mort
Reprenons le fil de la leçon 2-01. Quand un processus se termine, le noyau détruit sa RAM, ferme ses canaux... mais conserve une dernière trace : un petit entier que le processus a rendu en partant — son code de retour (exit status). Ce code est posé dans une variable spéciale du shell, notée $?, et reste là jusqu'à la prochaine commande.
Vérifions :
$ ls /etc
acpi hosts network systemd
apt hostname passwd ...
$ echo $?
0
ls a réussi : son code est 0. Maintenant :
$ ls /dossier-inexistant
ls: impossible d'accéder à '/dossier-inexistant' : Aucun fichier ou dossier de ce type
$ echo $?
2
Même commande, deux destins : 0 quand tout s'est bien passé, 2 ici parce que l'argument pointe nulle part.
La règle : zéro signifie « succès »
La convention universelle des systèmes Unix :
0 = succès (la tâche demandée a été accomplie)
toute valeur = échec (quelque chose n'a pas fonctionné)
non nulle
Pourquoi cette convention inversée en apparence ? Pour la même raison qu'un interrupteur a une position « tout va bien » unique mais plusieurs façons de tomber en panne : il y a une seule façon de réussir, et mille raisons d'échouer. Un seul code suffit pour dire « OK » ; les autres valeurs servent à distinguer les familles d'échecs. Quelques valeurs courantes :
| Code | Signification habituelle | Exemple |
|---|---|---|
| 0 | succès | ls /etc |
| 1 | erreur générique | which programme-inexistant |
| 2 | mauvaise utilisation | ls /inexistant ; options malformées |
| 127 | commande introuvable | taper nodejs alors que seul node existe |
| 130 | interrompu par Ctrl+C | arrêter top avec Ctrl+C puis $? |
Les valeurs exactes peuvent varier selon les programmes ; la frontière qui compte vraiment est zéro / non-zéro.
Message d'erreur et code de retour : deux audiences différentes
Regardez bien l'échec de tout à l'heure : il a produit deux choses distinctes.
- Le message à l'écran (
impossible d'accéder à ...) : écrit sur stderr, destiné à vous, pour comprendre et corriger. - Le code de retour (
2) : invisible, destiné aux programmes qui lancent d'autres programmes.
Cette séparation est une conséquence directe du chapitre précédent : un script ne peut pas lire l'écran, mais il peut parfaitement lire un code. C'est ainsi que l'automatisation devient possible : au lieu d'analyser du texte, on teste un nombre.
Vous connaissez déjà ces tests sans le savoir — ils ont été vus en passant au chapitre 1 :
$ which node && echo "node est prêt"
/usr/bin/node
node est prêt
$ which curl || echo "curl manquant"
curl manquant
Maintenant vous savez exactement comment ça marche : && exécute la suite si le code valait 0 ; || si le code était non nul. Rien de magique — juste la lecture du dernier code de retour.
Un cas intéressant : grep utilise ses codes pour autre chose qu'une erreur technique :
$ grep root /etc/passwd # trouvé → 0
root:x:0:0:root:/root:/bin/bash
$ echo $?
0
$ grep zzzz /etc/passwd # rien trouvé → 1 (pas une panne : réponse négative)
$ echo $?
1
« Code non nul » ne veut pas toujours dire « panne » : parfois c'est simplement « non ». Lisez la doc quand le sens compte.
Observer depuis Node.js
Un programme peut choisir son propre code de départ. Créez verifie.js :
const fs = require("fs");
if (fs.existsSync(process.argv[2])) {
console.log("présent");
process.exit(0); // je déclare : mission accomplie
} else {
console.error("absent : " + process.argv[2]);
process.exit(1); // je déclare : échec
}
Exécutez-le dans les deux cas :
$ node verifie.js /etc/hosts
présent
$ echo $?
0
$ node verifie.js /rien.txt
absent : /rien.txt
$ echo $?
1
Notez le raffinement : le message d'échec part sur stderr (console.error), le résultat sur stdout — les canaux de la leçon 1-05 appliqués consciemment. Votre script se comporte désormais comme un vrai outil Unix : message pour l'humain, code pour la machine.
La chaîne complète : && et || lisent, votre script parle
Tout s'assemble enfin :
node verifie.js /etc/hosts && echo "on peut continuer"
node verifie.js /rien.txt || echo "fichier absent, on gère"
Et c'est exactement ce mécanisme qui fera tourner vos pipelines CI plus tard (« build && test && deploy ») : chaque étape vérifie le code de retour de la précédente avant d'oser continuer.
⚠️ Une subtilité de lecture : $? ne vit que jusqu'à la commande suivante. Après un echo $?, c'est le code du echo (donc 0) qui occupe la place. Observez toujours immédiatement après la commande visée.
Exercice
- Exécutez
ls /etcpuisecho $?. - Exécutez
ls /etc/zazouxpuisecho $?. Comparez. - Testez
cd /puisecho $?, puiscd /impossibleetecho $?. - Tapez une commande qui n'existe pas (par exemple
pouet), puis lisez$?. Quel code obtenez-vous ? - Écrivez une ligne qui affiche « réseau ok » si
which curlréussit, « curl manquant » sinon. - Question de réflexion : pourquoi une commande qui n'affiche rien peut avoir un code 0 ?
Résumé
- Chaque processus rend un entier en mourant : 0 = succès, ≠ 0 = échec.
$?contient le code de la dernière commande, jusqu'à la suivante.- Message d'erreur = pour l'humain (stderr) ; code de retour = pour les programmes.
&&enchaîne sur succès,||sur échec,;sans condition — toute l'automatisation repose là-dessus.- Un programme choisit son code (
process.exit(n)), et peut ainsi devenir brique d'une chaîne.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Exercices 1 et 2.
$ ls /etc ; echo $?
(liste des fichiers)
0
$ ls /etc/zazoux ; echo $?
ls: impossible d'accéder à '/etc/zazoux' : Aucun fichier ou dossier de ce type
2
Même programme, deux codes : le chemin existant donne 0, le chemin fantôme donne 2 (mauvaise utilisation). La valeur exacte importe peu : elle est non nulle.
Exercice 3.
$ cd / ; echo $?
0
$ cd /impossible
bash: cd: /impossible: Aucun fichier ou dossier de ce type
$ echo $?
1
Oui, cd aussi a un code de retour — chaque commande, sans exception.
Exercice 4.
$ pouet
pouet : commande introuvable
$ echo $?
127
Le shell lui-même suit la convention : 127 = programme introuvable.
Exercice 5.
which curl && echo "réseau ok" || echo "curl manquant"
Version plus lisible et plus rigoureuse :
if which curl > /dev/null; then echo "réseau ok"; else echo "curl manquant"; fi
(> /dev/null jette poliment la sortie de which, seul le code nous intéresse.) La version composée && ... || ... fonctionne ici, mais a un défaut théorique : si le premier echo échouait (rare mais possible), le || prendrait la main à tort. Le if/else n'a pas cette ambiguïté.
Exercice 6. Parce que le code de retour décrit la réussite de la mission, pas le volume de texte produit. mkdir -p sur un dossier déjà existant ne crée rien et n'affiche rien — pourtant la mission (« garantir que ce dossier existe ») est remplie : code 0. De même, grep sans résultat affiche zéro ligne mais répond proprement « rien trouvé » par son code 1. Silence et succès ne sont pas incompatibles — c'est même fréquent sous Unix.