L'objet de cet article est de présenter au débutant le concept de code de retour et de lui montrer comment l'utiliser correctement. Pour favoriser la compréhension du lecteur, le style de cette présentation peut se résumer ainsi : une phrase, un exemple. D'autre part, aucune hypothèse particulière n'est faite sur l'environnement utilisé si ce n'est qu'il a ouvert une fenêtre console.
Le code de retour d'une commande est un mécanisme fourni par le shell (quel qu'il soit) qui signale à l'utilisateur si l'exécution de cette commande s'est bien déroulée ou bien s'il y a eu un problème quelconque. Le code de retour est un petit entier positif ou nul, toujours compris entre 0 et 255.
Par convention, un code de retour égal à 0 signifie que la commande s'est exécutée correctement. Un code différent de 0 signifie soit une erreur d'exécution, soit une erreur syntaxique.
Paramètre spécial ?
Le paramètre spécial ? du shell (à ne pas confondre avec le caractère générique ? du shell) contient le code de retour de la dernière commande exécutée de manière séquentielle; on emploie également l'expression exécution synchrone. L'exécution séquentielle signifie que le shell n'exécute une commande que lorsque la précédente s'est terminée. C'est bien sûr le mode d'exécution par défaut d'une commande.
$ pwd /home/sanchis $ echo $? 0 => la commande s'est exécutée correctement $ ls -l vi ls: vi: Aucun fichier ou répertoire de ce type $ echo $? 1 => erreur d'exécution !
Dans l'exemple précédent, la commande ls ne trouve pas le fichier correspondant à l'éditeur de texte vi dans le répertoire courant (ce qui est tout à fait normal !) et positionne un code de retour à 1.
Un problème qui se pose au débutant lorsqu'il utilise le code de retour d'une commande, c'est que chaque commande positionne « à sa manière » les codes de retour différents de 0. Ainsi, un code de retour égal à 1 positionné par la commande Unix ls n'a pas la même signification qu'un code de retour à 1 positionné par la commande Unix grep. Il n'existe qu'une seule solution à ce problème : lire le manuel correspondant à la commande.
Donnons d'autres exemples :
La commande interne deux points ( : ) sans argument retourne toujours un code de retour égal à 0.
$ : $ echo $? 0
Il en est de même avec la commande interne echo : elle retourne toujours un code de retour égal à 0, sauf dans quelques cas très particuliers. Enfin, certaines commandes utilisent plusieurs valeurs pour indiquer des significations différentes, comme la commande Unix grep. A ce titre, cette commande mérite que l'on s'y attarde un peu.
Commande Unix grep
Cette commande affiche sur sa sortie standard l'ensemble des lignes contenant une chaîne de caractères spécifiée en argument, lignes appartenant à un ou plusieurs fichiers textes (ou par défaut, son entrée standard).
La syntaxe générale de cette commande peut s'écrire :
grep [ option(s) ] chaîne_cherchée [ fichier_texte(s) ]
Les crochets indiquent que ce qui est à l'intérieur est facultatif (les options et les fichiers textes). La chaîne cherchée, elle, est obligatoire.
$ cat /etc/passwd root:x:0:3:Super User:/root:/bin/bash daemon:x:2:2:daemon:/sbin: bertrand:x:103:20::/home/bertrand:/bin/bash albert:x:104:20::/home/albert:/bin/bash sanchis:x:122:20::/home/sanchis:/bin/bash $ grep daemon /etc/passwd daemon:x:2:2:daemon:/sbin:
Cette commande affiche toutes les lignes du fichier /etc/passwd contenant la chaîne daemon.
grep positionne un code de retour
$ grep daemon /etc/passwd daemon:x:2:2:daemon:/sbin: $ echo $? 0 $ grep toto /etc/passwd $ $ echo $? 1 => la chaîne toto n'est pas présente dans /etc/passwd $ grep sanchis turlututu grep: turlututu: Aucun fichier ou répertoire de ce type $ echo $? 2 => le fichier turlututu n'existe pas !
Code de retour d'une suite de commandes
Le code de retour d'une suite de commandes est le code de retour de la dernière commande exécutée. Par exemple, le code de retour de la suite de commandes cmd1; cmd2; cmd3 est le code de retour de la commande cmd3.
$ pwd; ls vi; echo bonjour /home/sanchis ls: vi: Aucun fichier ou répertoire de ce type bonjour $ echo $? 0 => code de retour de echo bonjour
Il en est de même pour le pipeline cmd1 | cmd2 | cmd3. Le code de retour sera celui de cmd3.
$ cat /etc/passwd | grep daemon daemon:x:2:2:daemon:/sbin: $ echo $? 0 => code de retour de grep daemon $ cat /etc/passwd | grep toto $ $ echo $? 1 => code de retour de grep toto
En Bash, il est possible d'obtenir la négation d'un code de retour d'un pipeline en plaçant le mot-clé ! devant celui-ci. Cela signifie que si le code de retour de pipeline est égal à 0, alors le code de retour de ! pipeline est égal à 1.
$ ! ls fichier => le code de retour de ls fichier est égal à 0 fichier $ echo $? 1 $ ! cat /etc/passwd | grep daemon daemon:x:2:2:daemon:/sbin: $ echo $? 1
Inversement, si le code de retour de pipeline est différent de 0, alors celui de ! pipeline est égal à 0.
$ ! grep sanchis turlututu grep: turlututu: Aucun fichier ou répertoire de ce type $ echo $? 0
Code de retour d'un programme shell
Un programm shell (ou script shell) peut être vu comme une suite de commandes à laquelle on a donné un nom. Il s'en suit que le code de retour d'un programme shell est le code de retour de la dernière commande qu'il a exécutée.
Soit le programme shell lvi contenant l'unique commande ls vi. Pour créer ce programme, le lecteur utilisera son éditeur de texte préféré (vi, emacs, ou autre), donnera la permission x à ce programme (en tapant chmod u+x lvi) et enfin, tapera lvi (comme dans l'exemple ci-dessous) ou ./lvi si le répertoire courant n'est pas mentionné dans la variable PATH. Le script shell lvi produira une erreur car vi ne se trouve pas dans le répertoire courant. Après exécution, le code de retour sera celui de la commande ls vi (dernière commande exécutée).
$ cat lvi #!/bin/sh ls vi $ lvi ls: vi: Aucun fichier ou répertoire de ce type $ echo $? 1 => code de retour de la dernière commande exécutée par lvi
Prenons un autre exemple. Le fichier lvi1 contient deux commandes ls vi et echo Fin :
$ cat lvi1 #!/bin/sh ls vi echo Fin $ lvi1 ls: vi: Aucun fichier ou répertoire de ce type Fin $ echo $? 0 => code de retour de la dernière commande exécutée par lvi1 (echo Fin)
Il est parfois nécessaire de positionner explicitement le code de retour d'un programme shell avant qu'il ne se termine pour signaler une erreur particulière à l'utilisateur : on utilise alors la commande interne exit.
Commande interne exit
Sa syntaxe est particulièrement simple : exit [ n ]
Elle provoque l'arrêt du programme shell avec un code de retour égal à n. Si n n'est pas précisé, le code de retour fourni est celui de la dernière commande exécutée.
$ cat lvi2 #!/bin/sh ls vi exit 23 $ lvi2 ls: vi: Aucun fichier ou répertoire de ce type $ echo $? 23 => code de retour de exit 23
Le programme shell lvi2 positionne un code de retour différent (ici égal à 23) après exécution de la commande ls vi.
Résultat et code de retour
On ne doit pas confondre le résultat d'une commande et son code de retour : le résultat correspond à ce qui est écrit sur sa sortie standard; le code de retour indique uniquement si l'exécution de la commande s'est bien effectuée ou non. Parfois, on est intéressé uniquement par le code de retour d'une commande et non par les résultats qu'elle produit sur la sortie standard ou la sortie standard pour les messages d'erreurs.
Dans l'exemple ci-dessous, sortie standard et sortie standard pour les messages d'erreur de la commande grep ont été redirigées vers la « poubelle » /dev/null.
$ grep toto /etc/passwd > /dev/null 2>&1 $ $ echo $? 1 => on en déduit que la chaîne toto n'est pas présente dans /etc/passwd
Structures de contrôle et code de retour
Le code de retour joue un rôle très important dans les programmes shell car il conditionne le fonctionnement de certaines structures de contrôle telles que les structures until, while et if.
Détaillons par exemple la syntaxe de until :
until suite_cmd1 do suite_cmd2 done
Cette structure fonctionne de la manière suivante : la suite de commandes suite_cmd1 est exécutée et si son code de retour est différent de 0, alors le shell exécute le contenu de la boucle, c'est-à-dire la suite de commandes suite_cmd2. On ne sort de l'itération que lorsque le code de retour de suite_cmd1 est égal à 0.
Examinons à ce propos l'ensemble suivant : la commande rm titi 2>&- cherche à effacer le fichier titi qui n'existe pas. Le message d'erreur correspondant ne s'affiche pas car la sortie standard pour les messages d'erreur a été fermée (2>&-). Le code de retour de cette commande est différent de 0, donc les commandes à l'intérieur de la boucle sont exécutées : tant que le fichier titi ne pourra être effacé par rm (ou bien que l'utilisateur n'arrête explicitement cette itération), le shell reste dans la boucle. Pour y remédier, on pourra, en ouvrant une autre fenêtre, créer ce fichier titi (en tapant par exemple > titi à l'invite du shell).
La chaîne " > " affichée par le shell indique à l'utilisateur que sa commande n'est syntaxiquement pas terminée.
$ until rm titi 2>&- > do > echo "titi n'a toujours pas ete cree" > sleep 4 > done titi n'a toujours pas ete cree titi n'a toujours pas ete cree ... titi n'a toujours pas ete cree
Autre exemple plus insolite : l'écriture d'une boucle infinie (pouvant constituer le squelette d'un serveur écrit en shell !)
while : do corps du serveur done
On utilise ici la propriété de la commande interne : qui retourne systématiquement un code de retour égal à 0.
Opérateurs && et || sur les codes de retour
Les opérateurs && et || autorisent l'exécution conditionnelle d'une commande cmd suivant la valeur du code de retour de la dernière commande précédemment exécutée
Syntaxe : cmd1 && cmd2
Le fonctionnement est le suivant : cmd1 est exécutée et si son code de retour est égal à 0, alors cmd2 est également exécutée.
$ grep daemon /etc/passwd && echo daemon existe daemon:x:2:2:daemon:/sbin: daemon existe
La chaîne de caractères daemon est présente dans le fichier /etc/passwd, le code de retour renvoyé par l'exécution de grep est 0; par conséquent, la commande echo daemon existante est exécutée.
Syntaxe : cmd1 || cmd2
cmd1 est exécutée et si son code de retour est différent de 0, alors cmd2 est également exécutée. Pour illustrer cela, créons rapidement un fichier titi et supposons que le fichier toto n'existe pas :
$ cp /etc/passwd titi => création de titi $ ls titi toto ls: toto: Aucun fichier ou répertoire de ce type titi $ rm toto || echo toto non efface rm: ne peut enlever `toto': Aucun fichier ou répertoire de ce type toto non efface
Le fichier toto n'existant pas, la commande rm toto affiche un message d'erreur et produit un code de retour différent de 0 : la commande interne echo qui suit est donc exécutée.
Combinaisons d'opérateurs && et ||
Les deux règles mentionnées ci-dessus sont appliquées par le shell lorsqu'un suite de commandes contient plusieurs opérateurs && et ||. Ces deux opérateurs ont la même priorité et leur évaluation s'effectue de gauche à droite.
$ ls titi || ls toto || echo fini aussi titi
Le code de retour de ls titi est égal à 0 car titi existe, la commande ls toto ne sera donc pas exécutée. D'autre part, le code de retour de l'ensemble ls titi || ls toto est le code de retour de la dernière commande exécutée, c'est-à-dire est égal à 0 (car c'est le code de retour de ls titi), donc echo fini aussi n'est pas exécuté.
Intervertissons maintenant les deux commandes ls :
$ ls toto || ls titi || echo fini ls: toto: Aucun fichier ou répertoire de ce type titi
Le code de retour de ls toto est différent de 0, donc ls titi s'exécute. Cette commande renvoie un code de retour égal à 0, par conséquent echo fini n'est pas exécuté.
Combinons maintenant opérateurs && et || :
$ ls titi || ls toto || echo suite et && echo fin titi fin
La commande ls titi est exécutée avec un code de retour égal à 0, donc la commande ls toto n'est pas exécutée, donc le code de retour de l'ensemble ls titi || ls toto est égal à 0, donc la commande echo suite et n'est pas exécutée, donc le code de retour de ls titi || ls toto || echo suite et est égal à 0, donc la commande echo fin est exécutée ! (ouf !!!).
Bien sûr, un raisonnement analogue s'applique avec l'opérateur &&. Aussi, pour terminer, nous laisserons à la sagacité du lecteur l'interprétation des exemples ci-dessous :
$ ls titi && ls toto && echo fini titi ls: toto: Aucun fichier ou répertoire de ce type $ ls toto && ls titi && echo suite et && echo fin ls: toto: Aucun fichier ou répertoire de ce type $ ls titi && ls toto && echo suite et || echo fin titi ls: toto: Aucun fichier ou répertoire de ce type fin
Les codes de retour ne devraient plus avoir de secret pour vous !