Récupération des codes d'erreur dans un tube shell

97

J'ai actuellement un script qui fait quelque chose comme

./a | ./b | ./c

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

Quelle serait la manière la plus simple / la plus propre de le faire?

hugomg
la source
7
Il doit vraiment y avoir quelque chose comme &&|qui signifierait "ne continuer le tube que si la commande précédente a réussi". Je suppose que vous pourriez aussi avoir |||ce qui signifierait "continuer le tube si la commande précédente a échoué" (et éventuellement envoyer le message d'erreur comme celui de Bash 4 |&).
Suspendu jusqu'à nouvel ordre.
6
@DennisWilliamson, vous ne pouvez pas « arrêter la pipe » parce que a, b, les ccommandes ne sont pas exécutées de façon séquentielle , mais en parallèle. En d' autres termes, les flux de données séquentiellement à partir ade c, mais le réel a, bet ccommandes de démarrage ( à peu près) dans le même temps.
Giacomo le

Réponses:

20

Si vous ne voulez vraiment pas que la deuxième commande se poursuive jusqu'à ce que la première soit réussie, vous devez 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é depuis des lustres.

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' quand l'un des signaux 1 SIGHUP, 2 SIGINT, 3 SIGQUIT, 13 SIGPIPE ou 15 SIGTERM se produit, ou 0 (lorsque le shell sort pour une raison quelconque). Si vous écrivez un script shell, le trap final n'a besoin que de supprimer le trap sur 0, qui est le trap de sortie du 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 que «c» lise les données de «b» avant que «a» ne soit terminé - c'est généralement souhaitable (cela donne du travail à plusieurs cœurs, par exemple). Si 'b' est une phase de 'tri', alors cela ne s'appliquera pas - 'b' doit voir toutes ses entrées avant de pouvoir générer l'une de ses sorties.

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é.

Jonathan Leffler
la source
1
Je recommanderais d'utiliser mktempou tempfile.
Suspendu jusqu'à nouvel ordre.
@Dennis: oui, je suppose que je devrais m'habituer à des commandes telles que mktemp ou tmpfile; ils n'existaient pas au niveau de la coquille quand je l'ai appris, oh il y a tant d'années. Faisons une vérification rapide. Je trouve mktemp sur MacOS X; J'ai mktemp sur Solaris mais uniquement parce que j'ai installé des outils GNU; il semble que mktemp soit présent sur les anciens HP-UX. Je ne sais pas s'il existe une invocation commune de mktemp qui fonctionne sur toutes les plates-formes. POSIX ne standardise ni mktemp ni tmpfile. Je n'ai pas trouvé tmpfile sur les plates-formes auxquelles j'ai accès. Par conséquent, je ne pourrai pas utiliser les commandes dans les scripts shell portables.
Jonathan Leffler
1
Lorsque vous utilisez, trapgardez à l'esprit que l'utilisateur peut toujours envoyer SIGKILLà un processus pour le terminer immédiatement, auquel cas votre trap ne prendra pas effet. Il en va de même lorsque votre système subit une panne de courant. Lors de la création de fichiers temporaires, assurez-vous d'utiliser mktempcar cela placera vos fichiers quelque part, où ils seront nettoyés après un redémarrage (généralement dans /tmp).
josch
156

Dans bash, vous pouvez utiliser set -eet set -o pipefailau début de votre fichier. Une commande suivante ./a | ./b | ./céchouera lorsque l'un des trois scripts échoue. Le code retour sera le code retour du premier script ayant échoué.

Notez que ce pipefailn'est pas disponible dans sh standard .

Michel Samia
la source
Je ne connaissais pas pipefail, vraiment pratique.
Phil Jackson
Il est destiné à le mettre dans un script, pas dans le shell interactif. Votre comportement peut être simplifié pour définir -e; faux; il quitte également le processus shell, ce qui est le comportement attendu;)
Michel Samia
11
Remarque: cela exécutera toujours les trois scripts et n'arrêtera pas le tube à la première erreur.
hyde
1
@ n2liquid-GuilhermeVieira Btw, par "différentes variantes", je voulais spécifiquement supprimer un ou les deux set(pour 4 versions différentes au total), et voir comment cela affecte la sortie de la dernière echo.
hyde
1
@josch J'ai trouvé cette page en cherchant comment faire cela dans bash depuis Google, et cependant, "C'est exactement ce que je recherche". Je soupçonne que de nombreuses personnes qui votent pour des réponses passent par un processus de réflexion similaire et ne vérifient pas les balises et les définitions des balises.
Troy Daniels
44

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

./a | ./b | ./c

Ensuite, il y ${PIPESTATUS}aura un tableau de codes d'erreur de chaque commande dans le tube, donc si la commande du milieu échouait, echo ${PIPESTATUS[@]}contiendrait quelque chose comme:

0 1 0

et quelque chose comme ça 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 tube ont réussi.

Imron
la source
11
C'est bashish - c'est une extension bash et ne fait pas partie du standard Posix, donc d'autres shells comme dash et ash ne le supportent pas. Cela signifie que vous pouvez rencontrer des problèmes si vous essayez de l'utiliser dans des scripts qui démarrent #!/bin/sh, car si ce shn'est pas bash, cela ne fonctionnera pas. (Facilement réparable en pensant à utiliser à la #!/bin/bashplace.)
David donné le
2
echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$'- renvoie 0 si $PIPESTATUS[@]ne contient que 0 et des espaces (si toutes les commandes du tube réussissent).
MattBianco
@MattBianco C'est la meilleure solution pour moi. Cela fonctionne également avec &&, par exemple, command1 && command2 | command3si l'un de ces échecs échoue, votre solution renvoie une valeur non nulle.
DavidC du
8

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 tube avant que les processus ultérieurs ne soient démarrés. Tous les processus sont lancés en même temps et seront donc tous exécutés avant que des erreurs ne puissent être communiquées. Mais le titre de la question posait également des questions 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 rattrape toutes les erreurs dans le tuyau 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 echocommande s'imprime sur une erreur standard en cas d'échec d'une commande. Ensuite, la sortie d'erreur standard combinée est enregistrée $reset étudiée ultérieurement. C'est également pourquoi l'erreur standard de tous les processus est redirigée vers la sortie standard. Vous pouvez également envoyer cette sortie /dev/nullou la laisser comme un autre indicateur que quelque chose s'est mal passé. Vous pouvez remplacer la dernière redirection vers /dev/nullpar un fichier si vous ne souhaitez pas stocker la sortie de la dernière commande n'importe où.

Pour jouer plus avec cette construction et pour vous convaincre que cela fait vraiment ce qu'il faut, j'ai remplacé ./a, ./bet ./cpar des sous-shell qui exécutent echo, catet exit. Vous pouvez l'utiliser pour vérifier que cette construction transfère réellement toute la sortie d'un processus à un autre et que les codes d'erreur sont correctement enregistrés.

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
Josch
la source