tee reads standard input and writes it to both standard output and one or more files, like a T-shaped pipe fitting. You see the output and keep a copy. It also solves the classic “sudo with redirection” problem.
command | tee file
Save output while watching it
sudo apt upgrade 2>&1 | tee upgrade.log
./deploy.sh 2>&1 | tee deploy-$(date +%F).log
2>&1 sends error messages into the pipe too. Without it, only standard output reaches the file.
Append instead of overwrite
echo "$(date) backup done" | tee -a /var/log/backup.log
-a appends. Without it, tee truncates the file first.
Write root-owned files: sudo tee
This doesn’t work:
sudo echo "vm.swappiness=10" > /etc/sysctl.d/99-swap.conf
# Permission denied
Your own (non-root) shell performs the > redirect before sudo runs. Instead, let a root process write the file:
echo "vm.swappiness=10" | sudo tee /etc/sysctl.d/99-swap.conf
echo "10.0.0.5 db" | sudo tee -a /etc/hosts
To avoid echoing the content back to the screen, discard stdout:
echo "..." | sudo tee /etc/file > /dev/null
Multi-line content with a heredoc:
sudo tee /etc/apt/apt.conf.d/20auto-upgrades > /dev/null <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF
This pattern shows up in countless install guides, including the Docker install repository setup.
Write to several files at once
echo "maintenance" | tee /var/www/a/status.txt /var/www/b/status.txt
Branch a pipeline
tee can save an intermediate stage while the pipeline continues:
grep ' 500 ' access.log | tee errors-500.log | wc -l
That saves the matching lines and prints their count. With process substitution in Bash, you can send the data to several commands at once:
cat access.log | tee >(grep ' 404 ' > 404.log) >(grep ' 500 ' > 500.log) > /dev/null
Log a whole script
Inside a Bash script, send everything to a log as well as the terminal:
#!/usr/bin/env bash
exec > >(tee -a /var/log/myscript.log) 2>&1
echo "starting"
Every line after exec goes to both places. For cron jobs, where there’s no terminal, plain redirection (>> log 2>&1) is enough. See crontab.
Exit status in pipelines
In cmd | tee log, the pipeline’s exit status is tee’s (almost always 0), not cmd’s. A failed command looks successful. In Bash, enable pipefail:
set -o pipefail
./build.sh 2>&1 | tee build.log # now fails if build.sh fails
Or check ${PIPESTATUS[0]} right after the pipeline.
Buffering
Some programs buffer their output when it goes to a pipe, so lines reach tee in bursts. Python is one example. Run them unbuffered:
python3 -u script.py | tee out.log
stdbuf -oL some-command | tee out.log
Quick reference
| Task | Command |
|---|---|
| Save and show | cmd | tee file |
| Include errors | cmd 2>&1 | tee file |
| Append | cmd | tee -a file |
| Write as root | echo x | sudo tee file > /dev/null |
| Several files | cmd | tee f1 f2 |
| Keep exit status | set -o pipefail |
Related: echo and cat and heredocs.