Linux & Bash · Part I · Module 12 · Streams, Redirection and Pipes
tee: Saving a Stream While Still Watching It
Redirecting a long run to a log means staring at a blank terminal for forty minutes. Not redirecting it means the record scrolls away. These are not the only two options, and the tool that gives you both is one word long — with one consequence worth meeting deliberately.
Foundation16 min readLinuxPart IFundamentalsStreamsPipes
Part I · Module 12 · Chapter 12.6 · tee: Saving a Stream While Still Watching It
Chapter 12.2 offered a choice: output on your screen, or output in a file. For a forty-minute build that is a bad choice to have to make.
1. Both at Once
$ printf 'line1\nline2\n' | tee teed.txt
line1
line2
$ cat teed.txt
line1
line2tee copies its input to a file and to its own output. It reads stream one and writes the same bytes to both places.
The name is from a T-shaped plumbing junction: one flow in, two out.
./long_job.sh | tee run.logNow you watch it happen and have a complete record afterwards. No choice to make.
The form you will actually type
./long_job.sh 2>&1 | tee run.log2>&1 first, because a pipe carries only stream two (Chapter 12.5) and a log missing the error messages is the wrong log. Measured:
$ { echo n; echo e >&2; } 2>&1 | tee run.log >/dev/null
$ cat run.log
n
eWithout the 2>&1, run.log would hold only n and the error would have gone to your terminal — visible at the time, absent from the record.
2. Appending, and Several Files
$ echo line3 | tee -a teed.txt
line3
$ cat teed.txt
line1
line2
line3-a appends instead of replacing, the same distinction as > and >>.
$ echo both | tee f1.out f2.out
both
$ cat f1.out f2.out
both
bothSeveral filenames write the same stream to all of them. Occasionally useful for writing a run log into both a per-run directory and a shared one.
3. In the Middle of a Pipeline
tee writes to its own output as well as to the file, so it can sit between two stages and tap the stream as it passes:
grep ERROR run.log | tee errors.txt | wc -lThe error lines are saved to errors.txt and counted. One pass, both results.
This is genuinely useful when a pipeline is long and you want to see what an intermediate stage produced:
grep -h ERROR *.log \
| tee /tmp/stage1.txt \
| sed 's/.*ERROR: //' \
| tee /tmp/stage2.txt \
| sort | uniq -c | sort -rnNow when the final answer looks wrong you can inspect each stage's output without rerunning anything. Remove the taps once it works.
4. The Trap: tee Owns the Exit Status
$ false 2>&1 | tee x.log >/dev/null
$ echo " status: $?"
status: 0The pipeline reported success, and false plainly failed.
Chapter 12.5 left this as a named problem: a pipeline's exit status is its last command's, and tee almost always succeeds. Putting tee at the end of a pipeline therefore throws away the status of the thing you actually ran.
This matters most in exactly the place tee is most useful:
./regress.sh 2>&1 | tee regress.log
# $? is tee's. The regression's result has been discarded.A continuous-integration step written that way always passes.
$ ( set -o pipefail; false 2>&1 | tee x.log >/dev/null ); echo " with pipefail: $?"
with pipefail: 15. Writing Where You Cannot
A well-known use that is worth understanding rather than copying:
echo "something" | sudo tee /etc/some/config > /dev/nullThe obvious form does not work:
sudo echo "something" > /etc/some/config # fails: permission deniedBecause the shell performs the redirect, and the shell is not running as root. sudo elevates echo, but echo's output is being redirected by your unprivileged shell into a file it cannot open — and Chapter 12.2 established that the shell opens the file first, before sudo is anywhere near it.
sudo tee works because the writing is done by tee, which is the elevated process. The > /dev/null on the end discards tee's copy of the output, which you do not need on screen.
Use -a for appending:
echo "export PATH=/opt/tool/bin:\$PATH" | sudo tee -a /etc/profile.d/tool.sh > /dev/nullThis is the standard idiom for editing a system file from a script, and the reason it looks strange is worth knowing: it is a workaround for who performs the redirect.
6. When Not to Use It
tee is not free — it is a process and a pipe, and it means the pipeline's status needs handling. Three cases where something simpler is better:
You do not need to watch. Use a plain redirect:
./nightly.sh > nightly.log 2>&1 # nobody is looking at 3 a.m.You want to watch afterwards, not during. Redirect and follow (Chapter 4.4):
./long_job.sh > run.log 2>&1 &
tail -f run.logThis is often better than tee: the job is backgrounded so it survives your terminal scrolling, you can stop following and come back, and $? is the job's rather than tail's. For a job you will walk away from, this beats tee.
You only want part of it. Filter first, so the log is the useful subset:
./verbose_tool.sh 2>&1 | grep -v '^DEBUG' | tee run.log7. In Practice
#!/bin/bash
set -uo pipefail
run="${1:?usage: $0 <run-name>}"
logdir="$HOME/runs/$run"
mkdir -p "$logdir" || { echo "cannot create $logdir" >&2; exit 2; }
log="$logdir/regress.log"
echo "logging to $log"
# Both streams, watched live and recorded, with the status preserved.
./regress.sh --name "$run" 2>&1 | tee "$log"
status=${PIPESTATUS[0]}
# A summary derived from the log we just captured, not from a second run.
{
echo "run : $run"
echo "status : $status"
echo "errors : $(grep -c 'ERROR' "$log")"
echo "warnings : $(grep -c 'WARNING' "$log")"
} | tee "$logdir/summary.txt"
exit "$status"Four decisions worth naming:
2>&1before the pipe, so the log is complete.status=${PIPESTATUS[0]}immediately afterwards — the regression's status, nottee's, captured on the very next line (Chapter 11.1).- The summary is computed from the log, so it describes the run that just happened rather than requiring a second one.
| teeon the summary too, so it is both printed and saved — the same reasoning one level down.
Practice
mkdir -p /tmp/tee && cd /tmp/tee1 — Both at once.
printf 'line1\nline2\n' | tee teed.txt
echo "--- file holds: $(tr '\n' ' ' < teed.txt)"2 — Append, and write to two files.
echo line3 | tee -a teed.txt
echo both | tee f1.out f2.out >/dev/null
echo "f1=[$(cat f1.out)] f2=[$(cat f2.out)]"3 — Capture both streams.
{ echo n; echo e >&2; } | tee only_stdout.log >/dev/null
echo "without 2>&1: [$(tr '\n' ' ' < only_stdout.log)]"
{ echo n; echo e >&2; } 2>&1 | tee both.log >/dev/null
echo "with 2>&1: [$(tr '\n' ' ' < both.log)]"4 — Tap the middle of a pipeline.
printf 'a\nERROR x\nb\nERROR y\n' > src.txt
grep ERROR src.txt | tee errors.txt | wc -l
echo "errors.txt = [$(tr '\n' ' ' < errors.txt)]"5 — Lose an exit status, then keep it.
false 2>&1 | tee x.log >/dev/null; echo " plain: $?"
( set -o pipefail; false 2>&1 | tee x.log >/dev/null ); echo " pipefail: $?"
false 2>&1 | tee x.log >/dev/null; echo " PIPESTATUS[0]: ${PIPESTATUS[0]}"6 — Compare with the background-and-follow approach.
( for i in 1 2 3; do echo "line $i"; sleep 1; done ) > slow.log 2>&1 &
tail -f slow.log &
sleep 4; kill %2 2>/dev/null; wait 2>/dev/null
cat slow.log7 — Work out why sudo echo > file fails. Write the answer down before reading §5 again.
8 — Clean up. cd /tmp && rm -rf /tmp/tee
Check Your Understanding
1. What does tee do, and where does the name come from?
2. Why is ./job.sh | tee run.log usually the wrong form?
3. ./regress.sh 2>&1 | tee regress.log and the CI step always passes. Why, and what are two fixes?
4. When is > log 2>&1 & plus tail -f better than tee?
5. Why does sudo echo "x" > /etc/file fail while echo "x" | sudo tee /etc/file works?
6. What does tee /dev/tty achieve that plain tee cannot?
8. Remember
teecopies its input to a file and to its own output — watch it and keep it, with no choice to make.- The form to type is
2>&1 | tee run.log. Without the redirect the log is missing every error. -aappends; several filenames write the same stream to all of them.teein the middle of a pipeline taps the stream without interrupting it — useful for inspecting a stage.tee /dev/ttyforces a copy to the terminal even whentee's output goes elsewhere.teebecomes the last stage, so it owns the exit status — and it almost always succeeds.- Any pipeline ending in
teeneedspipefailor${PIPESTATUS[0]}. This is the usual reason a CI step passes on a failing build. - Unattended work wants a plain redirect, not
tee. - A long job you will leave wants
> log 2>&1 &andtail -f— better thanteein almost every respect. | sudo tee file > /dev/nullwrites to a file you do not own, because the shell does the redirect and the shell is not elevated.
Two of the commands in this chapter ended in > /dev/null without explanation. The next chapter is that — and the difference between silencing output you do not want and hiding errors you needed.
Where this fits
Part of the Linux & Bash curriculum.
