Obtenir le statut de sortie du processus transféré à un autre

288

J'ai deux processus fooet bar, connecté avec un tuyau:

$ foo | bar

barquitte toujours 0; Je suis intéressé par le code de sortie de foo. Y a-t-il un moyen d'y arriver?

Michael Mrozek
la source
stackoverflow.com/questions/1221833/…
Ciro Santilli a annoncé

Réponses:

256

Si vous utilisez bash, vous pouvez utiliser la PIPESTATUSvariable de tableau pour obtenir le statut de sortie de chaque élément du pipeline.

$ false | true
$ echo "${PIPESTATUS[0]} ${PIPESTATUS[1]}"
1 0

Si vous utilisez zsh, ils s'appellent tableau pipestatus(la casse compte!) Et les indices de tableau commencent à un:

$ false | true
$ echo "${pipestatus[1]} ${pipestatus[2]}"
1 0

Pour les combiner dans une fonction de manière à ne pas perdre 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 bashou zshet vous obtiendrez les mêmes résultats. un seul retval_bashet retval_zshsera réglé. L'autre sera vide. Cela permettrait à une fonction de se terminer par return $retval_bash $retval_zsh(notez le manque de guillemets!).

camh
la source
9
Et pipestatusen zsh. Malheureusement, d'autres coquilles n'ont pas cette fonctionnalité.
Gilles
8
Remarque: Les tableaux dans zsh commencent de façon contre-intuitive à l'index 1, donc c'est echo "$pipestatus[1]" "$pipestatus[2]".
Christoph Wurm
5
Vous pouvez vérifier l'ensemble du pipeline comme if [ `echo "${PIPESTATUS[@]}" | tr -s ' ' + | bc` -ne 0 ]; then echo FAIL; fi
suit
7
@JanHudec: Peut-être devriez-vous lire les cinq premiers mots de ma réponse. Indiquez également avec bonté où la question demandait une réponse POSIX uniquement.
camh
12
@JanHudec: Ce n'était pas non plus étiqueté POSIX. Pourquoi supposez-vous que la réponse doit être POSIX? Ce n'était pas précisé, alors j'ai fourni une réponse qualifiée. Ma réponse n’est pas fausse, de plus, il existe de nombreuses réponses pour traiter d’autres cas.
camh
238

Il y a 3 façons courantes de le faire:

Pipefail

La première consiste à définir l’ pipefailoption ( ksh, zshou bash). C’est le plus simple. En général, l’état $?de sortie est défini sur le code de sortie du dernier programme à quitter non nul (ou zéro si tous les éléments sont sortis avec succès).

$ false | true; echo $?
0
$ set -o pipefail
$ false | true; echo $?
1

$ PIPESTATUS

Bash a également une variable de tableau appelée $PIPESTATUS( $pipestatusin 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 l'exemple de la 3ème commande pour obtenir la valeur spécifique dans le pipeline dont vous avez besoin.

Exécutions séparées

C'est la plus lourde des solutions. Exécutez chaque commande séparément et capturez le statut

$ OUTPUT="$(echo foo)"
$ STATUS_ECHO="$?"
$ printf '%s' "$OUTPUT" | grep -iq "bar"
$ STATUS_GREP="$?"
$ echo "$STATUS_ECHO $STATUS_GREP"
0 1
Patrick
la source
2
Zut! J'allais juste poster sur PIPESTATUS.
slm
1
Pour référence, il existe plusieurs autres techniques abordées dans cette question SO: stackoverflow.com/questions/1221833/…
slm
1
@Patrick la solution pipestatus fonctionne sur bash, juste plus si vous utilisez un script ksh vous pensez que nous pouvons trouver quelque chose de similaire à pipestatus? , (depuis que je vois le statut de tuyauterie non supporté par ksh)
2
2
@yael, je ne l'utilise pas ksh, mais d'un simple coup d'œil à sa page de manuel, elle ne prend pas en charge $PIPESTATUSou quelque chose de similaire. Il supporte cependant l' pipefailoption.
Patrick le
1
J'ai décidé d'y aller avec pipefail car cela me permet d'obtenir le statut de la commande ayant échoué ici:LOG=$(failed_command | successful_command)
vmrob le
51

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 le statut de sortie someproget la sortie 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 from filteras stdout de la construction et status de someprogexit from exit statut de la construction.


cette construction fonctionne également avec un simple regroupement de commandes {...}au lieu de sous-shell (...). Les sous-coques ont des implications, entre autres un coût de performance, dont nous n’avons pas besoin ici. lisez le manuel de 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 afin que la construction devienne beaucoup plus spacieuse.

Pour la suite de ce texte, je vais utiliser la variante du sous-shell.


Exemple someproget 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 fichier ouverts du parent. Cela signifie someprogqu’il héritera des descripteurs de fichier ouverts 3 et 4. Si l’ someprogécriture dans le descripteur de fichier 3 devient le statut de sortie. Le statut de sortie réel sera ignoré car il readne lit qu'une fois.

Si vous craignez d' someprogé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

L' exec 3>&- 4>&-avant someprogferme le descripteur de fichier avant de l'exécuter, someprogil someprogn'existe donc tout simplement pas pour ces descripteurs de fichier.

Cela peut aussi être écrit comme ceci: someprog 3>&- 4>&-


Explication pas à pas 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:

  1. Un sous-shell est créé avec le descripteur de fichier 4 redirigé vers stdout. Cela signifie que tout ce qui est imprimé dans le descripteur de fichier 4 dans le sous-shell finira par devenir la sortie standard de la construction entière.
  2. Un canal est créé et les commandes de gauche ( #part3) et de droite ( #part2) sont exécutées. exit $xsest également la dernière commande du tube et cela signifie que la chaîne de stdin sera le statut de sortie de la construction entière.
  3. Un sous-shell est créé avec le descripteur de fichier 3 redirigé vers stdout. Cela signifie que tout ce qui est imprimé dans le descripteur de fichier 3 de ce sous-shell finira par #part2devenir le statut de sortie de la construction entière.
  4. Un pipe est créé et les commandes à gauche ( #part5et #part6) et à droite ( filter >&4) sont exécutées. La sortie de filterest redirigée vers le descripteur de fichier 4. Dans #part1le descripteur de fichier, 4 a été redirigé vers la sortie standard. Cela signifie que la sortie de filterest la sortie standard de la construction entière.
  5. Le statut de sortie de #part6est imprimé dans le descripteur de fichier 3. Dans #part3le descripteur de fichier 3 a été redirigé vers #part2. Cela signifie que le statut de sortie de #part6sera le statut de sortie final de la construction entière.
  6. someprogest exécuté. Le statut de sortie est pris en compte #part5. La sortie standard est prise par le tuyau #part4et transmise à filter. La sortie de filteraboutira à son tour à stdout comme expliqué dans#part4
lesmana
la source
1
Agréable. Pour la fonction que je pourrais faire(read; exit $REPLY)
jthill
5
(exec 3>&- 4>&-; someprog)simplifie à someprog 3>&- 4>&-.
Roger Pate
Cette méthode fonctionne aussi sans sous-shell:{ { { { someprog 3>&- 4>&-; echo $? >&3; } | filter >&4; } 3>&1; } | { read xs; exit $xs; }; } 4>&1
josch
36

Bien que pas exactement ce que vous avez demandé, vous pouvez utiliser

#!/bin/bash -o pipefail

afin que vos tuyaux renvoient le dernier retour non nul.

pourrait être un peu moins codant

Edit: Exemple

[root@localhost ~]# false | true
[root@localhost ~]# echo $?
0
[root@localhost ~]# set -o pipefail
[root@localhost ~]# false | true
[root@localhost ~]# echo $?
1
Chris
la source
9
set -o pipefaill'intérieur du script devrait être plus robuste, par exemple au cas où quelqu'un l'exécuterait via bash foo.sh.
maxschlepzig
Comment ça marche? as-tu un exemple?
Johan
2
Notez que ce -o pipefailn'est pas dans POSIX.
scy
2
Cela ne fonctionne pas dans ma version 3.2.25 (1) de BASH. Au sommet de / tmp / ff j'ai #!/bin/bash -o pipefail. L'erreur est:/bin/bash: line 0: /bin/bash: /tmp/ff: invalid option name
Felipe Alvarez
2
@FelipeAlvarez: Certains environnements (y compris Linux) n'analysent pas les espaces sur les #!lignes au-delà de la première, de sorte que cela devient /bin/bash -o pipefail /tmp/ff, au lieu de l' analyse nécessaire /bin/bash -o pipefail /tmp/ff- getopt(ou similaire), l'utilisation de optarg, l'élément suivant dans ARGVl'argument to -o, donc ça échoue. Si vous deviez créer un wrapper (par exemple, bash-pfcela vient de se produire exec /bin/bash -o pipefail "$@", et le mettrait en #!ligne, cela fonctionnerait. Voir aussi: en.wikipedia.org/wiki/Shebang_%28Unix%29
lindes
22

Ce que je fais lorsque cela est possible est d'alimenter le code de sortie fooen bar. Par exemple, si je sais que cela foone produit jamais une ligne comportant uniquement des chiffres, je peux simplement ajouter le code de sortie:

{ foo; echo "$?"; } | awk '!/[^0-9]/ {exit($0)} {…}'

Ou si je sais que la sortie de foone contient jamais une ligne avec juste .:

{ foo; echo .; echo "$?"; } | awk '/^\.$/ {getline; exit($0)} {…}'

Cela peut toujours être fait s'il existe un moyen de bartravailler sur toutes les lignes sauf la dernière et de passer à la dernière ligne comme code de sortie.

S'il bars'agit d'un pipeline complexe dont vous n'avez pas besoin en 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 cela $exit_codesest généralement foo:X bar:Y, mais cela pourrait être bar:Y foo:Xsi vous barquittez avant de lire toutes ses entrées ou si vous êtes malchanceux. Je pense aux tuyaux écrit jusqu'à 512 octets sont atomiques sur tous unix, de sorte que les foo:$?et bar:$?parties ne seront pas entremêlées tant que les chaînes de balise sont sous 507 octets.

Si vous devez capturer la sortie bar, cela devient difficile. Vous pouvez combiner les techniques ci-dessus en faisant en sorte que la sortie de barne jamais contenir une ligne qui ressemble à une indication de code de sortie, mais cela devient difficile.

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 existe la possibilité simple d’ utiliser un fichier temporaire pour stocker le statut. Simple, mais pas si simple en production:

  • Si plusieurs scripts s'exécutent simultanément ou si le même script utilise cette méthode à plusieurs endroits, vous devez vous assurer qu'ils utilisent des noms de fichiers temporaires différents.
  • Il est difficile de créer un fichier temporaire en toute sécurité dans un répertoire partagé. C'est souvent /tmple seul endroit où un script est sûr de pouvoir écrire des fichiers. Utilisez mktemp, ce qui n’est pas POSIX mais disponible sur tous les ordinateurs 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")
Gilles
la source
1
Lorsque j'utilise l'approche de fichier temporaire, je préfère ajouter une interruption pour EXIT qui supprime tous les fichiers temporaires de sorte qu'aucune faille ne reste, même si le script meurt
miracle173
17

À partir du pipeline:

foo | bar | baz

Voici une solution générale utilisant uniquement un 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 en échec, 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 $error_statusesde mes tests; sans eux, il grepest impossible de faire la différence car les nouvelles lignes sont contraintes aux espaces.

Jander
la source
12

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 ceci est mieux expliqué de l'intérieur - Command1 exécutera et imprimera sa sortie standard sur stdout (descripteur de fichier 1), puis une fois terminé, printf exécutera et imprimera le code de sortie de command1 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.

La magie finale est que exec 4>&1nous avons d' abord créé une commande distincte: elle 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 - cependant, une fois qu'il est "sorti" de la substitution de commande, il reste dans le descripteur de fichier général du script 1.

(Il exec 4>&1doit s'agir d'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 la commande "external" qui utilise la substitution. moyen portable 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 à 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, il $?contiendra toujours le code de retour de la deuxième commande dans le tube, 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. command2 devrait être propagé - ceci, et ne pas avoir à définir une fonction supplémentaire, est la raison pour laquelle je pense que cela pourrait être une solution légèrement 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 des sous-shell (utiliser ( )au lieu de { }fonctionnera également, même s’il est 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>&1hérite du descripteur de fichier trois. Ainsi, le groupe 4>&-s'assure que la commande composée interne n'héritera pas du descripteur de fichier quatre et 3>&-n'héritera pas du descripteur de fichier trois. Ainsi, commande1 obtient un environnement plus «propre», plus standard. Vous pouvez également déplacer l’intérieur à 4>&-côté du 3>&-, mais j’imagine pourquoi ne pas simplement en limiter le plus possible la portée.

Je ne suis pas sûr de la fréquence à laquelle les choses utilisent 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, mais que le code écrit parfois directement dans le descripteur de fichier 3. 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.

métreur
la source
Cela semble intéressant, mais je ne peux pas vraiment comprendre ce que vous attendez de cette commande, et mon ordinateur ne le peut pas non plus; Je reçois -bash: 3: Bad file descriptor.
G-Man
@ G-Man Bien, j'oublie toujours que bash n'a aucune idée de ce qu'il fait lorsqu'il s'agit de descripteurs de fichiers, contrairement aux shells que j'utilise généralement (les cendres de busybox). Je vous ferai savoir quand je penserai à une solution de contournement qui rend Bash heureux. En attendant, si vous avez une boîte Debian à portée de main, vous pouvez l’essayer au tiret, ou si vous avez une boîte occupée à portée de main, vous pouvez l’essayer avec busybox ash / sh.
mtraceur
@ G-Man En ce qui concerne ce que j'attend de la commande et ce qu'elle fait dans les autres shells, redirige stdout à partir de command1 pour ne pas être intercepté par la substitution de commande, mais une fois en dehors de la substitution de commande, elle passe fd3 retourne à stdout afin qu'il soit acheminé comme prévu par command2. Lorsque commande1 se ferme, printf se déclenche et affiche son état de sortie, qui est capturé dans la variable par la substitution de commande. Ventilation très détaillée ici: stackoverflow.com/questions/985876/tee-and-exit-status/… De plus, votre commentaire se lit-il comme s'il était censé être un peu insultant?
mtraceur
Où vais-je commencer? (1) Je suis désolé si vous vous êtes senti insulté. «Ça a l'air intéressant» était sérieux. ce serait génial si quelque chose d'aussi compact que cela fonctionnait aussi bien que prévu. Au-delà de cela, je disais simplement que je ne comprenais pas ce que votre solution était censée faire. Je travaille / joue avec Unix depuis longtemps (avant même que Linux n’existe), et si je ne comprends pas quelque chose, c’est un drapeau rouge qui peut-être, d’ autres personnes ne le comprendront pas non plus, et cela a besoin de plus d'explications (IMNSHO). … (Suite)
G-Man le
(Suite)… Puisque vous «aimez penser… que vous comprenez à peu près tout plus que la personne moyenne», vous devriez peut-être vous rappeler que l'objectif de Stack Exchange n'est pas d'être un service de rédaction de commandes, des milliers de solutions uniques à des questions trivialement distinctes; mais plutôt d'apprendre aux gens comment résoudre leurs propres problèmes. Et, à cette fin, vous devrez peut-être expliquer suffisamment les choses pour qu'une "personne moyenne" puisse les comprendre. Regardez la réponse de Lesmana pour un exemple d'une excellente explication. … (Suite)
G-Man le
11

Si vous avez le paquet moreutils installé, vous pouvez utiliser l’ utilitaire faux - tuyau qui fait exactement ce que vous avez demandé.

Emanuele Aina
la source
7

La solution de lesmana ci-dessus peut également être réalisée sans la surcharge de démarrer 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 tableau de bord 0.5.5 et les versions 3.2.25 et 4.2.42 de Bash. Par conséquent, même si certains shells ne prennent pas en charge le { .. }groupement, ils sont toujours compatibles POSIX.

pkeller
la source
Cela fonctionne très bien avec la plupart des shell avec lesquels j'ai essayé, y compris NetBSD sh, pdksh, mksh, dash, bash. Cependant, je ne peux pas le faire fonctionner avec AT & T Ksh (93s +, 93u +) ou zsh (4.3.9, 5.2), même avec set -o pipefailin ksh ou un nombre quelconque de waitcommandes saupoudrées . Je pense que cela peut, au moins en partie, être un problème d’analyse syntaxique pour ksh, comme si je m’en tenais à l’utilisation de subshells, alors cela fonctionnerait bien, mais même avec un ifchoix de la variante de sous-shell pour ksh mais en laissant les commandes composées pour d’autres, cela échoue .
Greg A. Woods
4

C'est portable, c'est-à-dire qu'il fonctionne avec n'importe quel shell compatible POSIX, ne nécessite pas que le répertoire actuel 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 dbiousjim:

(s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s))
jlliagre
la source
3
Cela ne fonctionne pas pour plusieurs raisons. 1. Le fichier temporaire peut être lu avant d'être écrit. 2. La création d'un fichier temporaire dans un répertoire partagé avec un nom prévisible n'est pas sécurisée (DoS trivial, race de lien symbolique). 3. Si le même script utilise cette astuce plusieurs fois, il utilisera toujours le même nom de fichier. Pour résoudre le problème 1, lisez le fichier une fois le pipeline terminé. Pour résoudre les problèmes 2 et 3, utilisez un fichier temporaire avec un nom généré de manière aléatoire ou dans un répertoire privé.
Gilles
+1 Bien le $ {PIPESTATUS [0]} est plus facile, mais l'idée de base ici fonctionne si on connaît les problèmes mentionnés par Gilles.
Johan
Vous pouvez économiser quelques sous - couches: (s=/tmp/.$$_$RANDOM;{foo;echo $?>$s;}|bar; exit $(cat $s;rm $s)). @Johan: Je suis d'accord que c'est plus facile avec Bash, mais dans certains contextes, savoir comment éviter Bash en vaut la peine.
dubiousjim
4

Ce qui suit est considéré comme un complément à la réponse de @Patrik, au cas où vous ne pourriez pas utiliser l’une des solutions courantes.

Cette réponse suppose que:

  • Vous avez une coquille qui ne sait $PIPESTATUSni deset -o pipefail
  • Vous voulez utiliser un canal pour une exécution parallèle, donc pas de fichiers temporaires.
  • Si vous interrompez le script, vous ne souhaitez peut-être pas créer de fouillis supplémentaire en raison d'une panne de courant soudaine.
  • Cette solution devrait être relativement facile à suivre et à lire.
  • Vous ne voulez pas introduire de sous-shell supplémentaires.
  • Vous ne pouvez pas manipuler les descripteurs de fichiers existants, vous ne devez donc pas toucher à stdin / out / err (vous pouvez toutefois en introduire temporairement de nouveaux)

Hypothèses supplémentaires. Vous pouvez vous débarrasser de tout, mais cette recette est trop lourde, elle n'est donc pas couverte 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 que toutes les commandes de canal soient renvoyées.

Avant:, foo | bar | bazcependant, cela ne retourne que le code de sortie de la dernière commande ( baz)

Wanted: $?ne doit pas être 0(true), si l'une des commandes du canal 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

A expliqué:

  • Un fichier temporaire est créé avec mktemp. Cela crée généralement immédiatement un fichier dans/tmp
  • Ce fichier temporaire est ensuite redirigé vers FD 9 pour écriture et FD 8 pour lecture.
  • Ensuite, le fichier temporaire est immédiatement supprimé. Il reste ouvert jusqu'à ce que les deux FD disparaissent.
  • Maintenant, le tuyau est démarré. Chaque étape n’ajoute que dans FD 9, s’il ya eu une erreur.
  • La commande waitest nécessaire pour ksh, car kshsinon n'attend pas que toutes les commandes de canal soient terminées. Toutefois, veuillez noter qu'il existe des effets indésirables si certaines tâches d'arrière-plan sont présentes. Je l'ai donc commenté par défaut. Si l'attente ne fait pas mal, vous pouvez le commenter.
  • Ensuite, le contenu du fichier est lu. Si elle est vide (parce que tous ont travaillé) readretours false, donc trueindique une erreur

Ceci peut être utilisé comme remplacement de plugin pour une seule commande et n'a besoin que de ce qui suit:

  • FD inutilisés 9 et 8
  • Une seule variable d'environnement pour contenir le nom du fichier temporaire
  • Et cette recette peut être adaptée à n'importe quel shell permettant la redirection IO
  • En outre, il est assez agnostique sur la plate-forme et n'a pas besoin de choses comme /proc/fd/N

Bogues:

Ce script a un bug dans le cas où l' /tmpespace est insuffisant. Si vous avez besoin de protection contre ce cas artificiel, aussi, vous pouvez le faire comme suit, mais cela présente l'inconvénient, que le nombre d' 0en 000fonction du nombre de commandes dans le tuyau, il est donc 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 de portabilité:

  • kshet des coquilles similaires qui n'attendent que la dernière commande de tuyau ont besoin du non waitcommenté

  • Le dernier exemple utilise printf "%1s" "$?"au lieu de echo -n "$?"parce que c'est plus portable. Toutes les plateformes n'interprètent pas -ncorrectement.

  • printf "$?"le ferait aussi bien, cependant printf "%1s"attrape certains cas au cas où vous exécutez le script sur une plate-forme vraiment cassée. (Lire: si vous programmez paranoia_mode=extreme.)

  • Les formats FD 8 et FD 9 peuvent être supérieurs sur les plates-formes prenant en charge plusieurs chiffres. AFAIR, un shell conforme à POSIX n'a ​​besoin que de prendre en charge les chiffres uniques.

  • A été testé avec Debian 8.2 sh, bash, ksh, ash, sashet mêmecsh

Tino
la source
3

Avec un peu de précaution, cela devrait fonctionner:

foo-status=$(mktemp -t)
(foo; echo $? >$foo-status) | bar
foo_status=$(cat $foo-status)
alex
la source
Que diriez-vous de nettoyer comme jlliagre? Ne laissez-vous pas un fichier appelé foo-status?
Johan
@Johan: Si vous préférez ma suggestion, n'hésitez pas à la voter;) En plus de ne pas laisser de fichier, cela présente l'avantage de permettre à plusieurs processus de l'exécuter simultanément et que le répertoire en cours ne doit pas nécessairement être accessible en écriture.
jlliagre
2

Le bloc 'if' suivant ne sera exécuté que si '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 va exécuter haconf -makerwet stocker son stdout et stderr dans "$ haconf_out". Si la valeur renvoyée haconfest true, le bloc 'if' sera exécuté et grepindiquera "$ haconf_out", en essayant de le faire correspondre à "Le cluster est déjà accessible en écriture".

Notez que les tuyaux se nettoient automatiquement; avec la redirection, vous devrez prendre soin de supprimer "$ haconf_out" lorsque vous aurez terminé.

Pas aussi élégant que pipefail, mais une alternative légitime si cette fonctionnalité n’est pas à la portée de la main.

Rany Albeg Wein
la source
1
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
$
CG
la source
0

(Avec bash au moins) combiné avec set -eon peut utiliser un sous-shell pour émuler explicitement pipefail et sortir en cas d'erreur

set -e
foo | bar
( exit ${PIPESTATUS[0]} )
rest of program

Donc si fooéchoue pour une raison quelconque, le reste du programme ne sera pas exécuté et le script se terminera avec le code d'erreur correspondant. (Cela suppose que soit fooimprimée sa propre erreur, ce qui est suffisant pour comprendre la raison de l'échec)

le midi
la source
-1

EDIT : Cette réponse est fausse, mais intéressante, alors je la laisserai pour référence future.


L'ajout de a !à la commande inverse le code de retour.

http://tldp.org/LDP/abs/html/exit-status.html

# =========================================================== #
# Preceding a _pipe_ with ! inverts the exit status returned.
ls | bogus_command     # bash: bogus_command: command not found
echo $?                # 127

! ls | bogus_command   # bash: bogus_command: command not found
echo $?                # 0
# Note that the ! does not change the execution of the pipe.
# Only the exit status changes.
# =========================================================== #
Falmarri
la source
1
Je pense que cela n'a aucun rapport. Dans votre exemple, je souhaite connaître le code de sortie de ls- pas inverser le code de sortie debogus_command
Michael Mrozek
2
Je suggère de retirer cette réponse.
maxschlepzig
3
Eh bien, il semble que je sois un idiot. J'ai en fait utilisé cela dans un script avant de penser que c'était ce que le PO voulait. C'est une bonne chose que je ne l'utilise pas pour quelque chose d'important
Falmarri Le