why doesn't a bash while loop exit when piping to terminated subcommand?

12

4

Why doesn't the command below exit? Rather than exit, the loop runs indefinitely.

While I discovered this behavior using a more complex setup, the simplest form of the command reduces to the following.

Does not exit:

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

There are no typos above. Each '|' is a pipe. The 'exit 1' stands in for another process that ran and exited.

I expect the "exit 1" to cause a SIGPIPE on the while loop (write on a pipe with no reader) and for the loop to break out. But, the loop continues running.

Why doesn't the command stop?

stephen.z

Posted 2014-01-04T07:34:54.207

Reputation: 323

zsh exits normally. – Braiam – 2014-01-04T14:27:25.747

Answers

13

It is due to a choice in implementation.

Running the same script on Solaris with ksh93 produces a different behavior:

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

What triggers the issue is the inner pipeline, without it, the loop exits whatever the shell/OS:

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

cat is getting a SIGPIPE signal under bash but the shell is iterating the loop anyway.

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 documentation states:

The shell waits for all commands in the pipeline to terminate before returning a value.

Ksh documentation states:

Each command, except possibly the last, is run as a separate process; the shell waits for the last command to terminate.

POSIX states:

If the pipeline is not in the background (see Asynchronous Lists), the shell shall wait for the last command specified in the pipeline to complete, and may also wait for all commands to complete.

jlliagre

Posted 2014-01-04T07:34:54.207

Reputation: 12 469

I think it’s not exactly the inner pipeline that triggers the issue, it’s that builtin echo ignores SIGPIPE. You can also reproduce the issue by using env echo instead of echo (to force the actual echo binary to be used). (Compare also the output of { echo hi; echo $? >&2; } | exit 1 and { env echo hi; echo $? >&2; } | exit 1.) – Lucas Werkmeister – 2018-06-07T15:54:28.290

1

This issue has bugged me for years. Thanks to jilliagre for the nudge in the right direction.

Restating the question a little, on my linux box, this quits as expected:

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

But if I add a pipe, it does not quit as expected:

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

That frustrated me for years. By considering the answer written by jilliagre, I came up with this wonderful fix:

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

Q.E.D. ...

Well, not quite. Here's something a bit more complicated:

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

This doesn't work right. I added the || exit so it knows how to terminate early, but the very first echo does not match the grep so the loop quits right away. In this case, you really aren't interested in the exit status of the grep. My work-around is to add another cat. So, here is a contrived script called "tens":

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

This properly terminates when run as tens | head. Thank God.

PaulC

Posted 2014-01-04T07:34:54.207

Reputation: 11