bash: comment propager les erreurs de substitution de processus?

19

Je veux que mes scripts shell échouent chaque fois qu'une commande exécutée avec eux échoue.

En général, je le fais avec:

set -e
set -o pipefail

(généralement j'ajoute set -uaussi)

Le fait est que rien de ce qui précède ne fonctionne avec la substitution de processus. Ce code affiche "ok" et quitte avec le code retour = 0, alors que j'aimerais qu'il échoue:

#!/bin/bash -e
set -o pipefail
cat <(false) <(echo ok)

Y a-t-il quelque chose d'équivalent à "pipefail" sauf pour la substitution de processus? Est-il possible de transmettre à une commande la sortie des commandes telles qu'elles étaient des fichiers, mais déclencher une erreur chaque fois que l'un de ces programmes échoue?

La solution d'un pauvre serait de détecter si ces commandes écrivent sur stderr (mais certaines commandes écrivent sur stderr dans des scénarios réussis).

Une autre solution plus conforme à Posix consisterait à utiliser des canaux nommés, mais je dois lancer ces commandes qui utilisent la substitution de processus comme des oneliners construits à la volée à partir du code compilé, et la création de canaux nommés compliquerait les choses (commandes supplémentaires, erreur de piégeage pour les supprimer, etc.)

juanleon
la source
Vous n'avez pas besoin d'intercepter les erreurs pour supprimer les canaux nommés. En fait, ce n'est pas du tout une bonne façon. Considérez mkfifo pipe; { rm pipe; cat <file; } >pipe. Cette commande va se bloquer jusqu'à ce qu'un lecteur s'ouvre pipecar c'est le shell qui fait le open()et donc dès qu'il y a un lecteur sur pipele lien fs pour pipeis rm'd puis catcopie les fichiers dans le descripteur du shell pour ce pipe. Et de toute façon, si vous voulez propager une erreur à partir d'un sous-processus: <( ! : || kill -2 "$$")
mikeserv
Merci pour le conseil sur la suppression des canaux nommés. Malheureusement, la $$substitution ne fonctionne pas pour moi, car cette substitution de commande ne se fait pas car la commande qui utilise la substitution de processus se fait à l'intérieur d'un pipeline de commandes généré à partir d'un code "non shell" (python). Je devrais probablement créer un sous-processus en python et les diriger par programme.
juanleon
Alors utilisez kill -2 0.
mikeserv
Malheureusement, "kill -2 0" tuerait (signalerait) le python. L'écriture de gestionnaires de signaux pour effectuer la logique métier dans une application multithread n'est pas quelque chose que j'attends avec impatience :-)
juanleon
Si vous ne voulez pas gérer les signaux, pourquoi essayez-vous de les recevoir? Quoi qu'il en soit, je m'attends à ce que les canaux nommés soient la solution la plus simple à la fin, ne serait-ce que parce que le faire correctement nécessite de disposer de votre propre cadre et de configurer votre propre répartiteur personnalisé. Surmontez cet obstacle et tout se réunit.
mikeserv

Réponses:

6

Vous ne pouvez contourner ce problème qu'avec cela, par exemple:

cat <(false || kill $$) <(echo ok)
other_command

Le sous-shell du script est SIGTERMd avant que la deuxième commande puisse être exécutée ( other_command). La echo okcommande est exécutée "parfois": le problème est que les substitutions de processus sont asynchrones. Il n'y a aucune garantie que la kill $$commande est exécutée avant ou après la echo okcommande. C'est une question de planification des systèmes d'exploitation.

Considérez un script bash comme celui-ci:

#!/bin/bash
set -e
set -o pipefail
cat <(echo pre) <(false || kill $$) <(echo post)
echo "you will never see this"

La sortie de ce script peut être:

$ ./script
Terminated
$ echo $?
143           # it's 128 + 15 (signal number of SIGTERM)

Ou:

$ ./script
Terminated
$ pre
post

$ echo $?
143

Vous pouvez l'essayer et après quelques essais, vous verrez les deux ordres différents dans la sortie. Dans le premier, le script a été interrompu avant que les deux autres echocommandes puissent écrire dans le descripteur de fichier. Dans le second, la falseou la killcommande était probablement programmée après les echocommandes.

Ou pour être plus précis: l'appel système signal()de l' killutillité qui envoie le SIGTERMsignal au processus shell a été planifié (ou a été délivré) plus tard ou plus tôt que les write()appels système d' écho .

Mais cependant, le script s'arrête et le code de sortie n'est pas 0. Il devrait donc résoudre votre problème.

Une autre solution consiste, bien sûr, à utiliser des canaux nommés pour cela. Mais, cela dépend de la complexité de votre script pour implémenter des canaux nommés ou de la solution de contournement ci-dessus.

Les références:

le chaos
la source
2

Pour mémoire, et même si les réponses et commentaires étaient bons et utiles, j'ai fini par implémenter quelque chose d'un peu différent (j'avais des restrictions sur la réception des signaux dans le processus parent que je n'ai pas mentionné dans la question)

En gros, j'ai fini par faire quelque chose comme ça:

command <(subcomand 2>error_file && rm error_file) <(....) ...

Ensuite, je vérifie le fichier d'erreur. S'il existe, je sais quelle sous-commande a échoué (et le contenu du fichier d'erreur peut être utile). Plus verbeux et hack que je voulais à l'origine, mais moins lourd que de créer des pipes nommées dans une commande bash à une ligne.

juanleon
la source
Tout ce qui fonctionne pour vous est généralement le meilleur moyen. Merci surtout d'être revenu et d'avoir répondu - les selfies sont mes préférés.
mikeserv
2

Cet exemple montre comment utiliser killavec trap.

#! /bin/bash
failure ()
{
  echo 'sub process failed' >&2
  exit 1
}
trap failure SIGUSR1
cat < <( false || kill -SIGUSR1 $$ )

Mais killne peut pas passer un code retour de votre sous-processus au processus parent.

ceving
la source
Je aime cette variation sur la réponse acceptée
sehe
1

De la même manière que vous implémenteriez $PIPESTATUS/ $pipestatusavec un shell POSIX qui ne le prend pas en charge , vous pouvez obtenir l'état de sortie des commandes en les passant via un tube:

unset -v false_status echo_status
{ code=$(
    exec 3>&1 >&4 4>&-
    cat 3>&- <(false 3>&-; echo >&3 "false_status=$?") \
             <(echo ok 3>&-; echo >&3 "echo_status=$?")
);} 4>&1
cat_status=$?
eval "$code"
printf '%s_code=%d\n' cat   "$cat_status" \
                      false "$false_status" \
                      echo  "$echo_status"

Qui donne:

ok
cat_code=0
false_code=1
echo_code=0

Ou vous pouvez utiliser pipefailet implémenter la substitution de processus à la main comme vous le feriez avec des shells qui ne le prennent pas en charge:

set -o pipefail
{
  false <&5 5<&- | {
    echo OK <&5 5<&- | {
      cat /dev/fd/3 /dev/fd/4
    } 4<&0 <&5 5<&-
  } 3<&0
} 5<&0
Stéphane Chazelas
la source