Skip to content

head closes the pipe early

Level: 201 · you have seen status 141, or Broken pipe, from a pipeline that ends in head

One line: When the last reader of a pipe exits, the next write into the pipe kills the writer with SIGPIPE, and the shell reports that as 141, which is 128 + 13. That is the only reason yes | head -n 1 ever ends. A writer that had already finished reports 0. A writer that ignores SIGPIPE gets an error instead, prints Broken pipe and exits 1. And a loop in fish is not stopped at all unless it checks whether its echo failed.

Measured

Verified output of head_closes_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ yes | head -n 2; echo "PIPESTATUS ${PIPESTATUS[*]}"
y
y
PIPESTATUS 141 0

$ echo "141 is 128 + $((141 - 128)), and signal 13 is $(kill -l 13)"
141 is 128 + 13, and signal 13 is PIPE

$ seq 30000 | head -n 1; echo "PIPESTATUS ${PIPESTATUS[*]}"
1
PIPESTATUS 141 0

$ seq 3 | head -n 1; echo "PIPESTATUS ${PIPESTATUS[*]}"
1
PIPESTATUS 0 0

$ last=none; for i in $(seq 30000); do echo "$i"; last=$i; done | head -n 1; echo "PIPESTATUS ${PIPESTATUS[*]}, last is $last"
1
PIPESTATUS 141 0, last is none
  • yes never stops by itself. head -n 2 printed two lines and exited, which closed the only read end of the pipe. yes was still writing, and its next write killed it with SIGPIPE: status 141, and no message, because the signal ended yes before it could print one.
  • 141 is 128 + 13. A shell reports "killed by signal N" as 128 + N, and kill -l 13 names signal 13: PIPE.
  • Any writer with more left to write dies the same way. seq 30000 writes about 170 KB, more than the pipe and head take in before head exits.
  • A writer that has finished does not. seq 3 was not killed: its three short lines fit in the pipe, so it finished writing without waiting for head. The pipeline is the same shape and its status is 0 instead of 141. What decides it is how much the left side still had to write when the right side stopped reading.
  • A shell loop is a writer too. The for loop is one stage, run in a child bash (each stage runs in a subshell). One of its echos hit the closed pipe, the signal killed the whole child, and the shell's last is still none.

set -o pipefail counts each of these 141s as a failure. A pipeline reports its last command measures that.

On a Mac

The key above is shared: BSD yes, seq and head die of SIGPIPE in the same way, and bash 3.2 reports 141. The machines part only when SIGPIPE is ignored, because then yes survives the write and reports the failure in its own words. The report goes to a file and is printed after the pipeline, so it cannot race head's line:

Verified output of head_closes_ignored_sh.sh on Linux — regenerated by tools/run_examples.py, never hand-typed.

$ trap "" PIPE; yes 2>err | head -n 1; echo "PIPESTATUS ${PIPESTATUS[*]}"; cat err; trap - PIPE
y
PIPESTATUS 1 0
yes: standard output: Broken pipe

$ yes 2>err | head -n 1; echo "PIPESTATUS ${PIPESTATUS[*]}"; cat err
y
PIPESTATUS 141 0

$ trap '' PIPE; bash -c 'trap - PIPE; yes 2>err | head -n 1; echo "PIPESTATUS ${PIPESTATUS[*]}"; cat err'; trap - PIPE
y
PIPESTATUS 1 0
yes: standard output: Broken pipe

Verified output of head_closes_ignored_sh.sh on macOS — regenerated by tools/run_examples.py, never hand-typed.

$ trap "" PIPE; yes 2>err | head -n 1; echo "PIPESTATUS ${PIPESTATUS[*]}"; cat err; trap - PIPE
y
PIPESTATUS 1 0
yes: stdout: Broken pipe

$ yes 2>err | head -n 1; echo "PIPESTATUS ${PIPESTATUS[*]}"; cat err
y
PIPESTATUS 141 0

$ trap '' PIPE; bash -c 'trap - PIPE; yes 2>err | head -n 1; echo "PIPESTATUS ${PIPESTATUS[*]}"; cat err'; trap - PIPE
y
PIPESTATUS 1 0
yes: stdout: Broken pipe
  • trap "" PIPE makes the shell ignore SIGPIPE, and the commands it starts inherit the setting. The write that would have killed yes fails with an error instead, and yes reports it and exits 1. GNU yes calls the stream standard output and BSD yes calls it stdout. That wording is the whole reason this key is split.
  • trap - PIPE restores the default for the commands that follow: the second line is 141 again, with nothing in err.
  • A shell that started with SIGPIPE ignored cannot restore it. The third line starts a child bash while its parent ignores SIGPIPE. The child's trap - PIPE changed nothing, and yes still reported Broken pipe. The bash manual says why: a signal that was ignored when a non-interactive shell started cannot be trapped or reset. So if something starts your script with SIGPIPE ignored, yes | head in that script prints the message and returns 1, and nothing inside the script can change it.

This library's runner starts every example with SIGPIPE at its default (see CONTRIBUTING), which is why the pages show 141.

In zsh and fish

zsh does what bash does:

Verified output of head_closes_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ yes | head -n 1; echo "pipestatus $pipestatus"
y
pipestatus 141 0

$ last=none; for i in $(seq 30000); do echo $i; last=$i; done | head -n 1; echo "pipestatus $pipestatus, last is $last"
1
pipestatus 141 0, last is none

$ trap "" PIPE; yes 2>/dev/null | head -n 1; echo "pipestatus $pipestatus"; trap - PIPE
y
pipestatus 1 0

yes is killed, 141. The for loop in the first stage runs in a child zsh, the signal kills that child, and last stays none. trap "" PIPE turns 141 into 1. The message is yes's own and is shown above, so here it goes to /dev/null.

fish agrees about yes and disagrees about the loop:

Verified output of head_closes_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.

$ yes | head -n 1; echo "pipestatus $pipestatus"
y
pipestatus 141 0

$ set last none; for i in (seq 30000); echo $i; set last $i; end | head -n 1; echo "pipestatus $pipestatus, last is $last"
1
pipestatus 1 0, last is 30000

$ set last none; for i in (seq 30000); echo $i; or break; set last $i; end | head -n 1; if test $last -lt 30000; echo "the loop stopped early"; end
1
the loop stopped early

$ trap "" PIPE; yes | head -n 1; echo "pipestatus $pipestatus"; trap - PIPE
y
pipestatus 141 0
  • yes is an external command, and fish reports its death as 141 like the other shells.
  • The for loop is not a separate process. fish runs it inside the fish process (each stage runs in a subshell), fish itself was not killed, and the loop carried on after head had gone: last is 30000, every iteration run. The stage's status is 1, not 141.
  • or break is the fix. Once the pipe is closed, echo fails, and the loop can stop on that. the loop stopped early means the test $last -lt 30000 came out true. The iteration it stopped at depends on timing, so the example does not print it.
  • trap "" PIPE did not reach yes: still 141, where bash and zsh gave 1.
bash zsh fish
yes \| head -n 1 141 141 141
a loop in the first stage, feeding head killed, 141 killed, 141 runs to its end, status 1
trap "" PIPE first, then yes \| head -n 1 1, and Broken pipe 1 141

If you are coming from another library

  • Python. stdin, stdout and pipes ↗ is the same event from inside the writer: a print() into a closed pipe raises BrokenPipeError rather than ending the process. That page is still an outline, with the measurement noted.
  • Concurrency. Who waits when main returns? ↗ reads an exit status the same way from the other side: a C++ std::thread destroyed without being joined aborts the process, and the shell reports 134, which is 128 + 6, SIGABRT.

See also