101 votes

Capture des 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 des éléments a, b ou c sort avec un code d'erreur, j'imprime un message d'erreur et je m'arrête au lieu de faire suivre la mauvaise sortie.

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

8 votes

Il faudrait vraiment quelque chose comme &&| ce qui signifierait "ne continuer le pipe que si la commande précédente a réussi". Je suppose que vous pourriez aussi avoir ||| ce qui signifierait "continuer le pipe si la commande précédente a échoué" (et éventuellement transmettre le message d'erreur comme la commande de Bash 4 |& ).

7 votes

@DennisWilliamson, vous ne pouvez pas "arrêter le tuyau" parce que a , b , c Les commandes ne sont pas exécutées séquentiellement mais en parallèle. En d'autres termes, les données circulent séquentiellement de a a c mais l'actuel a , b y c Les commandes commencent (à peu près) au même moment.

168voto

Michel Samia Points 775

Sur bash vous pouvez utiliser set -e y set -o pipefail au début de votre fichier. Une commande ultérieure ./a | ./b | ./c échouera si l'un des trois scripts échoue. Le code de retour sera le code de retour du premier scripts qui a échoué.

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

0 votes

Je ne connaissais pas la queue de pie, c'est très pratique.

0 votes

Il est prévu de le mettre dans un script, pas dans le shell interactif. Votre comportement peut être simplifié en mettant -e ; false ; cela quitte également le processus shell, ce qui est le comportement attendu ;)

13 votes

Note : ceci va quand même exécuter les trois scripts, et ne pas arrêter le pipe à la première erreur.

50voto

Imron Points 226

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

./a | ./b | ./c

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

0 1 0

et quelque chose comme ça exécuté 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.

11 votes

C'est bashish --- c'est une extension de bash et ne fait pas partie du standard Posix, donc d'autres shells comme dash et ash ne le supporteront pas. Cela signifie que vous pouvez rencontrer des problèmes si vous essayez de l'utiliser dans des scripts qui commencent par #!/bin/sh car si sh n'est pas bash, cela ne fonctionnera pas. (Facilement corrigeable en se rappelant d'utiliser #!/bin/bash à la place).

3 votes

echo ${PIPESTATUS[@]} | grep -qE '^[0 ]+$' - renvoie 0 si $PIPESTATUS[@] ne contient que des 0 et des espaces (si toutes les commandes du pipe réussissent).

0 votes

@MattBianco C'est la meilleure solution pour moi. Elle fonctionne également avec &&, par exemple. command1 && command2 | command3 Si l'un d'entre eux échoue, votre solution renvoie un résultat différent de zéro.

22voto

Jonathan Leffler Points 299946

Si vous ne voulez vraiment pas que la deuxième commande soit exécutée tant que la première n'a pas réussi, vous devez probablement utiliser des fichiers temporaires. La version simple est la suivante :

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 en '>&2' ; cependant, une ancienne version du shell MKS ne gérait pas correctement la redirection des erreurs sans le '1' précédent. J'utilise donc depuis longtemps cette notation sans ambiguïté pour la fiabilité.

Cela fait fuir les fichiers si vous interrompez quelque chose. Utilisations de la programmation shell à l'épreuve des bombes (plus ou moins) :

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 piège 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 l'interpréteur de commandes quitte pour une raison quelconque). Si vous écrivez un shell script, le piège final n'a besoin que de supprimer le piège sur 0, qui est le piège 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 original, il est possible que 'c' lise les données de 'b' avant que 'a' n'ait terminé - ce qui est généralement souhaitable (cela donne du travail à plusieurs cœurs, 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 ses sorties.

Si vous voulez détecter quelle(s) commande(s) échoue(nt), 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 l'étendre à un pipeline à 4 ou N parties.

Une simple expérience avec 'set -e' n'a rien donné.

1 votes

Je recommande d'utiliser mktemp o tempfile .

0 votes

@Dennis : oui, je suppose que je devrais m'habituer à des commandes telles que mktemp ou tmpfile ; elles n'existaient pas au niveau du shell lorsque je l'ai appris, il y a tant d'années. Faisons une vérification rapide. Je trouve mktemp sur MacOS X ; j'ai mktemp sur Solaris mais seulement parce que j'ai installé les outils GNU ; il semble que mktemp soit présent sur l'antique HP-UX. Je ne suis pas sûr qu'il y ait une invocation commune de mktemp qui fonctionne sur toutes les plateformes. POSIX ne standardise ni mktemp ni tmpfile. Je n'ai pas trouvé tmpfile sur les plateformes auxquelles j'ai accès. Par conséquent, je ne pourrai pas utiliser les commandes dans des scripts d'interpréteur de commandes portables.

1 votes

Lorsque vous utilisez trap gardez à l'esprit que l'utilisateur peut toujours envoyer SIGKILL à un processus pour y mettre fin immédiatement, auquel cas votre piège ne prendra pas effet. Il en va de même lorsque votre système subit une coupure de courant. Lorsque vous créez des fichiers temporaires, assurez-vous d'utiliser mktemp parce que cela mettra vos fichiers quelque part, où ils seront nettoyés après un redémarrage (généralement dans /tmp ).

9voto

josch Points 790

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à fait remarquer, il n'est pas possible d'interrompre le pipe avant que les processus suivants ne soient lancés. Tous les processus sont lancés en même temps et s'exécuteront donc tous avant que des erreurs puissent être communiquées. Mais le titre de la question portait également sur les codes d'erreur. Ceux-ci peuvent être récupérés et examinés après la fin du pipe pour déterminer si l'un des processus impliqués a échoué.

Voici une solution qui attrape toutes les erreurs dans le pipe 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 retrouver todo 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é, un echo imprime sur l'erreur standard en cas d'échec d'une commande. Ensuite, la sortie combinée de l'erreur standard est enregistrée dans $res et enquêté 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 vers /dev/null ou le laisser comme un autre indicateur que quelque chose a mal tourné. Vous pouvez remplacer la dernière redirection par /dev/null avec un fichier si vous ne voulez pas stocker la sortie de la dernière commande quelque part.

Pour jouer davantage avec cette construction et pour vous convaincre que cela fait vraiment ce qu'il faut, j'ai remplacé ./a , ./b y ./c par des sous-shells qui exécutent echo , cat y exit . Vous pouvez l'utiliser pour vérifier que cette construction transmet réellement tous les résultats 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

1voto

Jasha Points 176

Cette réponse est dans l'esprit de la réponse acceptée, mais en utilisant des variables shell au lieu de fichiers temporaires.

if TMP_A="$(./a)"
then
 if TMP_B="$(echo "TMP_A" | ./b)"
 then
  if TMP_C="$(echo "TMP_B" | ./c)"
  then
   echo "$TMP_C"
  else
   echo "./c failed"
  fi
 else
  echo "./b failed"
 fi
else
 echo "./a failed"
fi

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X