web-dev-qa-db-fra.com

Attraper les codes d'erreur dans un tube Shell

J'ai actuellement un script qui fait quelque chose comme

./a | ./b | ./c

Je veux le modifier de sorte que si l'un de a, b ou c se termine avec un code d'erreur, j'imprime un message d'erreur et m'arrête au lieu de transmettre une mauvaise sortie vers l'avant.

Quelle serait la façon la plus simple/la plus propre de le faire?

88
hugomg

Si vous ne voulez vraiment pas que la deuxième commande se poursuive jusqu'à ce que la première soit réussie, vous devrez probablement utiliser des fichiers temporaires. La version simple de cela est:

tmp=${TMPDIR:-/tmp}/mine.$$
if ./a > $tmp.1
then
    if ./b <$tmp.1 >$tmp.2
    then
        if ./c <$tmp.2
        then : OK
        else echo "./c failed" 1>&2
        fi
    else echo "./b failed" 1>&2
    fi
else echo "./a failed" 1>&2
fi
rm -f $tmp.[12]

La redirection '1> & 2' peut également être abrégée '> & 2'; cependant, une ancienne version du shell MKS a mal géré la redirection d'erreur sans le "1" précédent, j'ai donc utilisé cette notation sans ambiguïté pour la fiabilité des âges.

Cela fuit des fichiers si vous interrompez quelque chose. La programmation Shell à l'épreuve des bombes (plus ou moins) utilise:

tmp=${TMPDIR:-/tmp}/mine.$$
trap 'rm -f $tmp.[12]; exit 1' 0 1 2 3 13 15
...if statement as before...
rm -f $tmp.[12]
trap 0 1 2 3 13 15

La première ligne de déroutement dit "exécuter les commandes" rm -f $tmp.[12]; exit 1 'lorsque l'un des signaux 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE ou 15 SIGTERM se produit, ou 0 (lorsque le shell se termine pour une raison quelconque). Si vous écrivez un script Shell, l'interruption finale n'a qu'à supprimer l'interruption sur 0, qui est l'interruption de sortie Shell (vous pouvez laisser les autres signaux en place puisque le processus est sur le point de se terminer de toute façon).

Dans le pipeline d'origine, il est possible pour "c" de lire les données de "b" avant la fin de "a" - ceci est généralement souhaitable (cela donne à plusieurs cœurs un travail à faire, par exemple). Si "b" est une phase de "tri", cela ne s'applique pas - "b" doit voir toutes ses entrées avant de pouvoir générer une partie de sa sortie.

Si vous souhaitez détecter les commandes qui échouent, vous pouvez utiliser:

(./a || echo "./a exited with $?" 1>&2) |
(./b || echo "./b exited with $?" 1>&2) |
(./c || echo "./c exited with $?" 1>&2)

C'est simple et symétrique - il est trivial de s'étendre à un pipeline en 4 parties ou en N parties.

Une simple expérimentation avec 'set -e' n'a pas aidé.

19
Jonathan Leffler

Dans bash vous pouvez utiliser set -e et set -o pipefail au début de votre fichier. Une commande suivante ./a | ./b | ./c échouera en cas d'échec de l'un des trois scripts. Le code retour sera le code retour du premier script ayant échoué.

Notez que pipefail n'est pas disponible en standard sh.

143
Michel Samia

Vous pouvez également vérifier le ${PIPESTATUS[]} tableau après l'exécution complète, par exemple si vous exécutez:

./a | ./b | ./c

Alors ${PIPESTATUS} sera un tableau de codes d'erreur de chaque commande du tube, donc si la commande du milieu a échoué, echo ${PIPESTATUS[@]} contiendrait quelque chose comme:

0 1 0

et quelque chose comme ça s'exécute après la commande:

test ${PIPESTATUS[0]} -eq 0 -a ${PIPESTATUS[1]} -eq 0 -a ${PIPESTATUS[2]} -eq 0

vous permettra de vérifier que toutes les commandes du pipe ont réussi.

39
Imron

Malheureusement, la réponse de Johnathan nécessite des fichiers temporaires et les réponses de Michel et Imron nécessitent bash (même si cette question est étiquetée Shell). Comme d'autres l'ont déjà souligné, il n'est pas possible d'interrompre le tuyau avant le démarrage de processus ultérieurs. Tous les processus sont démarrés en même temps et seront donc tous exécutés avant que toute erreur ne puisse être communiquée. Mais le titre de la question portait également sur les codes d'erreur. Ceux-ci peuvent être récupérés et examinés une fois le tuyau terminé pour déterminer si l'un des processus impliqués a échoué.

Voici une solution qui capture toutes les erreurs du tube et pas seulement les erreurs du dernier composant. C'est donc comme le pipefail de bash, juste plus puissant dans le sens où vous pouvez récupérer tous les codes d'erreur.

res=$( (./a 2>&1 || echo "1st failed with $?" >&2) |
(./b 2>&1 || echo "2nd failed with $?" >&2) |
(./c 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi

Pour détecter si quelque chose a échoué, une commande echo s'imprime sur l'erreur standard en cas d'échec d'une commande. Ensuite, la sortie d'erreur standard combinée est enregistrée dans $res et étudié plus tard. C'est aussi pourquoi l'erreur standard de tous les processus est redirigée vers la sortie standard. Vous pouvez également envoyer cette sortie à /dev/null ou le laisser comme un autre indicateur que quelque chose s'est mal passé. Vous pouvez remplacer la dernière redirection vers /dev/null avec un fichier si vous ne devez pas stocker la sortie de la dernière commande n'importe où.

Pour jouer davantage avec cette construction et pour vous convaincre que cela fait vraiment ce qu'il faut, j'ai remplacé ./a, ./b et ./c par des sous-coquilles qui exécutent echo, cat et exit. Vous pouvez l'utiliser pour vérifier que cette construction transmet vraiment toutes les sorties d'un processus à un autre et que les codes d'erreur sont enregistrés correctement.

res=$( (sh -c "echo 1st out; exit 0" 2>&1 || echo "1st failed with $?" >&2) |
(sh -c "cat; echo 2nd out; exit 0" 2>&1 || echo "2nd failed with $?" >&2) |
(sh -c "echo start; cat; echo end; exit 0" 2>&1 || echo "3rd failed with $?" >&2) > /dev/null 2>&1)
if [ -n "$res" ]; then
    echo pipe failed
fi
8
josch