opscape

Text processing

tee

Copy standard input to standard output and to one or more files at the same time.

tee [OPTION...] [FILE...]

7 of 8 examples run in Debian 13.7 on 2026-10-31 · coreutils 9.7-3 · manual: tee(1)

tee is a T-junction for a pipe: everything it reads on standard input goes both to standard output and into each file you name. Use it to save a copy of a pipeline's output while still watching it, to log a long-running command, or to capture an intermediate stage of a pipeline for debugging.

It truncates its files first unless you pass -a to append. Its other everyday job is writing a file that your shell cannot write: in cmd > /etc/file the redirection is done by your unprivileged shell, while in cmd | sudo tee /etc/file the file is opened by tee running as root.

Examples

See output and save it to a file

grep ERROR logs/app.log | tee errors.txt
echo "--- saved:"; wc -l errors.txt
output
2026-03-14 08:16:40 ERROR health check failed: database timeout after 5000ms
2026-03-14 08:30:00 ERROR disk usage on /var at 91%
--- saved:
2 errors.txt

The matching lines appear on the terminal and are written to errors.txt at the same time.

Append instead of overwriting

echo "first run" | tee run.log
echo "second run" | tee -a run.log
echo "---"; cat run.log
output
first run
second run
---
first run
second run

Without -a, tee truncates the file. With -a it appends, so run.log ends up with both lines.

Write a file without printing anything

printf 'API_URL=http://localhost:9090\nDEBUG=true\n' | tee .env.local > /dev/null
cat .env.local
output
API_URL=http://localhost:9090
DEBUG=true

Redirecting tee's own output to /dev/null leaves only the file write. This is the shape used with sudo tee to write root-owned files from a script.

Write to several files at once

cut -d, -f2 data/users.csv | tail -n +2 | tee names.txt names.bak | wc -l
cmp names.txt names.bak && echo "both copies identical"
output
8
both copies identical

tee writes the same stream to every file named and still passes it on, so wc -l counts the eight names.

Capture an intermediate stage of a pipeline

awk '{print $1}' access.log | sort | tee ips-sorted.txt | uniq -c | sort -rn | head -3
echo "--- stage saved:"; wc -l ips-sorted.txt
output
      5 192.0.2.10
      4 203.0.113.42
      4 198.51.100.7
--- stage saved:
20 ips-sorted.txt

Placed in the middle of a pipeline, tee records exactly what flowed through that point (all 20 sorted IPs) while the rest of the pipeline continues. Handy for checking which stage of a long pipeline went wrong.

Log both output and errors

ls todo.txt nosuch.txt 2>&1 | tee ls.log
echo "--- log:"; cat ls.log
output
ls: cannot access 'nosuch.txt': No such file or directory
todo.txt
--- log:
ls: cannot access 'nosuch.txt': No such file or directory
todo.txt

A pipe carries only stdout, so 2>&1 merges stderr into it first. Without that, the error would reach the terminal but not the log.

Write a root-owned file

echo '10.0.0.50 web03' | sudo tee -a /etc/hosts
explain

Not run in the sandbox: the sandbox has no sudo. No output is shown because none was captured.

sudo echo ... >> /etc/hosts fails because the redirection runs in your own shell before sudo starts. Here tee is the process that opens the file, and it runs as root.

Keep the real exit status through tee

grep CRITICAL logs/app.log | tee crit.txt; echo "without pipefail: $?"
set -o pipefail
grep CRITICAL logs/app.log | tee crit.txt; echo "with pipefail: $?"
output
without pipefail: 0
with pipefail: 1

A pipeline's status is that of its last command, and tee almost always succeeds, so grep's "no match" status 1 is lost. With set -o pipefail the pipeline returns the failure. ${PIPESTATUS[0]} is the other way to read it in bash.

Common pitfalls

  • tee truncates its files immediately, so sort file | tee file empties the file before sort has finished reading it. Write to a new file and move it.

  • In a pipeline ending with tee, $? is tee's status, not the command you care about. Use set -o pipefail or ${PIPESTATUS[0]}.

  • Output through tee is often block-buffered by the producing program, so a log appears in bursts. For live output use stdbuf -oL cmd | tee log or the program's own line-buffering option.

GNU vs BSD and macOS

The examples above ran the GNU/Linux version. These are the differences that matter on macOS and the BSDs (written from their documentation, not run here).

  • -a and -i (ignore interrupts) are POSIX. -p and --output-error for handling broken pipes are GNU additions.

Options worth knowing

A selection, described in our own words. For the full list, see tee(1) or tee --help.

OptionWhat it does
-a, --appendAppend to the files instead of overwriting them.
-i, --ignore-interruptsIgnore the interrupt signal (Ctrl-C).
-pDiagnose errors writing to pipes, and keep writing to the other outputs.
--output-error[=MODE]Choose what to do on write errors: warn, warn-nopipe, exit or exit-nopipe.

Related