tee and the exit status¶
Level: 201 · your build runs as make 2>&1 | tee build.log and has never once failed
One line: cmd | tee log is a pipeline, and a pipeline reports its last command, so a build that fails through tee reports tee's success. PIPESTATUS, set -o pipefail in bash and zsh, or fish's $pipestatus bring the failure back. tee also has a status of its own: 1 when it cannot write a file, and 141 when the reader after it stops early, which cuts the log short.
Measured¶
Verified output of tee_status_sh.sh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ false | tee log.txt; echo "status $?"
status 0
$ build() { echo compiling; echo "error: x is undefined" >&2; return 2; }
$ build 2>&1 | tee build.log; echo "status $?, PIPESTATUS ${PIPESTATUS[*]}"
compiling
error: x is undefined
status 0, PIPESTATUS 2 0
$ if build 2>&1 | tee build.log > /dev/null; then echo "build succeeded?"; else echo "build failed"; fi
build succeeded?
$ set -o pipefail; if build 2>&1 | tee build.log > /dev/null; then echo "build succeeded?"; else echo "build failed"; fi; set +o pipefail
build failed
$ mkdir locked; chmod 555 locked
$ echo data | tee locked/log.txt; echo "status $?"
tee: locked/log.txt: Permission denied
data
status 1
$ chmod 755 locked
$ seq 1 1000000 | tee all.txt | head -n 1; echo "PIPESTATUS ${PIPESTATUS[*]}"
1
PIPESTATUS 141 141 0
$ if [ "$(wc -l < all.txt)" -lt 1000000 ]; then echo "all.txt stopped early"; fi
all.txt stopped early
false | teeis 0. This is A pipeline reports its last command with a log file attached.- The build failed with 2, and the pipeline said 0.
PIPESTATUSkept the 2. Soif build 2>&1 | tee build.logtook the success branch, and a CI step written that way stays green. set -o pipefailmakes the rightmost failing stage count, and the sameiftook the failure branch.teefails on its own terms. It could not createlocked/log.txtand said so, still copieddatato its stdout, and exited 1.teeis a stage like any other.head -n 1exited after one line.tee's next write went into a pipe nobody was reading, SIGPIPE killed it (141), andseqwent the same way.all.txtholds only whatteecopied before it died, far fewer than the million linesseqwould have written.
So a log is only complete if everything after tee reads to the end. cmd | tee log | head saves the start of the output, not all of it.
On a Mac¶
Nothing changes. The key is shared: bash 3.2 has PIPESTATUS and pipefail, and BSD tee is killed by SIGPIPE exactly as GNU tee is.
In zsh and fish¶
zsh reads the stages from $pipestatus, counted from 1, and uses |& for 2>&1 |:
Verified output of tee_status_zsh.zsh, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ false | tee log.txt; echo "status $?, pipestatus $pipestatus"
status 0, pipestatus 1 0
$ build() { echo compiling; echo "error: x is undefined" >&2; return 2 }
$ build |& tee build.log; echo "status $?, pipestatus $pipestatus"
compiling
error: x is undefined
status 0, pipestatus 2 0
$ setopt pipefail; if build |& tee build.log > /dev/null; then echo "build succeeded?"; else echo "build failed"; fi; unsetopt pipefail
build failed
$ seq 1 1000000 | tee all.txt | head -n 1; echo "pipestatus $pipestatus"
1
pipestatus 141 141 0
fish has no pipefail, so the check is written by hand:
Verified output of tee_status_fish.fish, identical on Linux and macOS — regenerated by tools/run_examples.py, never hand-typed.
$ false | tee log.txt; echo "status $status, pipestatus $pipestatus"
status 0, pipestatus 1 0
$ function build; echo compiling; echo "error: x is undefined" >&2; return 2; end
$ build &| tee build.log; echo "status $status, pipestatus $pipestatus"
compiling
error: x is undefined
status 0, pipestatus 2 0
$ build &| tee build.log > /dev/null; set -l stages $pipestatus; if test $stages[1] -ne 0; echo "build failed with $stages[1]"; end
build failed with 2
$ seq 1 1000000 | tee all.txt | head -n 1; echo "pipestatus $pipestatus"
1
pipestatus 141 141 0
Copy $pipestatus into a variable before testing it: test is itself a command, and it would replace $pipestatus with its own. The pipeline lesson covers that trap in full.
| bash | zsh | fish | |
|---|---|---|---|
did cmd in cmd \| tee log fail? |
${PIPESTATUS[0]} |
${pipestatus[1]} |
$pipestatus[1] |
| make any failure count | set -o pipefail |
setopt pipefail |
no option; test $pipestatus |
If you are coming from another library¶
- Rust. Standard error, and exit status ↗ is the number from the program's side: the only part of a program's result another program reads, and the part
| teehides. - Python. Standard in, standard out, and pipes ↗ outlines the reader that stops early, seen from a Python program in
tee's place:BrokenPipeErrorinstead of a quiet 141.
See also¶
- A pipeline reports its last command —
PIPESTATUS,pipefailand SIGPIPE withouttee - tee saves and passes on —
2>&1 | tee build.log - bash manual, Pipelines ↗
- zsh manual, Options ↗ —
PIPE_FAIL - fish language, special variables ↗ —
pipestatus