web-dev-qa-db-fra.com

Sortie de tuyau et capture du statut de sortie dans Bash

Je souhaite exécuter une commande d'exécution longue dans Bash et capturer son statut de sortie et tee 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 tee et non de la commande. Comment puis-je résoudre ça?

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

384
flybywire

Il existe une variable Bash interne appelée $PIPESTATUS; c'est un tableau qui contient le statut 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 aussi avec d’autres shells (comme zsh) serait de permettre le pipefail:

set -o pipefail
...

La première option fonctionne pas fonctionne avec zsh en raison d'une syntaxe légèrement différente.

480
codar

utiliser set -o pipefail de bash est utile

pipefail: la valeur de retour d'un pipeline est l'état de la dernière commande à quitter avec un statut différent de zéro, ou zéro si aucune commande n'a été fermée avec un statut différent de zéro

137
Felipe Alvarez

Solution Dumb: Les connecter via un tube nommé (mkfifo). Ensuite, la commande peut être exécutée en second.

 mkfifo pipe
 tee out.txt < pipe &
 command > pipe
 echo $?
116
EFraim

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
35
Stefano Borini

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

Situation:

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 et une alternative sans sous-shell et sans certaines mises en garde.

25
lesmana

En combinant PIPESTATUS[0] et le résultat de l'exécution de la commande exit 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

19
par

Je voulais donc apporter une réponse comme celle de Lesmana, mais je pense que la mienne est peut-être une solution un peu plus simple et légèrement plus avantageuse de 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 ceci est mieux expliqué de l'intérieur - Command1 exécutera et imprimera sa sortie normale sur stdout (descripteur de fichier 1), puis une fois terminé, printf exécutera et imprimera le code de sortie de icommand1 sur sa sortie standard, mais ce dernier sera redirigé vers descripteur de fichier 3.

Pendant que commande1 est en cours d'exécution, son stdout est redirigé vers commande2 (la sortie de printf ne le fait jamais sur commande2 car nous l'envoyons dans le descripteur de fichier 3 au lieu de 1, ce qui est lu par le canal). Ensuite, nous redirigeons la sortie de command2 vers le descripteur de fichier 4, de sorte qu'il reste également en dehors du descripteur de fichier 1 - car nous voulons que le descripteur de fichier 1 soit libre un peu plus tard, car nous ramènerons la sortie de printf sur le descripteur de fichier 3 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 sera placé dans la variable.

Le dernier brin de magie est que le premier exec 4>&1 que nous avons fait en tant que commande distincte: il ouvre le descripteur de fichier 4 en tant que copie de la sortie standard du shell externe. La substitution de commande capturera tout ce qui est écrit en sortie standard du point de vue des commandes qu’il contient - mais comme la sortie de command2 est dirigée vers le descripteur de fichier 4 en ce qui concerne la substitution de commande, la substitution de commande ne le capture pas - "sort" de la substitution de commande, il continue en fait à accéder au descripteur de fichier général du script 1.

(Le exec 4>&1 doit être une commande séparée 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 ouverte dans la commande "external" qui utilise la substitution. C'est donc le moyen le plus simple et portable de le faire.)

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

De plus, si j'ai bien compris, $? contiendra toujours le code de retour de la deuxième commande du canal, car les affectations de variables, les substitutions de commandes et les commandes composées sont toutes transparentes pour le code de retour de la commande qu'elles contiennent. donc, le statut de retour de command2 devrait être propagé - ceci, sans avoir à définir une fonction supplémentaire, est la raison pour laquelle je pense que cela pourrait être une solution un peu meilleure que celle proposée par lesmana.

Selon les mises en garde lesmana, il est possible que command1 finisse par utiliser les descripteurs de fichier 3 ou 4, aussi, 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 subshells (utiliser ( ) au lieu de { } fonctionnera aussi, mais peut-être moins efficace.)

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

Je ne suis pas sûr de la fréquence à laquelle les choses utilisent souvent les descripteurs de fichier trois et quatre directement. Je pense que la plupart du temps, les programmes utilisent des appels système qui renvoient des descripteurs de fichier non utilisés pour le moment, devinez (je pourrais imaginer un programme vérifiant un descripteur de fichier pour voir s'il est ouvert, et l'utiliser si c'est le cas, ou se comporter différemment si ce n'est pas le cas). Donc, ce dernier est probablement préférable de garder à l'esprit et à utiliser pour les cas à usage général.

10
mtraceur

Dans Ubuntu et Debian, vous pouvez apt-get install moreutils. Celui-ci contient un utilitaire appelé mispipe qui renvoie le statut de sortie de la première commande du canal.

6
Bryan Larsen

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

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 piquée?

3
maxdev137
(command | tee out.txt; exit ${PIPESTATUS[0]})

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

3
jakob-r

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 doit être /bin/sh.

2
Anthony Scemama

Le moyen le plus simple de faire cela en plain bash est d'utiliser 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:

  • Lorsque vous exécutez un pipeline, bash attend que tous les processus soient terminés.
  • En envoyant Ctrl-C à bash, il tue tous les processus d'un pipeline, pas seulement le processus principal.
  • L'option pipefail et la variable PIPESTATUS ne sont pas pertinentes pour traiter la substitution.
  • Peut-être plus

Avec la substitution de processus, bash commence simplement le processus et l’oublie, il n’est même pas visible dans jobs.

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

Si vous voulez inverser le processus "principal", vous devez simplement inverser les commandes et le sens de la substitution en 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 existe des différences par rapport à l'expression pipe. Le processus peut ne jamais cesser de s'exécuter, sauf s'il est sensible à la fermeture du tuyau. En particulier, il se peut que vous continuiez à écrire sur votre stdout, ce qui peut prêter à confusion.

2
clacke

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'il produit

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

2: tout le pipeline doit réussir

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

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 , à partir du langage de script de processus minimal execline , quitte avec le code de retour de la deuxième commande *, comme le fait un pipeline sh, mais contrairement à sh, il permet d’inverser la direction du tuyau afin que nous puissions capturer le code de retour du processus producteur (tout ce qui suit est sur la ligne de commande sh, 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

L'utilisation de pipeline présente, par rapport aux pipelines bash natifs, les mêmes différences que la substitution de processus bash utilisée dans la réponse # 43972501 .

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

1
clacke

Solution Pure Shell:

% 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 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 le premier chat échoue aussi, parce que c'est stdout est fermé à ce sujet. 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 également les transférer dans un fichier journal en cas d'erreur ou les supprimer si aucune erreur ne se produit (comme la sortie de dd).

1
Coroos