Le B.A BA de Bash : le code de retour

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

  • égal à 0 pour indiquer qu'une ou plusieurs lignes ont été trouvées.
  • égal à 1 pour indiquer qu'aucune ligne n'a été trouvée.
  • égal à 2 pour indiquer la présence d'une erreur de syntaxe ou qu'un fichier mentionné en argument est inaccessible.

    $ 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

    Opérateurs &&

    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.

    Opérateurs ||

    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 !

    Eric Sanchis
    Linux Magazine France n°39 - Mai 2002