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?
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!).
Il existe 3 façons courantes de procéder:
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
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.
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
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:
#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.#part2
et sera à son tour le statut de sortie de la construction entière.#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.#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.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
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
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1
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:
/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.
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.
Si le package moreutils est installé, vous pouvez utiliser l'utilitaire de mise en erreur qui fait exactement ce que vous avez demandé.
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.
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:
$PIPESTATUS
ni set -o pipefail
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é:
mktemp
. Cela crée généralement immédiatement un fichier dans /tmp
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.read
renvoie false
, donc true
indique une erreurCela peut être utilisé comme remplacement de plugin pour une seule commande et ne nécessite que les éléments suivants:
/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
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))
Avec un peu de précaution, cela devrait fonctionner:
foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
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.
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
$