Question

Je souhaite exécuter une commande d'exécution longue dans Bash et capturer son statut de sortie, ainsi que tee sa sortie.

Je fais donc ceci:

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 ce problème?

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.

Était-ce utile?

La solution

Il existe une variable Bash interne appelée $PIPESTATUS; il & # 8217; est un tableau contenant le statut de sortie de chaque commande de votre dernier pipeline de commandes en 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 ne fonctionne pas avec zsh en raison d'une syntaxe légèrement différente.

Autres conseils

utiliser bash set -o pipefail est utile

  

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

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

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

Il y a un tableau qui vous donne le statut de sortie de chaque commande d'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

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-coques et quelques mises en garde.

En combinant PIPESTATUS[0] 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: $?"

vous donnera:

return value: 1

Je voulais donc apporter une réponse semblable à 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 cela s’explique mieux de l’intérieur - Command1 exécutera et imprimera sa sortie normale sur la sortie standard (descripteur de fichier 1), puis, une fois terminé, printf exécutera et imprimera le code de sortie de icommand1 sur sa sortie standard. est redirigé vers le descripteur de fichier 3.

Pendant que commande1 est en cours d'exécution, sa sortie standard est redirigée vers commande2 (la sortie de printf ne parvient jamais à atteindre commande2 car nous l'envoyons dans le descripteur de fichier 3 au lieu de 1, comme le lit le tuyau). 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) capturera et c’est ce qui sera placé dans la variable.

Le dernier mot magique est le premier exec 4>&1 que nous avons créé en tant que commande distincte: le descripteur de fichier 4 est ouvert 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 - obtient " out " de la substitution de commande, il est toujours utilisé dans le descripteur de fichier global 1 du script.

(La $? 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, qui est ouverte dans le " external "commande qui utilise la substitution. C’est donc le moyen le plus simple 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 à commande2, puis la sortie de printf saute par-dessus la commande 2 afin 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 afin qu'il se retrouve dans la variable, et la sortie de command2 continue à être écrite de manière joyeuse sur la sortie standard. , comme dans un tuyau normal.

De plus, si j'ai bien compris, ( ) contient 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, et ne pas avoir à définir une fonction supplémentaire, est pourquoi je pense que cela pourrait être une solution légèrement meilleure que celle proposée par lesmana.

Selon les avertissements lesmana, il est possible que command1 finisse par utiliser les descripteurs de fichier 3 ou 4, alors 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-shell (utiliser { } au lieu de 3>&1 fonctionnera également, même si cela risque d'ê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 4>&- hérite du descripteur de fichier trois. Ainsi, 3>&- s'assure que la commande composée interne n'héritera pas du descripteur de fichier quatre et <=> n'héritera pas du descripteur de fichier trois, de sorte que commande1 obtiendra un environnement plus «propre», plus standard. Vous pouvez également déplacer le <=> intérieur à côté du <=>, mais je suppose que vous ne devez pas simplement en limiter autant que possible la portée.

Je ne suis pas sûr de savoir à quelle fréquence les descripteurs de fichier trois et quatre sont utilisés directement. Je pense que la plupart du temps, les programmes utilisent des appels système qui renvoient nDes descripteurs de fichiers actuellement utilisés, mais parfois du code écrit directement dans le descripteur de fichier 3 (j'imagine que je pourrais imaginer un programme vérifiant un descripteur de fichier pour voir s'il est ouvert, et s'il est utilisé différemment, ou se comporter différemment en conséquence si ce n'est pas). Il est donc probablement préférable de garder ce dernier point à l’esprit et de l’utiliser pour les cas généraux.

Sous 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 du canal.

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 tube est dans un sous-shell. Pour une solution à ce problème,
voir bash pipestatus dans la commande backticked?

(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.

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.

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:

  • Lors de l'exécution d'un pipeline, bash attend que tous les processus soient terminés.
  • Envoi de 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 démarre 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 lequel est le " principal " processus, il vous suffit de retourner les commandes et le sens 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 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 peut continuer à écrire sur votre stdout, ce qui peut prêter à confusion.

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

Veuillez noter que le premier chat échoue également, car sa sortie standard 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 aussi 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).

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 ne produire aucun résultat. 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

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 retour de la deuxième commande *, comme le fait un pipeline sh, mais contrairement à execline , cela 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 pipeline ligne de commande, mais avec <=> 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 <=> différences par rapport aux pipelines bash natifs est identique à la substitution de processus bash utilisée dans la réponse N ° 43972501 .

* En réalité, <=> ne 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 qui effectue le renvoi.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top