pourquoi une boucle bash while ne se termine-t-elle pas lors de la redirection vers la sous-commande terminée?

12

Pourquoi la commande ci-dessous ne se ferme-t-elle pas? Plutôt que de sortir, la boucle fonctionne indéfiniment.

Alors que j'ai découvert ce comportement en utilisant une configuration plus complexe, la forme la plus simple de la commande se réduit à ce qui suit.

Ne quitte pas:

while /usr/bin/true ; do echo "ok" | cat ; done | exit 1

Il n'y a pas de fautes de frappe ci-dessus. Chaque '|' est une pipe. La «sortie 1» représente un autre processus qui s'est exécuté et s'est terminé.

Je m'attends à ce que la "sortie 1" provoque un SIGPIPE sur la boucle while (écrivez sur un tuyau sans lecteur) et que la boucle éclate. Mais, la boucle continue de fonctionner.

Pourquoi la commande ne s'arrête-t-elle pas?

stephen.z
la source
zsh se ferme normalement.
Braiam

Réponses:

13

Elle est due à un choix de mise en œuvre.

L'exécution du même script sur Solaris avec ksh93produit un comportement différent:

$ while /usr/bin/true ; do echo "ok" | cat ; done | exit 1
cat: write error [Broken pipe]

Ce qui déclenche le problème, c'est le pipeline interne, sans lui, la boucle se termine quel que soit le shell / OS:

$ while /usr/bin/true ; do echo "ok" ; done | exit 1
$

cat reçoit un signal SIGPIPE sous bash mais le shell itère de toute façon la boucle.

Process 5659 suspended
[pid 28801] execve("/bin/cat", ["cat"], [/* 63 vars */]) = 0
[pid 28801] --- SIGPIPE (Broken pipe) @ 0 (0) ---
Process 5659 resumed
Process 28801 detached
Process 28800 detached
--- SIGCHLD (Child exited) @ 0 (0) ---
Process 28802 attached
Process 28803 attached
[pid 28803] execve("/bin/cat", ["cat"], [/* 63 vars */]) = 0
Process 5659 suspended
[pid 28803] --- SIGPIPE (Broken pipe) @ 0 (0) ---
Process 5659 resumed
Process 28803 detached
Process 28802 detached
--- SIGCHLD (Child exited) @ 0 (0) ---
Process 28804 attached
Process 28805 attached (waiting for parent)
Process 28805 resumed (parent 5659 ready)
Process 5659 suspended
[pid 28805] execve("/bin/cat", ["cat"], [/* 63 vars */]) = 0
[pid 28805] --- SIGPIPE (Broken pipe) @ 0 (0) ---
Process 5659 resumed
Process 28805 detached
Process 28804 detached
--- SIGCHLD (Child exited) @ 0 (0) ---

La documentation de Bash indique:

Le shell attend que toutes les commandes du pipeline se terminent avant de renvoyer une valeur.

La documentation de Ksh indique:

Chaque commande, sauf éventuellement la dernière, est exécutée comme un processus distinct; le shell attend la fin de la dernière commande .

POSIX déclare:

Si le pipeline n'est pas en arrière-plan (voir Listes asynchrones), le shell doit attendre la fin de la dernière commande spécifiée dans le pipeline et peut également attendre la fin de toutes les commandes .

jlliagre
la source
Je pense que ce n'est pas exactement le pipeline interne qui déclenche le problème, c'est que la fonction intégrée echoignore SIGPIPE. Vous pouvez également reproduire le problème en utilisant env echoau lieu de echo(pour forcer l'utilisation du echobinaire réel ). (Comparez également la sortie de { echo hi; echo $? >&2; } | exit 1et { env echo hi; echo $? >&2; } | exit 1.)
Lucas Werkmeister
1

Ce problème me dérange depuis des années. Merci à Jilliagre pour le coup de pouce dans la bonne direction.

En reformulant un peu la question, sur ma boîte Linux, cela se termine comme prévu:

while true ; do echo "ok"; done | head

Mais si j'ajoute un tuyau, il ne se ferme pas comme prévu:

while true ; do echo "ok" | cat; done | head

Cela m'a frustré pendant des années. En considérant la réponse écrite par jilliagre, j'ai trouvé cette merveilleuse solution:

while true ; do echo "ok" | cat || exit; done | head

QED ...

Enfin, pas tout à fait. Voici quelque chose d'un peu plus compliqué:

i=0
while true; do
    i=`expr $i + 1`
    echo "$i" | grep '0$' || exit
done | head

Cela ne fonctionne pas correctement. J'ai ajouté le || exitafin qu'il sache comment se terminer tôt, mais le tout premier echone correspond pas au grepafin que la boucle se ferme immédiatement. Dans ce cas, vous n'êtes vraiment pas intéressé par le statut de sortie de grep. Ma solution consiste à en ajouter un autre cat. Alors, voici un script artificiel appelé "dizaines":

#!/bin/bash
i=0
while true; do
    i=`expr $i + 1`
    echo "$i" | grep '0$' | cat || exit
done

Cela se termine correctement lors de l'exécution en tant que tens | head. Dieu merci.

PaulC
la source