Skip to content

tee saves and passes on

Level: 101 · you want to watch a command's output and keep a copy of it

One line: tee file copies its stdin to stdout and to every file it is named, so cmd | tee file | next keeps a copy without breaking the pipe. It empties each file first unless you give it -a, and it copies only what reaches its stdin, which is why a build log needs 2>&1 before the pipe.

Measured

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

$ printf "alma\ndarkfi\nnano\n" | tee names.txt | wc -l | tr -d " "
3

$ cat names.txt
alma
darkfi
nano

$ echo "first run" | tee run.log
first run

$ echo "second run" | tee run.log; echo "--- run.log:"; cat run.log
second run
--- run.log:
second run

$ echo "third run" | tee -a run.log > /dev/null; echo "--- run.log:"; cat run.log
--- run.log:
second run
third run

$ echo hello | tee a.txt b.txt c.txt; grep . a.txt b.txt c.txt
hello
a.txt:hello
b.txt:hello
c.txt:hello

$ printf "b\na\nb\n" | sort | tee after_sort.txt | uniq; echo "--- after_sort.txt:"; cat after_sort.txt
a
b
--- after_sort.txt:
a
b
b

$ build() { echo compiling; echo "warning: x is unused" >&2; echo done; }

$ build | tee build.log > /dev/null; echo "--- build.log:"; cat build.log
warning: x is unused
--- build.log:
compiling
done

$ build 2>&1 | tee build.log; echo "--- build.log:"; cat build.log
compiling
warning: x is unused
done
--- build.log:
compiling
warning: x is unused
done
  • A T-junction. wc -l counted three lines, and names.txt holds the same three: one stream, two destinations.
  • tee empties the file first. The second run replaced the first in run.log. -a appended the third instead. > /dev/null after tee keeps the file and silences the copy on the screen.
  • Several files at once. One tee a.txt b.txt c.txt wrote all three and printed hello once.
  • In the middle of a pipeline, tee records what one stage handed to the next: after_sort.txt still has the two b lines that uniq merged.
  • A pipe carries stdout only. build | tee build.log logged compiling and done. The warning went to stderr, past tee, and never reached the log. With 2>&1 in front of the pipe, all three lines went through tee, in the order build wrote them.

A copy on the screen and a copy in a file is the everyday use: make 2>&1 | tee build.log lets you watch a long build and read it again afterwards. Checking whether that build failed is its own lesson, tee and the exit status.

On a Mac

Nothing changes. The key is shared: the BSD tee a Mac ships and GNU tee from coreutils 9.4 printed the same bytes for everything on this page.

In zsh and fish

tee is a program, not shell syntax, so it works the same everywhere. What differs is the shorthand for sending stderr down the pipe with stdout. zsh spells 2>&1 | as |&:

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

$ printf "alma\ndarkfi\nnano\n" | tee names.txt | wc -l | tr -d " "
3

$ echo "third run" | tee -a run.log > /dev/null; echo "fourth run" | tee -a run.log > /dev/null; cat run.log
third run
fourth run

$ build() { echo compiling; echo "warning: x is unused" >&2; echo done }

$ build |& tee build.log; echo "--- build.log:"; cat build.log
compiling
warning: x is unused
done
--- build.log:
compiling
warning: x is unused
done

fish spells it &|, and a function's stderr still goes past tee unless you send it down the pipe:

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

$ printf "alma\ndarkfi\nnano\n" | tee names.txt | wc -l | tr -d " "
3

$ echo "third run" | tee -a run.log > /dev/null; echo "fourth run" | tee -a run.log > /dev/null; cat run.log
third run
fourth run

$ function build; echo compiling; echo "warning: x is unused" >&2; echo done; end

$ build | tee build.log > /dev/null; echo "--- build.log:"; cat build.log
warning: x is unused
--- build.log:
compiling
done

$ build 2>&1 | tee build.log; echo "--- build.log:"; cat build.log
compiling
warning: x is unused
done
--- build.log:
compiling
warning: x is unused
done

$ build &| tee build.log; echo "--- build.log:"; cat build.log
compiling
warning: x is unused
done
--- build.log:
compiling
warning: x is unused
done
bash zsh fish
save and see cmd \| tee f cmd \| tee f cmd \| tee f
stderr into the log too cmd 2>&1 \| tee f cmd 2>&1 \| tee f, cmd \|& tee f cmd 2>&1 \| tee f, cmd &\| tee f

If you are coming from another library

  • Encodings. split, paste, look and tee uses tee as the control: a NUL byte and a byte that cannot appear in UTF-8 go through it untouched, which is what makes … | tee raw | … the place to look when a pipeline mangles its input.
  • Python. Standard in, standard out, and pipes ↗ outlines why a Python program's output can reach tee in blocks rather than line by line: stdout is buffered differently when it is a pipe.

See also