Sortie du tube et statut de sortie de capture dans Bash

je veux exécuter une commande de longue durée dans Bash, et tous les deux capturer son statut de sortie, et tee sa sortie.

alors je fais ceci:

command | tee out.txt
ST=$?

le problème est que la variable ST capture l'état de sortie de tee et non de commande. Comment je peux résoudre ça?

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

344
demandé sur codeforester 2009-08-03 15:31:52

15 réponses

il y a une variable interne de Bash appelée $PIPESTATUS ; c'est un tableau qui tient le statut de sortie de chaque commande dans votre dernier pipeline de commandes à l'avant-plan.

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

ou une autre alternative qui fonctionne aussi avec d'autres coques (comme zsh) serait d'activer pipefail:

set -o pipefail
...

la première option ne pas travailler avec zsh en raison d'un peu de syntaxe différente.

431
répondu cODAR 2016-12-21 17:17:14

bash set -o pipefail est utile

tuyau: la valeur de retour d'un pipeline est le statut de la dernière commande pour sortir avec un statut non-zéro, ou zéro si aucune commande n'est sortie avec un statut non nul

127
répondu Felipe Alvarez 2014-02-04 08:14:11

Dumb solution: Relier entre eux par l'intermédiaire d'un tube nommé (mkfifo). Ensuite, vous pouvez exécuter la commande seconde.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
99
répondu EFraim 2015-03-14 15:26:39

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

$ 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
34
répondu Stefano Borini 2013-03-30 08:41:45

cette solution fonctionne sans utiliser des caractéristiques spécifiques de bash ou des fichiers temporaires. Bonus: à la fin, le statut de sortie est en fait un statut de sortie et non une chaîne dans un fichier.

:

someprog | filter

vous voulez le statut 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

echo $?

Voir ma réponse à la même question sur unix.stackexchange.com pour une explication détaillée de la façon dont cela fonctionne, et quelques mises en garde.

21
répondu lesmana 2018-01-10 13:27:29

en combinant PIPESTATUS[0] et le résultat de l'exécution de la commande exit dans un sous-puits, 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: $?"

vous donnera:

return value: 1

17
répondu par 2013-08-18 03:56:19

J'ai donc voulu donner une réponse comme celle de lesmana, mais je pense que la mienne est peut-être un peu plus simple et un peu plus avantageux solution pure-Bourne-shell:

# 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 c'est mieux expliqué à partir de l'intérieur out - command1 exécutera et imprimera sa sortie régulière sur stdout (descripteur de fichier 1), puis une fois qu'il est fait, printf exécutera et imprimera le code de sortie d'icommand1 sur son stdout, mais que stdout est redirigé vers le descripteur de fichier 3.

pendant que la commande 1 est en cours d'exécution, son stdout est redirigé vers la commande 2 (la sortie de printf ne le rend jamais vers la commande 2 parce que nous l'envoyons vers le descripteur de fichier 3 au lieu de 1, ce qui est ce que le tube lit). Ensuite, nous redirigeons la sortie de la commande 2 vers le descripteur de fichier 4, de sorte qu'il reste également en dehors du descripteur de fichier 1-parce que nous voulons le descripteur de fichier 1 libre pour un peu plus tard, parce que nous ramènerons la sortie printf sur le descripteur de fichier 3 vers le bas dans le descripteur de fichier 1 - parce que c'est ce que la substitution de commande (les backticks), va capturer et c'est ce qui va être placé dans la variable.

le dernier morceau de magie est que le premier exec 4>&1 que nous avons fait comme une commande séparée - il ouvre le descripteur de fichier 4 comme une copie du stdout du shell externe. La substitution de commande va capturer tout ce qui est écrit sur standard du point de vue des commandes à l'intérieur - mais puisque la sortie de la commande 2 va au descripteur de fichier 4 aussi loin que la commande la substitution est concernée, la substitution de commande ne la saisit pas - cependant une fois qu'elle sort de la substitution de commande, elle va effectivement toujours dans le descripteur de fichier Global 1 du script.

(le exec 4>&1 doit être une commande séparée parce que beaucoup de shells communs ne l'aiment pas quand vous essayez d'écrire à un descripteur de fichier à l'intérieur d'une substitution de commande, qui est ouverte dans la commande "externe" qui utilise la substitution. Donc, c'est le plus simple portable façon de le faire.)

vous pouvez le regarder d'une manière moins technique et plus ludique, comme si les sorties des commandes se sautaient l'une l'autre: la commande 1 passe à la commande 2, puis la sortie de l'imprimante saute sur la commande 2 pour que la commande 2 ne la rattrape pas, et puis la sortie de la commande 2 saute sur la substitution de commande tout comme la commande printf atterrit juste à temps pour être capturée par la substitution de sorte qu'elle se retrouve dans la variable, et la sortie de la commande 2 va sur son joyeux comme dans un tube normal.

aussi, comme je le comprends, $? contiendra toujours le code de retour de la deuxième commande dans le tuyau, parce que les affectations variables, les substitutions de commande, et les commandes composées sont toutes effectivement transparentes au code de retour de la commande à l'intérieur d'eux, donc le statut de retour de command2 devrait se propager dehors - ceci, et ne pas avoir à définir une fonction supplémentaire, est pourquoi je pense que ceci peut-être une meilleure solution que celle proposée par lesmana.

selon les mises en garde mentionnées par lesmana, il est possible que la commande 1 finisse par utiliser les descripteurs de fichier 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 subshells (en utilisant ( ) au lieu de { } fonctionnera aussi, bien que peut-être moins efficace.)

Les commandes héritent des descripteurs de fichier du processus qui les lance, donc toute la deuxième ligne héritera du descripteur de fichier quatre, et la commande composée suivie de 3>&1 héritera du descripteur de fichier trois. Ainsi, le 4>&- s'assure que la commande de composé interne n'héritera pas du descripteur de fichier quatre, et le 3>&- n'héritera pas du descripteur de fichier trois, donc la commande 1 obtient un environnement plus propre et plus standard. Vous pouvez également déplacer l'intérieur 4>&- à côté de le 3>&- , mais je me demande pourquoi ne pas limiter son champ d'application autant que possible.

Je ne sais pas combien de fois les choses utilisent le descripteur de fichier trois et quatre directement - je pense que la plupart du temps les programmes utilisent des syscalls qui retournent des descripteurs de fichier non-utilisés-au-moment, mais parfois le code écrit au descripteur de fichier 3 directement, je suppose (je pourrais imaginer un programme vérifiant un descripteur de fichier pour voir si elle est ouverte, et l'utiliser si elle est, ou se comporter différemment en conséquence si elle est pas.) Il est donc probablement préférable de garder ce dernier à l'esprit et de l'utiliser dans des cas généraux.

8
répondu mtraceur 2016-10-13 06:33:11

dans Ubuntu et Debian, vous pouvez apt-get install moreutils . Il contient un utilitaire appelé mispipe qui renvoie l'état de sortie de la première commande dans le tuyau.

4
répondu Bryan Larsen 2013-12-13 18:33:04

PIPESTATUS [@] doit être copié dans un tableau immédiatement après le retour de la commande de pipe. Tout lit de PIPESTATUS[@] permet d'effacer le contenu. Copiez - le dans un autre tableau si vous prévoyez de vérifier l'état de toutes les commandes de pipe. "$?"est la même valeur que le dernier élément de" ${PIPESTATUS[@]}", et la 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 backticked command?

3
répondu maxdev137 2017-05-23 12:34:59
(command | tee out.txt; exit ${PIPESTATUS[0]})

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

3
répondu jakob-r 2017-09-25 08:42:27

en dehors de bash, vous pouvez faire:

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

ceci est utile par exemple dans les scripts ninja où le shell est censé être /bin/sh .

2
répondu Anthony Scemama 2016-02-17 18:53:38

la façon la plus simple de faire cela en bash simple est d'utiliser substitution de processus au lieu d'un pipeline. Il y a plusieurs différences, mais elles ne sont probablement pas très importantes pour votre cas d'utilisation:

  • lorsqu'il exécute un pipeline, bash attend jusqu'à ce que tous les processus soient terminés.
  • envoyer Ctrl-C à bash le fait tuer tous les processus d'un pipeline, pas seulement le principal.
  • le L'option pipefail et la variable PIPESTATUS ne sont pas pertinentes pour la substitution de procédés.
  • peut-être plus

avec substitution de processus, bash ne fait que commencer le processus et l'oublie, il n'est même pas visible dans jobs .

a mentionné les différences mises à part, consumer < <(producer) et producer | consumer sont essentiellement équivalents.

Si vous voulez retourner le "principal", vous n' retourner les commandes et la direction de la substitution, à producer > >(consumer) . Dans votre 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 avec l'expression pipe. Le processus peut ne jamais s'arrêter, à moins qu'il ne soit sensible à la fermeture de la conduite. En particulier, il peut continuer à écrire des choses à votre stdout, ce qui peut être déroutant.

2
répondu clacke 2017-05-15 07:09:44

Pur shell solution:

% 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 deuxième cat remplacé 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

s'il vous plaît noter que le premier chat échoue aussi, parce que c'est stdout se ferme sur elle. L'ordre des commandes manquées 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 de sorte que vous pouvez ensuite vider ce ainsi dans un fichier journal si une erreur se produit, ou tout simplement le supprimer si aucune erreur (comme la sortie de dd).

1
répondu Coroos 2015-03-31 10:08:58

Base sur la réponse de @brian-s-wilson; cette fonction d'aide 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 devrait produire aucun résultat; mais nous voulons voir le résultat qu'il produit

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

2: tout pipeline doit réussir

thing | something -q | thingy
pipeinfo || return
1
répondu Sam Liddicott 2016-01-15 15:29:41

il peut parfois être plus simple et plus clair d'utiliser une commande externe, plutôt que de creuser dans les détails de bash. pipeline , du langage de script de processus minimal execline , sort avec le code de retour de la deuxième commande*, tout comme un sh pipeline fait, mais à la différence de sh , il permet d'inverser la direction de la conduite, de sorte que nous pouvons capturer le code de retour du processus de producteur (le ci-dessous est tout sur le sh ligne de commande, mais avec execline installé):

$ # 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

, qui utilise pipeline , présente les mêmes différences avec les pipelines de bash indigènes que la substitution de procédé de bash utilisée dans la réponse #43972501 .

* en fait pipeline ne sort pas du tout à moins qu'il y ait une erreur. Il exécute la deuxième commande, donc c'est la deuxième commande qui fait le retour.

1
répondu clacke 2017-05-15 07:26:41