Sortie du tuyau et capture de l'état de sortie dans Bash

421

Je veux exécuter une commande longue dans Bash, capturer son état de sortie et envoyer sa sortie.

Alors je fais ça:

command | tee out.txt
ST=$?

Le problème est que la variable ST capture l'état de sortie de teeet non de commande. Comment puis-je resoudre ceci?

Notez que la commande est longue et rediriger la sortie vers un fichier pour la visualiser plus tard n'est pas une bonne solution pour moi.

fil volant
la source
1
[["$ {PIPESTATUS [@]}" = ~ [^ 0 \]]] && echo -e "Correspondance - erreur trouvée" || echo -e "No match - all good" Ceci testera toutes les valeurs du tableau à la fois et donnera un message d'erreur si l'une des valeurs de pipe retournées n'est pas nulle. Il s'agit d'une solution généralisée assez robuste pour détecter les erreurs dans une situation canalisée.
Brian S. Wilson
unix.stackexchange.com/questions/14270/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Réponses:

519

Il existe une variable Bash interne appelée $PIPESTATUS; c'est un tableau qui contient l'état de sortie de chaque commande dans votre dernier pipeline de commandes de premier plan.

<command> | tee out.txt ; test ${PIPESTATUS[0]} -eq 0

Ou une autre alternative qui fonctionne également avec d'autres shells (comme zsh) serait d'activer pipefail:

set -o pipefail
...

La première option ne fonctionne pas en zshraison d'une syntaxe légèrement différente.

codar
la source
21
Il y a une bonne explication avec des exemples de PIPESTATUS ET Pipefail ici: unix.stackexchange.com/a/73180/7453 .
slm
18
Remarque: $ PIPESTATUS [0] contient l'état de sortie de la première commande dans le canal, $ PIPESTATUS [1] l'état de sortie de la deuxième commande, etc.
simpleuser
18
Bien sûr, nous devons nous rappeler que cela est spécifique à Bash: si je devais (par exemple) écrire un script à exécuter sur l'implémentation "sh" de BusyBox sur mon appareil Android, ou sur une autre plate-forme intégrée utilisant un autre "sh" variante, cela ne fonctionnerait pas.
Asfand Qazi
4
Pour ceux qui s'inquiètent de l'expansion des variables sans guillemets: le statut de sortie est toujours un entier 8 bits non signé dans Bash , il n'est donc pas nécessaire de le citer. Cela vaut également pour Unix, généralement, où le statut de sortie est défini explicitement sur 8 bits et est supposé non signé même par POSIX lui-même, par exemple lors de la définition de sa négation logique .
Palec
3
Vous pouvez également utiliser exit ${PIPESTATUS[0]}.
Chaoran
142

utiliser bash set -o pipefailest utile

pipefail: la valeur de retour d'un pipeline est l'état de la dernière commande à quitter avec un état non nul, ou zéro si aucune commande n'est sortie avec un état non nul

Felipe Alvarez
la source
23
Dans le cas où vous ne souhaitez pas modifier le paramètre pipefail de l'ensemble du script, vous pouvez définir l'option uniquement localement:( set -o pipefail; command | tee out.txt ); ST=$?
Jaan
7
@Jaan Cela exécuterait un sous-shell. Si vous voulez éviter cela, vous pouvez faire set -o pipefailet ensuite faire la commande, et immédiatement après set +o pipefailpour désactiver l'option.
Linus Arver
2
Remarque: l'affiche de la question ne veut pas d'un "code de sortie général" du canal, il veut le code retour de "commande". Avec, -o pipefailil saurait si le tuyau échoue, mais si à la fois «commande» et «tee» échouent, il recevra le code de sortie de «tee».
t0r0X
@LinusArver n'effacerait-il pas le code de sortie car c'est une commande qui réussit?
carlin.scott
127

Solution stupide: les connecter via un canal nommé (mkfifo). Ensuite, la commande peut être exécutée en second.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
EFraim
la source
20
C'est la seule réponse à cette question qui fonctionne également pour le simple shell sh Unix. Merci!
JamesThomasMoon1979
3
@DaveKennedy: idiot comme dans "évident, ne nécessitant pas de connaissance complexe de la syntaxe bash"
EFraim
10
Bien que les réponses bash soient plus élégantes lorsque vous avez l'avantage des capacités supplémentaires de bash, il s'agit de la solution la plus multiplateforme. C'est aussi quelque chose qui mérite réflexion en général, car chaque fois que vous effectuez une commande de longue durée, un canal de nom est souvent le moyen le plus flexible. Il convient de noter que certains systèmes n'en ont pas mkfifoet peuvent à la place nécessiter mknod -psi je me souviens bien.
Haravikk
3
Parfois, en cas de débordement de pile, il y a des réponses que vous voteriez cent fois pour que les gens cessent de faire d'autres choses qui n'ont aucun sens, c'est l'une d'entre elles. Merci Monsieur.
Dan Chase
1
Dans le cas où quelqu'un a un problème avec mkfifoou mknod -p: dans mon cas, la commande appropriée pour créer le fichier pipe était mknod FILE_NAME p.
Karol Gil
36

Il y a un tableau qui vous donne le statut de sortie de chaque commande dans un tube.

$ cat x| sed 's///'
cat: x: No such file or directory
$ echo $?
0
$ cat x| sed 's///'
cat: x: No such file or directory
$ echo ${PIPESTATUS[*]}
1 0
$ touch x
$ cat x| sed 's'
sed: 1: "s": substitute pattern can not be delimited by newline or backslash
$ echo ${PIPESTATUS[*]}
0 1
Stefano Borini
la source
26

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 someproget la sortie de filter.

Voici ma solution:

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

echo $?

Voir ma réponse pour la même question sur unix.stackexchange.com pour une explication détaillée et une alternative sans sous-coquilles et quelques mises en garde.

lesmana
la source
20

En combinant PIPESTATUS[0]et le résultat de l'exécution de la exitcommande dans un sous-shell, vous pouvez accéder directement à la valeur de retour de votre commande initiale:

command | tee ; ( exit ${PIPESTATUS[0]} )

Voici un exemple:

# the "false" shell built-in command returns 1
false | tee ; ( exit ${PIPESTATUS[0]} )
echo "return value: $?"

te donnera:

return value: 1

par
la source
4
Merci, cela m'a permis d'utiliser la construction: VALUE=$(might_fail | piping)qui ne définira pas PIPESTATUS dans le shell principal mais définira son niveau d'erreur. En utilisant: VALUE=$(might_fail | piping; exit ${PIPESTATUS[0]})je reçois ce que je voulais.
vaab
@vaab, cette syntaxe a l'air vraiment sympa mais je suis confus sur ce que signifie «piping» dans votre contexte? Est-ce juste là que l'on ferait «tee» ou tout autre traitement sur la sortie de might_fail? ty!
AnneTheAgile
1
@AnneTheAgile 'piping' dans mon exemple représente les commandes dont vous ne voulez pas voir l'errlvl. Par exemple: une ou n'importe quelle combinaison piped de 'tee', 'grep', 'sed', ... Il n'est pas si rare que ces commandes de piping soient destinées à formater ou extraire des informations à partir d'une sortie plus grande ou d'une sortie de journal du principal commande: vous êtes alors plus intéressé par le niveau d'erreur de la commande principale (celle que j'ai appelée 'might_fail' dans mon exemple) mais sans ma construction, l'affectation entière renvoie le dernier errlvl de la commande canalisée qui n'a ici aucun sens. Est-ce plus clair?
vaab
command_might_fail | grep -v "line_pattern_to_exclude" || exit ${PIPESTATUS[0]}au cas où pas le tee mais le filtrage grep
user1742529
12

J'ai donc voulu 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 d'icommand1 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 exec 4>&1nous l'avons d' abord fait comme une 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 la capture pas - cependant une fois obtient "hors" de la substitution de commande, il va effectivement toujours au descripteur de fichier global du script 1.

(La exec 4>&1doit ê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 la substitution. 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.

Aussi, 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 à l'intérieur, donc l'état 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 que mentionne lesmana, il est possible que command1 finisse par utiliser des 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 les sous-coquilles (l'utilisation à la ( )place de { }fonctionnera également, bien que peut-être moins efficace.)

Les commandes héritent des descripteurs de fichiers du processus qui les lance, de sorte que la deuxième ligne entière héritera du descripteur de fichiers quatre, et la commande composée suivie de 3>&1héritera du descripteur de fichiers trois. Ainsi, la commande 4>&-s'assure que la commande interne composée n'héritera pas du descripteur de fichier quatre et 3>&-qu'elle n'héritera pas du descripteur de fichier trois, de sorte que command1 obtient un environnement plus propre et plus standard. Vous pouvez également déplacer l'intérieur à 4>&-côté du 3>&-, mais je pense 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.

mtraceur
la source
Belle explication!
selurvedu
6

Dans Ubuntu et Debian, vous pouvez apt-get install moreutils. Il contient un utilitaire appelé mispipequi renvoie l'état de sortie de la première commande du canal.

Bryan Larsen
la source
5
(command | tee out.txt; exit ${PIPESTATUS[0]})

Contrairement à la réponse de @ cODAR, cela renvoie le code de sortie d'origine de la première commande et pas seulement 0 pour le succès et 127 pour l'échec. Mais comme @Chaoran l'a souligné, vous pouvez simplement appeler ${PIPESTATUS[0]}. Il est cependant important que tout soit mis entre parenthèses.

jakob-r
la source
4

En dehors de bash, vous pouvez faire:

bash -o pipefail  -c "command1 | tee output"

C'est utile par exemple dans les scripts ninja où le shell devrait se trouver /bin/sh.

Anthony Scemama
la source
3

PIPESTATUS [@] doit être copié dans un tableau immédiatement après le retour de la commande pipe. Toute lecture de PIPESTATUS [@] effacera le contenu. Copiez-le dans un autre tableau si vous prévoyez de vérifier l'état de toutes les commandes de canalisation. "$?" est la même valeur que le dernier élément de "$ {PIPESTATUS [@]}", et sa lecture semble détruire "$ {PIPESTATUS [@]}", mais je ne l'ai pas absolument vérifié.

declare -a PSA  
cmd1 | cmd2 | cmd3  
PSA=( "${PIPESTATUS[@]}" )

Cela ne fonctionnera pas si le tuyau est dans un sous-shell. Pour une solution à ce problème,
voir bash pipestatus dans la commande backticked?

maxdev137
la source
3

La façon la plus simple de le faire en simple bash est d'utiliser la substitution de processus au lieu d'un pipeline. Il existe plusieurs différences, mais elles n'ont probablement pas beaucoup d'importance pour votre cas d'utilisation:

  • Lors de l'exécution d'un pipeline, bash attend que tous les processus soient terminés.
  • L'envoi de Ctrl-C vers bash le fait tuer tous les processus d'un pipeline, pas seulement le principal.
  • L' pipefailoption et la PIPESTATUSvariable ne sont pas pertinentes pour traiter la substitution.
  • Peut-être plus

Avec la substitution de processus, bash démarre simplement le processus et l'oublie, il n'est même pas visible dans jobs.

Mises à part les différences, consumer < <(producer)et producer | consumersont essentiellement équivalentes.

Si vous voulez inverser lequel est le processus "principal", il vous suffit de retourner les commandes et la direction de la substitution vers producer > >(consumer). Dans ton cas:

command > >(tee out.txt)

Exemple:

$ { echo "hello world"; false; } > >(tee out.txt)
hello world
$ echo $?
1
$ cat out.txt
hello world

$ echo "hello world" > >(tee out.txt)
hello world
$ echo $?
0
$ cat out.txt
hello world

Comme je l'ai dit, il y a des différences par rapport à l'expression pipe. Le processus peut ne jamais s'arrêter, sauf s'il est sensible à la fermeture du tuyau. En particulier, il peut continuer d'écrire des choses sur votre sortie standard, ce qui peut prêter à confusion.

clacke
la source
1

Solution de coque pure:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (cat || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
hello world

Et maintenant avec le second catremplacé par false:

% rm -f error.flag; echo hello world \
| (cat || echo "First command failed: $?" >> error.flag) \
| (false || echo "Second command failed: $?" >> error.flag) \
| (cat || echo "Third command failed: $?" >> error.flag) \
; test -s error.flag  && (echo Some command failed: ; cat error.flag)
Some command failed:
Second command failed: 1
First command failed: 141

Veuillez noter que le premier chat échoue également, car sa sortie est fermée. L'ordre des commandes ayant échoué dans le journal est correct dans cet exemple, mais ne vous y fiez pas.

Cette méthode permet de capturer stdout et stderr pour les commandes individuelles afin que vous puissiez ensuite les vider également dans un fichier journal si une erreur se produit, ou simplement les supprimer si aucune erreur (comme la sortie de dd).

Coroos
la source
1

Base sur la réponse de @ brian-s-wilson; cette fonction d'assistance bash:

pipestatus() {
  local S=("${PIPESTATUS[@]}")

  if test -n "$*"
  then test "$*" = "${S[*]}"
  else ! [[ "${S[@]}" =~ [^0\ ] ]]
  fi
}

utilisé ainsi:

1: get_bad_things doit réussir, mais il ne doit produire aucune sortie; mais nous voulons voir la sortie qu'elle produit

get_bad_things | grep '^'
pipeinfo 0 1 || return

2: tout pipeline doit réussir

thing | something -q | thingy
pipeinfo || return
Sam Liddicott
la source
1

Il peut parfois être plus simple et plus clair d'utiliser une commande externe, plutôt que de fouiller dans les détails de bash. pipeline , à partir du langage de script de processus minimal execline , se termine avec le code retour de la deuxième commande *, tout comme le fait un shpipeline, mais contrairement à shcela, il permet d'inverser la direction du tuyau, afin que nous puissions capturer le code retour du producteur processus (ce qui suit est tout sur la shligne de commande, mais avec execlineinstallé):

$ # using the full execline grammar with the execlineb parser:
$ execlineb -c 'pipeline { echo "hello world" } tee out.txt'
hello world
$ cat out.txt
hello world

$ # for these simple examples, one can forego the parser and just use "" as a separator
$ # traditional order
$ pipeline echo "hello world" "" tee out.txt 
hello world

$ # "write" order (second command writes rather than reads)
$ pipeline -w tee out.txt "" echo "hello world"
hello world

$ # pipeline execs into the second command, so that's the RC we get
$ pipeline -w tee out.txt "" false; echo $?
1

$ pipeline -w tee out.txt "" true; echo $?
0

$ # output and exit status
$ pipeline -w tee out.txt "" sh -c "echo 'hello world'; exit 42"; echo "RC: $?"
hello world
RC: 42
$ cat out.txt
hello world

L'utilisation pipelineprésente les mêmes différences avec les pipelines bash natifs que la substitution de processus bash utilisée dans la réponse # 43972501 .

* En fait, pipelinene se ferme pas du tout sauf en cas d'erreur. Il s'exécute dans la deuxième commande, c'est donc la deuxième commande qui fait le retour.

clacke
la source