it-swarm-fr.com

Obtenir l'état de sortie d'un processus acheminé vers un autre

J'ai deux processus foo et bar, connectés avec un tube:

$ foo | bar

bar quitte toujours 0; Je suis intéressé par le code de sortie de foo. Y a-t-il un moyen d'y arriver?

314
Michael Mrozek

Si vous utilisez bash, vous pouvez utiliser la variable de tableau PIPESTATUS pour obtenir l'état de sortie de chaque élément du pipeline.

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

Si vous utilisez zsh, leur tableau est appelé pipestatus (la casse compte!) Et les indices du tableau commencent à un:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

Pour les combiner au sein d'une fonction d'une manière qui ne perd pas les valeurs:

$ false | true
$ retval_bash="${PIPESTATUS[0]}" retval_zsh="${pipestatus[1]}" retval_final=$?
$ echo $retval_bash $retval_zsh $retval_final
1 0

Exécutez ce qui précède dans bash ou zsh et vous obtiendrez les mêmes résultats; un seul parmi retval_bash et retval_zsh sera défini. L'autre sera vide. Cela permettrait à une fonction de se terminer par return $retval_bash $retval_zsh (notez l'absence de guillemets!).

277
camh

Il existe 3 façons courantes de procéder:

Pipefail

La première consiste à définir l'option pipefail (ksh, zsh ou bash). C'est le plus simple et ce qu'il fait est essentiellement de définir l'état de sortie $? au code de sortie du dernier programme pour quitter différent de zéro (ou zéro si tous se sont terminés avec succès).

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$ PIPESTATUS

Bash possède également une variable de tableau appelée $PIPESTATUS ($pipestatus dans zsh) qui contient l'état de sortie de tous les programmes du dernier pipeline.

$ true | true; echo "${PIPESTATUS[@]}"
0 0
$ false | true; echo "${PIPESTATUS[@]}"
1 0
$ false | true; echo "${PIPESTATUS[0]}"
1
$ true | false; echo "${PIPESTATUS[@]}"
0 1

Vous pouvez utiliser le 3e exemple de commande pour obtenir la valeur spécifique dans le pipeline dont vous avez besoin.

Exécutions séparées

C'est la solution la plus lourde. Exécutez chaque commande séparément et capturez l'état

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
253
Patrick

Cette solution fonctionne sans utiliser de fonctionnalités spécifiques à bash ni de fichiers temporaires. Bonus: au final, le statut de sortie est en fait un statut de sortie et non une chaîne dans un fichier.

Situation:

someprog | filter

vous voulez l'état de sortie de someprog et la sortie de filter.

Voici ma solution:

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

le résultat de cette construction est stdout de filter comme stdout de la construction et l'état de sortie de someprog comme état de sortie de la construction.


cette construction fonctionne également avec un regroupement de commandes simple {...} au lieu de sous-coques (...). les sous-coques ont certaines implications, entre autres un coût de performance, dont nous n'avons pas besoin ici. lire le manuel fine bash pour plus de détails: https://www.gnu.org/software/bash/manual/html_node/Command-Grouping.html

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; } } 4>&1

Malheureusement, la grammaire bash nécessite des espaces et des points-virgules pour les accolades, de sorte que la construction devient beaucoup plus spacieuse.

Pour le reste de ce texte, j'utiliserai la variante du sous-shell.


Exemple someprog et filter:

someprog() {
  echo "line1"
  echo "line2"
  echo "line3"
  return 42
}

filter() {
  while read line; do
    echo "filtered $line"
  done
}

((((someprog; echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

echo $?

Exemple de sortie:

filtered line1
filtered line2
filtered line3
42

Remarque: le processus enfant hérite des descripteurs de fichiers ouverts du parent. Cela signifie que someprog héritera des descripteurs de fichiers ouverts 3 et 4. Si someprog écrit dans le descripteur de fichiers 3, cela deviendra l'état de sortie. Le véritable statut de sortie sera ignoré car read ne lit qu'une seule fois.

Si vous craignez que votre someprog puisse écrire dans le descripteur de fichier 3 ou 4, il est préférable de fermer les descripteurs de fichier avant d'appeler someprog.

(((((exec 3>&- 4>&-; someprog); echo $? >&3) | filter >&4) 3>&1) | (read xs; exit $xs)) 4>&1

Le exec 3>&- 4>&- avant someprog ferme le descripteur de fichier avant d'exécuter someprog donc pour someprog ces descripteurs de fichier n'existent tout simplement pas.

Il peut également s'écrire comme ceci: someprog 3>&- 4>&-


Explication étape par étape de la construction:

( ( ( ( someprog;          #part6
        echo $? >&3        #part5
      ) | filter >&4       #part4
    ) 3>&1                 #part3
  ) | (read xs; exit $xs)  #part2
) 4>&1                     #part1

De bas en haut:

  1. Un sous-shell est créé avec le descripteur de fichier 4 redirigé vers stdout. Cela signifie que tout ce qui est imprimé dans le descripteur de fichier 4 dans le sous-shell finira comme la sortie standard de la construction entière.
  2. Un tuyau est créé et les commandes à gauche (#part3) et à droite (#part2) sont exécutées. exit $xs est également la dernière commande du canal et cela signifie que la chaîne de stdin sera l'état de sortie de la construction entière.
  3. Un sous-shell est créé avec le descripteur de fichier 3 redirigé vers stdout. Cela signifie que tout ce qui est imprimé dans le descripteur de fichier 3 dans ce sous-shell se retrouvera dans #part2 et sera à son tour le statut de sortie de la construction entière.
  4. Un tuyau est créé et les commandes à gauche (#part5 et #part6) et à droite (filter >&4) sont exécutées. La sortie de filter est redirigée vers le descripteur de fichier 4. Dans #part1 le descripteur de fichier 4 a été redirigé vers stdout. Cela signifie que la sortie de filter est la sortie standard de la construction entière.
  5. Quitter le statut depuis #part6 est imprimé dans le descripteur de fichier 3. Dans #part3 Le descripteur de fichier 3 a été redirigé vers #part2. Cela signifie que l'état de sortie de #part6 sera le statut de sortie final pour la construction entière.
  6. someprog est exécuté. Le statut de sortie est pris dans #part5. La sortie standard est prise par le tuyau dans #part4 et transmis à filter. La sortie de filter atteindra à son tour stdout comme expliqué dans #part4
58
lesmana

Bien que ce ne soit pas exactement ce que vous avez demandé, vous pouvez utiliser

#!/bin/bash -o pipefail

de sorte que vos tuyaux retournent le dernier retour non nul.

pourrait être un peu moins de codage

Modifier: Exemple

[[email protected] ~]# false | true
[[email protected] ~]# echo $?
0
[[email protected] ~]# set -o pipefail
[[email protected] ~]# false | true
[[email protected] ~]# echo $?
1
37
Chris

Ce que je fais, lorsque cela est possible, consiste à introduire le code de sortie de foo dans bar. Par exemple, si je sais que foo ne produit jamais une ligne avec seulement des chiffres, alors je peux simplement clouer sur le code de sortie:

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

Ou si je sais que la sortie de foo ne contient jamais de ligne avec juste .:

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

Cela peut toujours être fait s'il existe un moyen de faire fonctionner bar sur toutes les lignes sauf la dernière et de passer la dernière ligne comme code de sortie.

Si bar est un pipeline complexe dont vous n'avez pas besoin de sortie, vous pouvez en contourner une partie en imprimant le code de sortie sur un descripteur de fichier différent.

exit_codes=$({ { foo; echo foo:"$?" >&3; } |
               { bar >/dev/null; echo bar:"$?" >&3; }
             } 3>&1)

Après ça $exit_codes est habituellement foo:X bar:Y, mais cela pourrait être bar:Y foo:X si bar se ferme avant de lire toutes ses entrées ou si vous n'avez pas de chance. Je pense que les écritures sur des canaux jusqu'à 512 octets sont atomiques sur tous les unités, donc le foo:$? et bar:$? les parties ne seront pas mélangées tant que les chaînes de balises sont inférieures à 507 octets.

Si vous devez capturer la sortie de bar, cela devient difficile. Vous pouvez combiner les techniques ci-dessus en faisant en sorte que la sortie de bar ne contienne jamais une ligne qui ressemble à une indication de code de sortie, mais cela devient compliqué.

output=$(echo;
         { { foo; echo foo:"$?" >&3; } |
           { bar | sed 's/^/^/'; echo bar:"$?" >&3; }
         } 3>&1)
nl='
'
foo_exit_code=${output#*${nl}foo:}; foo_exit_code=${foo_exit_code%%$nl*}
bar_exit_code=${output#*${nl}bar:}; bar_exit_code=${bar_exit_code%%$nl*}
output=$(printf %s "$output" | sed -n 's/^\^//p')

Et, bien sûr, il y a l'option simple de en utilisant un fichier temporaire pour stocker l'état. Simple, mais pas que simple en production:

  • Si plusieurs scripts s'exécutent simultanément ou si le même script utilise cette méthode à plusieurs endroits, vous devez vous assurer qu'ils utilisent des noms de fichiers temporaires différents.
  • Il est difficile de créer un fichier temporaire en toute sécurité dans un répertoire partagé. Souvent, /tmp est le seul endroit où un script est sûr de pouvoir écrire des fichiers. Utilisez mktemp , qui n'est pas POSIX mais disponible sur tous les appareils sérieux de nos jours.
foo_ret_file=$(mktemp -t)
{ foo; echo "$?" >"$foo_ret_file"; } | bar
bar_ret=$?
foo_ret=$(cat "$foo_ret_file"; rm -f "$foo_ret_file")

À partir du pipeline:

foo | bar | baz

Voici une solution générale utilisant uniquement le shell POSIX et aucun fichier temporaire:

exec 4>&1
error_statuses="`((foo || echo "0:$?" >&3) |
        (bar || echo "1:$?" >&3) | 
        (baz || echo "2:$?" >&3)) 3>&1 >&4`"
exec 4>&-

$error_statuses contient les codes d'état de tous les processus ayant échoué, dans un ordre aléatoire, avec des index pour indiquer quelle commande a émis chaque état.

# if "bar" failed, output its status:
echo "$error_statuses" | grep '1:' | cut -d: -f2

# test if all commands succeeded:
test -z "$error_statuses"

# test if the last command succeeded:
! echo "$error_statuses" | grep '2:' >/dev/null

Notez les citations autour de $error_statuses dans mes tests; sans eux, grep ne peut pas se différencier car les sauts de ligne sont contraints aux espaces.

17
Jander

Je voulais donc apporter une réponse comme celle de lesmana, mais je pense que la mienne est peut-être une solution pure-Bourne-Shell un peu plus simple et légèrement plus avantageuse:

# You want to pipe command1 through command2:
exec 4>&1
exitstatus=`{ { command1; printf $? 1>&3; } | command2 1>&4; } 3>&1`
# $exitstatus now has command1's exit status.

Je pense que cela s'explique mieux de l'intérieur - command1 exécutera et imprimera sa sortie régulière sur stdout (descripteur de fichier 1), puis une fois cela fait, printf exécutera et imprimera le code de sortie de command1 sur sa stdout, mais cette stdout est redirigée vers descripteur de fichier 3.

Pendant que command1 est en cours d'exécution, sa sortie standard est dirigée vers command2 (la sortie de printf ne parvient jamais à command2 car nous l'envoyons vers le descripteur de fichier 3 au lieu de 1, qui est ce que le tuyau lit). Ensuite, nous redirige la sortie de command2 vers le descripteur de fichier 4, afin qu'il reste également en dehors du descripteur de fichier 1 - parce que nous voulons que le descripteur de fichier 1 soit gratuit un peu plus tard, car nous ramènerons la sortie printf du descripteur de fichier 3 dans le descripteur de fichier 1 - parce que c'est ce que la substitution de commandes (les backticks) capturera et c'est ce qui sera placé dans la variable.

Le dernier morceau de magie est que d'abord exec 4>&1 Nous l'avons fait en tant que commande distincte - il ouvre le descripteur de fichier 4 comme une copie de la sortie standard du shell externe. La substitution de commandes capturera tout ce qui est écrit sur la norme du point de vue des commandes à l'intérieur - mais, puisque la sortie de command2 va dans le descripteur de fichier 4 en ce qui concerne la substitution de commandes, la substitution de commandes ne le capture pas - cependant, une fois qu'il "sort" de la substitution de commandes, il va effectivement toujours au descripteur de fichier global 1 du script.

(Le exec 4>&1 Doit être une commande distincte car de nombreux shells courants ne l'aiment pas lorsque vous essayez d'écrire dans un descripteur de fichier à l'intérieur d'une substitution de commande, qui est ouvert dans la commande "externe" qui utilise le C'est donc le moyen portable le plus simple de le faire.)

Vous pouvez le regarder d'une manière moins technique et plus ludique, comme si les sorties des commandes se dépassaient les unes les autres: command1 redirige vers command2, puis la sortie du printf saute par-dessus la commande 2 afin que la commande2 ne l'attrape pas, puis la sortie de la commande 2 saute au-dessus et en dehors de la substitution de commande juste au moment où printf atterrit juste à temps pour être capturé par la substitution afin qu'il finisse dans la variable, et la sortie de command2 continue à être joyeusement écrite sur la sortie standard, tout comme dans un tuyau normal.

De plus, si je comprends bien, $? Contiendra toujours le code retour de la deuxième commande dans le canal, car les affectations de variables, les substitutions de commandes et les commandes composées sont toutes effectivement transparentes pour le code retour de la commande qu'elles contiennent. , donc le statut de retour de command2 devrait se propager - ceci, et ne pas avoir à définir de fonction supplémentaire, c'est pourquoi je pense que cela pourrait être une meilleure solution que celle proposée par lesmana.

Selon les mises en garde mentionnées par lesmana, il est possible que command1 finisse à un moment donné par utiliser les descripteurs de fichiers 3 ou 4, donc pour être plus robuste, vous feriez:

exec 4>&1
exitstatus=`{ { command1 3>&-; printf $? 1>&3; } 4>&- | command2 1>&4; } 3>&1`
exec 4>&-

Notez que j'utilise des commandes composées dans mon exemple, mais des sous-coquilles (en utilisant ( ) Au lieu de { } Fonctionneront également, bien qu'elles soient peut-être moins efficaces.)

Les commandes héritent des descripteurs de fichiers du processus qui les lance, donc la deuxième ligne entière héritera du descripteur de fichiers quatre, et la commande composée suivie de 3>&1 Héritera du descripteur de fichiers trois. Ainsi, le 4>&- S'assure que la commande interne composée n'héritera pas du descripteur de fichier quatre, et le 3>&- N'héritera pas du descripteur de fichier trois, de sorte que la commande1 obtient un environnement "plus propre" et plus standard. Vous pouvez également déplacer le 4>&- À côté du 3>&-, Mais je me demande pourquoi ne pas limiter autant que possible sa portée.

Je ne sais pas combien de fois les choses utilisent directement les descripteurs de fichiers trois et quatre - je pense que la plupart du temps, les programmes utilisent des appels système qui renvoient des descripteurs de fichiers non utilisés pour le moment, mais parfois le code écrit directement dans le descripteur de fichier 3, je guess (je pourrais imaginer un programme vérifiant un descripteur de fichier pour voir s'il est ouvert, et l'utilisant s'il l'est, ou se comportant différemment en conséquence s'il ne l'est pas). Il est donc préférable de garder ce dernier à l'esprit et de l'utiliser pour les cas à usage général.

12
mtraceur

Si le package moreutils est installé, vous pouvez utiliser l'utilitaire de mise en erreur qui fait exactement ce que vous avez demandé.

11
Emanuele Aina

la solution de lesmana ci-dessus peut également être effectuée sans la surcharge de démarrage des sous-processus imbriqués en utilisant { .. } à la place (en se rappelant que cette forme de commandes groupées doit toujours se terminer par des points-virgules). Quelque chose comme ça:

{ { { { someprog; echo $? >&3; } | filter >&4; } 3>&1; } | stdintoexitstatus; } 4>&1

J'ai vérifié cette construction avec la version de tiret 0.5.5 et les versions bash 3.2.25 et 4.2.42, donc même si certains shells ne supportent pas { .. } groupement, il est toujours conforme POSIX.

7
pkeller

Ce qui suit est conçu comme un complément à la réponse de @Patrik, au cas où vous ne seriez pas en mesure d'utiliser l'une des solutions courantes.

Cette réponse suppose ce qui suit:

  • Vous avez un Shell qui ne connaît pas $PIPESTATUS ni set -o pipefail
  • Vous souhaitez utiliser un canal pour l'exécution parallèle, donc pas de fichiers temporaires.
  • Vous ne voulez pas avoir d'encombrement supplémentaire si vous interrompez le script, peut-être par une coupure de courant soudaine.
  • Cette solution doit être relativement facile à suivre et propre à lire.
  • Vous ne souhaitez pas introduire de sous-coquilles supplémentaires.
  • Vous ne pouvez pas jouer avec les descripteurs de fichiers existants, donc stdin/out/err ne doit pas être touché (cependant vous pouvez en introduire de nouveaux temporairement)

Hypothèses supplémentaires. Vous pouvez vous débarrasser de tout, mais cela encombre trop la recette, donc ce n'est pas couvert ici:

  • Tout ce que vous voulez savoir, c'est que toutes les commandes du PIPE ont le code de sortie 0.
  • Vous n'avez pas besoin d'informations supplémentaires sur la bande latérale.
  • Votre shell attend le retour de toutes les commandes de canal.

Avant: foo | bar | baz, mais cela ne renvoie que le code de sortie de la dernière commande (baz)

Voulait: $? ne doit pas être 0 (vrai), si l'une des commandes du tube a échoué

Après:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo || echo $? >&9; } |
{ bar || echo $? >&9; } |
{ baz || echo $? >&9; }
#wait
! read TMPRESULTS <&8
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

# $? now is 0 only if all commands had exit code 0

Expliqué:

  • Un fichier temporaire est créé avec mktemp. Cela crée généralement immédiatement un fichier dans /tmp
  • Ce fichier temporaire est ensuite redirigé vers FD 9 pour l'écriture et FD 8 pour la lecture
  • Ensuite, le fichier temporaire est immédiatement supprimé. Il reste ouvert, cependant, jusqu'à ce que les deux FD disparaissent.
  • Maintenant, le tuyau est démarré. Chaque étape s'ajoute à FD 9 uniquement, en cas d'erreur.
  • wait est nécessaire pour ksh, car ksh else n'attend pas la fin de toutes les commandes de canal. Cependant, veuillez noter qu'il y a des effets secondaires indésirables si certaines tâches d'arrière-plan sont présentes, donc je l'ai commenté par défaut. Si l'attente ne fait pas de mal, vous pouvez la commenter.
  • Ensuite, le contenu du fichier est lu. S'il est vide (car tout a fonctionné) read renvoie false, donc true indique une erreur

Cela peut être utilisé comme remplacement de plugin pour une seule commande et ne nécessite que les éléments suivants:

  • FD 9 et 8 inutilisés
  • Une seule variable d'environnement pour contenir le nom du fichier temporaire
  • Et cette recette peut être adaptée à pratiquement tous les Shell qui permettent la redirection IO
  • De plus, il est assez indépendant de la plateforme et n'a pas besoin de choses comme /proc/fd/N

Bugs:

Ce script contient un bogue au cas où /tmp manque d'espace. Si vous avez également besoin d'une protection contre ce boîtier artificiel, vous pouvez le faire comme suit, mais cela a l'inconvénient que le nombre de 0 dans 000 dépend du nombre de commandes dans le tube, donc c'est un peu plus compliqué:

TMPRESULTS="`mktemp`"
{
rm -f "$TMPRESULTS"

{ foo; printf "%1s" "$?" >&9; } |
{ bar; printf "%1s" "$?" >&9; } |
{ baz; printf "%1s" "$?" >&9; }
#wait
read TMPRESULTS <&8
[ 000 = "$TMPRESULTS" ]
} 9>>"$TMPRESULTS" 8<"$TMPRESULTS"

Notes sur la portabilité:

  • ksh et les shells similaires qui n'attendent que la dernière commande pipe ont besoin de wait sans commentaire

  • Le dernier exemple utilise printf "%1s" "$?" au lieu de echo -n "$?" parce que c'est plus portable. Toutes les plateformes n'interprètent pas -n correctement.

  • printf "$?" le ferait aussi bien, cependant printf "%1s" attrape quelques cas d'angle au cas où vous exécuteriez le script sur une plateforme vraiment cassée. (Lire: s'il vous arrive de programmer en paranoia_mode=extreme.)

  • FD 8 et FD 9 peuvent être supérieurs sur les plates-formes qui prennent en charge plusieurs chiffres. AFAIR un Shell conforme POSIX n'a ​​besoin que de prendre en charge des chiffres uniques.

  • A été testé avec Debian 8.2 sh, bash, ksh, ash, sash et même csh

5
Tino

C'est portable, c'est-à-dire qu'il fonctionne avec n'importe quel shell compatible POSIX, ne nécessite pas que le répertoire en cours soit accessible en écriture et permet à plusieurs scripts utilisant la même astuce de s'exécuter simultanément.

(foo;echo $?>/tmp/_$$)|(bar;exit $(cat /tmp/_$$;rm /tmp/_$$))

Edit: voici une version plus forte suite aux commentaires de Gilles:

(s=/tmp/.$$_$RANDOM;((foo;echo $?>$s)|(bar)); exit $(cat $s;rm $s))

Edit2: et voici une variante légèrement plus légère suite au commentaire de dubiousjim:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
4
jlliagre

Avec un peu de précaution, cela devrait fonctionner:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
3
alex

Le bloc 'if' suivant ne s'exécutera que si la 'commande' a réussi:

if command; then
   # ...
fi

Plus précisément, vous pouvez exécuter quelque chose comme ceci:

haconf_out=/path/to/some/temporary/file

if haconf -makerw > "$haconf_out" 2>&1; then
   grep -iq "Cluster already writable" "$haconf_out"
   # ...
fi

Qui s'exécutera haconf -makerw et stocke ses stdout et stderr dans "$ haconf_out". Si la valeur renvoyée par haconf est vraie, le bloc 'if' sera exécuté et grep lira "$ haconf_out", essayant de le faire correspondre avec "Cluster déjà accessible en écriture".

Notez que les tuyaux se nettoient automatiquement; avec la redirection, vous devrez faire attention à supprimer "$ haconf_out" une fois terminé.

Pas aussi élégant que pipefail, mais une alternative légitime si cette fonctionnalité n'est pas à portée de main.

2
Rany Albeg Wein
Alternate example for @lesmana solution, possibly simplified.
Provides logging to file if desired.
=====
$ cat z.sh
TEE="cat"
#TEE="tee z.log"
#TEE="tee -a z.log"

exec 8>&- 9>&-
{
  {
    {
      { #BEGIN - add code below this line and before #END
./zz.sh
echo ${?} 1>&8  # use exactly 1x prior to #END
      #END
      } 2>&1 | ${TEE} 1>&9
    } 8>&1
  } | exit $(read; printf "${REPLY}")
} 9>&1

exit ${?}
$ cat zz.sh
echo "my script code..."
exit 42
$ ./z.sh; echo "status=${?}"
my script code...
status=42
$
1
C.G.