Text processing
tee
Copy standard input to standard output and to one or more files at the same time.
tee [OPTION...] [FILE...]
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
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
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
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"
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
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
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
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: $?"
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
teetruncates its files immediately, sosort file | tee fileempties 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. Useset -o pipefailor${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 logor 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).
-aand-i(ignore interrupts) are POSIX.-pand--output-errorfor 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.
| Option | What it does |
|---|---|
-a, --append | Append to the files instead of overwriting them. |
-i, --ignore-interrupts | Ignore the interrupt signal (Ctrl-C). |
-p | Diagnose 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
catConcatenate files and print them to standard output; also the quickest way to see a short file.grepPrint the lines of files (or standard input) that match a pattern.echoPrint its arguments separated by spaces, followed by a newline.- Pipes and pipelinesHow cmd1 | cmd2 connects processes, which exit status a pipeline returns, PIPESTATUS, pipefail, SIGPIPE, and why variables set in a piped loop vanish.
- RedirectionHow >, >>, 2>, 2>&1, &>, < and exec move a command's input and output between files and file descriptors, and why the order matters.
- Exit codesWhat exit statuses mean, how $? works, the reserved values 126, 127 and 128+N, PIPESTATUS, and how to choose codes for your own scripts.