¿por qué no sale un bash while loop cuando se conecta a un subcomando terminado?

12

¿Por qué no sale el comando de abajo? En lugar de salir, el ciclo se ejecuta indefinidamente.

Mientras descubrí este comportamiento usando una configuración más compleja, la forma más simple del comando se reduce a lo siguiente.

No sale:

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

No hay errores tipográficos arriba. Cada '|' Es una pipa. La 'salida 1' representa otro proceso que se ejecutó y salió.

Espero que la "salida 1" provoque un SIGPIPE en el bucle while (escriba en una tubería sin lector) y que se rompa el bucle. Pero, el ciclo continúa corriendo.

¿Por qué no se detiene el comando?

stephen.z
fuente
zsh sale normalmente.
Braiam

Respuestas:

13

Se debe a una elección en la implementación.

Ejecutar el mismo script en Solaris con ksh93produce un comportamiento diferente:

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

Lo que desencadena el problema es la canalización interna, sin ella, el bucle sale sea cual sea el shell / OS:

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

cat está recibiendo una señal SIGPIPE en bash pero el shell está iterando el bucle de todos modos.

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 documentación de Bash dice:

El shell espera a que finalicen todos los comandos de la canalización antes de devolver un valor.

La documentación de Ksh dice:

Cada comando, excepto posiblemente el último, se ejecuta como un proceso separado; el shell espera a que termine el último comando .

POSIX declara:

Si la canalización no está en segundo plano (consulte Listas asincrónicas), el shell esperará a que se complete el último comando especificado en la canalización y también puede esperar a que se completen todos los comandos .

jlliagre
fuente
Creo que no es exactamente la tubería interna lo que desencadena el problema, es que la construcción echoignora SIGPIPE. También puede reproducir el problema mediante el uso de en env echolugar de echo(para forzar el uso del echobinario real ). (Compare también la salida de { echo hi; echo $? >&2; } | exit 1y { env echo hi; echo $? >&2; } | exit 1.)
Lucas Werkmeister
1

Este problema me ha molestado durante años. Gracias a jilliagre por empujar en la dirección correcta.

Reiterando un poco la pregunta, en mi caja de Linux, esto se cierra como se esperaba:

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

Pero si agrego una tubería, no se cierra como se esperaba:

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

Eso me frustró por años. Al considerar la respuesta escrita por jilliagre, se me ocurrió esta maravillosa solución:

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

QED ...

Bueno, no del todo. Aquí hay algo un poco más complicado:

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

Esto no funciona bien. Agregué el || exitpara que sepa cómo terminar antes, pero el primero echono coincide con el, greppor lo que el ciclo se cierra de inmediato. En este caso, realmente no está interesado en el estado de salida de grep. Mi solución es agregar otro cat. Entonces, aquí hay un script inventado llamado "decenas":

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

Esto termina correctamente cuando se ejecuta como tens | head. Gracias a Dios.

PaulC
fuente