Quitter le script shell en fonction du code de sortie du processus

378

J'ai un script shell qui exécute un certain nombre de commandes. Comment faire pour quitter le script shell si l'une des commandes se termine avec un code de sortie différent de zéro?

Mark Roddy
la source
3
J'ai répondu en supposant que vous utilisez bash, mais si c'est un shell très différent, pouvez-vous spécifier dans votre message?
Martin W
Méthode difficile: testez la valeur de $?après chaque commande. Méthode simple: mettez set -eou #!/bin/bash -een haut de votre script Bash.
mwfearnley

Réponses:

495

Après chaque commande, le code de sortie peut être trouvé dans la $?variable afin que vous ayez quelque chose comme:

ls -al file.ext
rc=$?; if [[ $rc != 0 ]]; then exit $rc; fi

Vous devez faire attention aux commandes pipées car la $?seule vous donne le code retour du dernier élément du tube donc, dans le code:

ls -al file.ext | sed 's/^/xx: /"

ne renverra pas de code d'erreur si le fichier n'existe pas (puisque la sedpartie du pipeline fonctionne réellement, renvoyant 0).

Le bashshell fournit en fait un tableau qui peut aider dans ce cas, c'est-à-dire PIPESTATUS. Ce tableau a un élément pour chacun des composants du pipeline, auquel vous pouvez accéder individuellement comme ${PIPESTATUS[0]}:

pax> false | true ; echo ${PIPESTATUS[0]}
1

Notez que cela vous donne le résultat de la falsecommande, pas le pipeline entier. Vous pouvez également obtenir la liste complète à traiter comme bon vous semble:

pax> false | true | false; echo ${PIPESTATUS[*]}
1 0 1

Si vous vouliez obtenir le plus gros code d'erreur d'un pipeline, vous pouvez utiliser quelque chose comme:

true | true | false | true | false
rcs=${PIPESTATUS[*]}; rc=0; for i in ${rcs}; do rc=$(($i > $rc ? $i : $rc)); done
echo $rc

Cela passe par chacun des PIPESTATUSéléments tour à tour, en le stockant rcs'il était supérieur à la rcvaleur précédente .

paxdiablo
la source
39
Même fonctionnalité dans une seule ligne de code portable: ls -al file.ext || exit $?([[]] n'est pas portable)
MarcH
19
MarcH, je pense que vous trouverez que [[ ]]c'est assez portable bash, c'est ce que la question est étiquetée :-) Curieusement, lsça ne marche pas command.comdonc ce n'est pas portable non plus, spécieux je sais, mais c'est le même genre d'argument que vous présent.
paxdiablo
39
Je sais que c'est ancien, mais il convient de noter que vous pouvez obtenir le code de sortie des commandes dans un tube via le tableau PIPESTATUS(c'est-à-dire ${PIPESTATUS[0]}pour la première commande, ${PIPESTATUS[1]}pour la seconde ou ${PIPESTATUS[*]}pour une liste de toutes les stati de sortie.
DevSolar
11
Il convient de souligner que les scripts shell élégants et idiomatiques doivent très rarement être examinés $?directement. Vous voulez généralement quelque chose comme if ls -al file.ext; then : nothing; else exit $?; fiqui, bien sûr, comme @MarcH dit est équivalent, ls -al file.ext || exit $?mais si les clauses thenou elsesont un peu plus complexes, elles sont plus faciles à maintenir.
tripleee
9
[[ $rc != 0 ]]vous donnera une 0: not foundou une 1: not founderreur. Cela devrait être changé en [ $rc -ne 0 ]. rc=$?Peut également être retiré et utilisé [ $? -ne 0 ].
CurtisLeeBolin
223

Si vous voulez travailler avec $ ?, vous devrez le vérifier après chaque commande, car $? est mis à jour après la fin de chaque commande. Cela signifie que si vous exécutez un pipeline, vous n'obtiendrez que le code de sortie du dernier processus du pipeline.

Une autre approche consiste à le faire:

set -e
set -o pipefail

Si vous placez cela en haut du script shell, il semblerait que bash s'en occupe pour vous. Comme l'a noté une affiche précédente, "set -e" provoquera la sortie de bash avec une erreur sur toute commande simple. "set -o pipefail" provoquera la sortie de bash avec une erreur sur n'importe quelle commande dans un pipeline.

Voir ici ou ici pour un peu plus de discussion sur ce problème. Voici la section du manuel bash sur l'ensemble intégré.

Jeff Hill
la source
6
Cela devrait vraiment être la meilleure réponse: c'est beaucoup, beaucoup plus facile à faire que d'utiliser PIPESTATUSet de vérifier les codes de sortie partout.
candu
2
#!/bin/bash -eest le seul moyen de démarrer un script shell. Vous pouvez toujours utiliser des choses comme foo || handle_error $?si vous devez réellement examiner les états de sortie.
Davis Herring
53

" set -e" est probablement le moyen le plus simple de procéder. Mettez juste cela avant toute commande dans votre programme.

Allen
la source
6
@SwaroopCH set -evotre script sera abandonné si une commande de votre script se termine avec un état d'erreur et que vous n'avez pas géré cette erreur.
Andrew
2
set -eest 100% équivalent à set -o errexitce qui, contrairement au premier, peut être recherché. Recherchez opengroup + errexit pour la documentation officielle.
MarcH
30

Si vous appelez simplement exit dans le bash sans paramètre, il retournera le code de sortie de la dernière commande. Combiné avec OR, le bash ne devrait appeler exit que si la commande précédente échoue. Mais je n'ai pas testé cela.

command1 || sortie;
command2 || sortie;

Le Bash stockera également le code de sortie de la dernière commande dans la variable $ ?.

Arvodan
la source
25
[ $? -eq 0 ] || exit $?; # exit for none-zero return code
chemila
la source
3
Cela ne devrait-il pas être exit $?(sans parens)?
malthe
21

http://cfaj.freeshell.org/shell/cus-faq-2.html#11

  1. Comment obtenir le code de sortie de cmd1incmd1|cmd2

    Tout d'abord, notez que le cmd1code de sortie peut être différent de zéro et ne signifie toujours pas une erreur. Cela se produit par exemple dans

    cmd | head -1

    vous pourriez observer un état de sortie 141 (ou 269 avec ksh93) de cmd1, mais c'est parce qu'il a cmdété interrompu par un signal SIGPIPE lorsqu'il s'est head -1terminé après avoir lu une ligne.

    Connaître l'état de sortie des éléments d'un pipeline cmd1 | cmd2 | cmd3

    une. avec zsh:

    Les codes de sortie sont fournis dans le tableau spécial pipestatus. cmd1le code de sortie est dans $pipestatus[1], le cmd3code de sortie est $pipestatus[3], donc c'est $?toujours la même chose que $pipestatus[-1].

    b. avec bash:

    Les codes de sortie sont fournis dans le PIPESTATUStableau spécial. cmd1le code de sortie est dans ${PIPESTATUS[0]}, le cmd3code de sortie est ${PIPESTATUS[2]}, donc c'est $?toujours la même chose que ${PIPESTATUS: -1}.

    ...

    Pour plus de détails, voir le lien suivant .

gatoatigrado
la source
19

pour bash:

# this will trap any errors or commands with non-zero exit status
# by calling function catch_errors()
trap catch_errors ERR;

#
# ... the rest of the script goes here
#  

function catch_errors() {
   # do whatever on errors
   # 
   #
   echo "script aborted, because of errors";
   exit 0;
}

la source
19
Ne devrait probablement pas "quitter 0", car cela indique le succès.
nobar
3
exit_code = $ ?; echo "script abandonné en raison d'erreurs"; exit $ exit_code
RaSergiy
1
@ HAL9001 en avez-vous la preuve? La documentation IBM dit le contraire .
Patrick James McDougle
11

En bash, c'est facile, il suffit de les lier avec &&:

command1 && command2 && command3

Vous pouvez également utiliser la construction imbriquée if:

if command1
   then
       if command2
           then
               do_something
           else
               exit
       fi
   else
       exit
fi
Martin W
la source
+1 C'était la solution la plus simple que je cherchais. De plus, vous pouvez également écrire if (! command)si vous attendez un code d'erreur différent de zéro de la commande.
Berci
c'est pour les commandes séquentielles .. que faire si je veux lancer ces 3 en parallèle et tuer tout le monde si l'un d'eux échoue?
Vasile Surdu
4
#
#------------------------------------------------------------------------------
# run a command on failure exit with message
# doPrintHelp: doRunCmdOrExit "$cmd"
# call by:
# set -e ; doRunCmdOrExit "$cmd" ; set +e
#------------------------------------------------------------------------------
doRunCmdOrExit(){
    cmd="$@" ;

    doLog "DEBUG running cmd or exit: \"$cmd\""
    msg=$($cmd 2>&1)
    export exit_code=$?

    # if occured during the execution exit with error
    error_msg="Failed to run the command:
        \"$cmd\" with the output:
        \"$msg\" !!!"

    if [ $exit_code -ne 0 ] ; then
        doLog "ERROR $msg"
        doLog "FATAL $msg"
        doExit "$exit_code" "$error_msg"
    else
        #if no errors occured just log the message
        doLog "DEBUG : cmdoutput : \"$msg\""
        doLog "INFO  $msg"
    fi

}
#eof func doRunCmdOrExit
Yordan Georgiev
la source
2
Il y a rarement une raison d'utiliser $*; utilisez "$@"plutôt pour conserver les espaces et les caractères génériques.
Davis Herring