Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ printf 'line1\nline2\n' | tee teed.txt
line1
line2

$ cat teed.txt
line1
line2

tee 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./long_job.sh | tee run.log

Now you watch it happen and have a complete record afterwards. No choice to make.

The form you will actually type

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./long_job.sh 2>&1 | tee run.log

2>&1 first, because a pipe carries only stream two (Chapter 12.5) and a log missing the error messages is the wrong log. Measured:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ { echo n; echo e >&2; } 2>&1 | tee run.log >/dev/null
$ cat run.log
n
e

Without 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo line3 | tee -a teed.txt
line3
$ cat teed.txt
line1
line2
line3

-a appends instead of replacing, the same distinction as > and >>.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ echo both | tee f1.out f2.out
both
$ cat f1.out f2.out
both
both

Several 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
grep ERROR run.log | tee errors.txt | wc -l

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
grep -h ERROR *.log \
  | tee /tmp/stage1.txt \
  | sed 's/.*ERROR: //' \
  | tee /tmp/stage2.txt \
  | sort | uniq -c | sort -rn

Now 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ false 2>&1 | tee x.log >/dev/null
$ echo "  status: $?"
  status: 0

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ ( set -o pipefail; false 2>&1 | tee x.log >/dev/null ); echo "  with pipefail: $?"
  with pipefail: 1

5. Writing Where You Cannot

A well-known use that is worth understanding rather than copying:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
echo "something" | sudo tee /etc/some/config > /dev/null

The obvious form does not work:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
sudo echo "something" > /etc/some/config     # fails: permission denied

Because 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
echo "export PATH=/opt/tool/bin:\$PATH" | sudo tee -a /etc/profile.d/tool.sh > /dev/null

This 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./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):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./long_job.sh > run.log 2>&1 &
tail -f run.log

This 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
./verbose_tool.sh 2>&1 | grep -v '^DEBUG' | tee run.log

7. In Practice

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#!/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>&1 before the pipe, so the log is complete.
  • status=${PIPESTATUS[0]} immediately afterwards — the regression's status, not tee'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.
  • | tee on the summary too, so it is both printed and saved — the same reasoning one level down.

Practice

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
mkdir -p /tmp/tee && cd /tmp/tee

1 — Both at once.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
printf 'line1\nline2\n' | tee teed.txt
echo "--- file holds: $(tr '\n' ' ' < teed.txt)"

2 — Append, and write to two files.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
{ 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
( 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.log

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

  • tee copies 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.
  • -a appends; several filenames write the same stream to all of them.
  • tee in the middle of a pipeline taps the stream without interrupting it — useful for inspecting a stage.
  • tee /dev/tty forces a copy to the terminal even when tee's output goes elsewhere.
  • tee becomes the last stage, so it owns the exit status — and it almost always succeeds.
  • Any pipeline ending in tee needs pipefail or ${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 & and tail -f — better than tee in almost every respect.
  • | sudo tee file > /dev/null writes 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.