почему цикл bash while не завершается при передаче по завершенной подкоманде?

12

Почему команда ниже не выходит? Вместо выхода цикл работает бесконечно.

В то время как я обнаружил это поведение с помощью более сложной настройки, простейшая форма команды сводится к следующему.

Не выходит:

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

Там нет опечаток выше. Каждый '|' это труба. «Выход 1» означает другой процесс, который запущен и завершен.

Я ожидаю, что «выход 1» вызовет SIGPIPE в цикле while (запись в канал без читателя) и разрыв цикла. Но цикл продолжает работать.

Почему команда не останавливается?

stephen.z
источник
Zsh выходит нормально.
Брайам

Ответы:

13

Это связано с выбором в реализации.

Выполнение одного и того же сценария в Solaris с ksh93приводит к другому поведению:

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

Проблема вызывает внутренний конвейер, без которого цикл завершается независимо от оболочки / ОС:

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

cat получает сигнал SIGPIPE под bash, но оболочка все равно повторяет цикл.

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

Документация Bash гласит:

Оболочка ожидает завершения всех команд в конвейере, прежде чем вернуть значение.

Кш документация гласит:

Каждая команда, кроме, возможно, последней, запускается как отдельный процесс; оболочка ожидает завершения последней команды .

POSIX заявляет:

Если конвейер не находится в фоновом режиме (см. Асинхронные списки), оболочка должна дождаться завершения последней команды, указанной в конвейере, а также может дождаться завершения всех команд .

jlliagre
источник
Я думаю, что это не только внутренний конвейер, который вызывает проблему, это то, что встроенный echoигнорирует SIGPIPE. Вы также можете воспроизвести проблему, используя env echoвместо echo(для принудительного использования фактического echoдвоичного файла). (Сравните также вывод { echo hi; echo $? >&2; } | exit 1и { env echo hi; echo $? >&2; } | exit 1.)
Лукас Веркмейстер
1

Эта проблема беспокоила меня годами. Спасибо jilliagre за толчок в правильном направлении.

Немного переформулировав вопрос, на моем linux box это выходит как положено:

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

Но если я добавлю канал, он не выйдет, как ожидалось:

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

Это расстраивало меня годами. Рассматривая ответ, написанный jilliagre, я придумал это замечательное исправление:

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

QED ...

Ну, не совсем. Вот что-то более сложное:

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

Это не работает правильно. Я добавил, || exitтак что он знает, как завершить работу рано, но самый первый echoне совпадает, grepтак что цикл завершается сразу. В этом случае вы действительно не заинтересованы в статусе выхода grep. Мой обходной путь - добавить еще один cat. Итак, вот надуманный сценарий под названием «десятки»:

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

Это правильно завершается при запуске как tens | head. Слава Богу.

PaulC
источник