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.

  • Linux
  • Bash
  • Command Line