Linux tee: Duplicating Output Streams

The `tee` command gets its name from T-shaped pipe fittings used in plumbing—it splits a single flow into multiple directions. In Unix-like systems, `tee` reads from standard input and writes the...

Key Insights

  • The tee command reads from standard input and writes simultaneously to standard output and one or more files, making it essential for logging command output while viewing it in real-time.
  • Using echo "content" | sudo tee /path/to/file is the correct way to write to protected files, avoiding the common mistake of sudo echo "content" > /path/to/file which fails due to shell redirection happening before privilege escalation.
  • Combining tee with process substitution enables powerful parallel processing patterns where a single data stream feeds multiple independent processes simultaneously.

Introduction to tee

The tee command gets its name from T-shaped pipe fittings used in plumbing—it splits a single flow into multiple directions. In Unix-like systems, tee reads from standard input and writes the same data to both standard output and one or more files simultaneously. This simple utility solves a common problem: how do you watch command output in your terminal while also saving it to a file?

The basic syntax is straightforward:

command | tee output.txt

This takes the output from command, displays it on your terminal, and simultaneously writes it to output.txt. Let’s see a concrete example:

ls -l /etc | tee directory-listing.txt

You’ll see the directory listing scroll past on your screen, and the exact same content gets saved to directory-listing.txt. Without tee, you’d have to choose between seeing the output or saving it—or run the command twice, which is problematic for non-deterministic commands or those with side effects.

Common Use Cases

The most frequent use case for tee is capturing build or deployment logs while monitoring progress. When you’re compiling software or running a lengthy process, you want to watch it execute but also preserve the output for later analysis:

make clean && make | tee build.log

This is invaluable when builds fail—you can review the complete output without re-running potentially time-consuming compilation steps.

Another practical scenario involves filtering output while preserving the original. Suppose you’re monitoring a verbose log file but only care about errors for immediate attention:

tail -f /var/log/application.log | tee full.log | grep ERROR

This displays only error lines in your terminal while full.log captures everything. If an error appears cryptic, you can examine full.log for surrounding context without losing any data.

System administrators frequently use tee when running diagnostic commands that produce important output:

netstat -tuln | tee network-state.txt
df -h | tee disk-usage-$(date +%Y%m%d).txt

These commands let you review the information immediately while creating a timestamped record for future reference or compliance requirements.

Append Mode and Multiple Files

By default, tee overwrites the target file. The -a (append) flag changes this behavior:

echo "New log entry" | tee -a logfile.txt

This is crucial for ongoing logging scenarios where you need to accumulate output over time. Running a daily backup script? Append each run’s output:

./backup.sh | tee -a backup-history.log

One of tee’s lesser-known capabilities is writing to multiple files simultaneously:

command | tee output1.txt output2.txt output3.txt

This creates identical copies in three separate files with a single execution. Why would you need this? Consider distributing logs to different locations:

./deployment.sh | tee /var/log/deploy.log ~/deploy-backup.log /mnt/archive/deploy-$(date +%Y%m%d).log

You can combine append mode with multiple files:

command | tee -a file1.log file2.log

All specified files will be appended to rather than overwritten.

Advanced Patterns with Pipes

The real power of tee emerges in complex pipelines. You can use it at multiple stages to capture intermediate results:

command | tee raw-output.txt | grep pattern | tee filtered-output.txt | sort | tee sorted-output.txt

This creates a complete audit trail of your data transformation pipeline. Each tee preserves that stage’s output while passing data to the next stage unchanged.

Process substitution with tee enables sophisticated parallel processing. Instead of writing to files, you can feed output to multiple commands simultaneously:

command | tee >(process1) >(process2) >(process3) > /dev/null

Here’s a practical example—suppose you want to analyze web server logs in multiple ways simultaneously:

tail -f /var/log/nginx/access.log | tee \
    >(grep "POST" > post-requests.log) \
    >(grep "ERROR" > errors.log) \
    >(awk '{print $1}' | sort | uniq -c > ip-frequency.txt) \
    > /dev/null

This single pipeline extracts POST requests, errors, and IP address frequencies in parallel. The final > /dev/null discards the stdout since we’re only interested in the processed outputs.

Permission Handling with sudo

One of the most common tee use cases addresses a frequent frustration: writing to protected files. This fails:

sudo echo "nameserver 8.8.8.8" > /etc/resolv.conf

The problem? The shell processes the redirection (>) with your user’s permissions before sudo elevates privileges. The echo runs as root, but the file opening doesn’t.

The solution uses tee:

echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf

This works because tee itself runs with elevated privileges and performs the file writing. The echo runs as your normal user, but that’s fine—it just outputs to stdout, which pipes to the privileged tee process.

If you don’t want the output echoed to your terminal (since tee writes to both the file and stdout), redirect to /dev/null:

echo "config line" | sudo tee /etc/config.conf > /dev/null

For appending to protected files:

echo "new entry" | sudo tee -a /var/log/protected.log > /dev/null

This pattern is so common that experienced Linux users internalize it as the standard way to modify system files from the command line.

Practical Real-World Examples

Let’s examine complete real-world scenarios where tee proves indispensable.

Monitoring and archiving system logs with filtering:

tail -f /var/log/syslog | tee -a syslog-archive-$(date +%Y%m%d).log | grep -E "ERROR|CRITICAL"

This continuously monitors the system log, archives everything to a dated file, and highlights only errors and critical messages on screen.

Debugging complex pipelines:

cat data.csv | \
    tee 01-raw.txt | \
    sed 's/,/\t/g' | \
    tee 02-tabs.txt | \
    awk '{print $1, $3}' | \
    tee 03-columns.txt | \
    sort -k2 -n > final-output.txt

Each transformation stage is preserved, making it trivial to identify where data processing goes wrong.

Creating timestamped audit trails:

If you have the moreutils package installed (which provides the ts command for adding timestamps):

./critical-operation.sh | ts '[%Y-%m-%d %H:%M:%S]' | tee -a audit-trail.log

Every line of output gets a timestamp, and everything is logged while displaying in real-time.

Capturing both stdout and stderr:

command 2>&1 | tee complete-output.log

The 2>&1 redirects stderr to stdout before piping to tee, ensuring you capture both output streams.

Performance Considerations and Alternatives

While tee is generally lightweight, it can introduce latency in high-throughput scenarios. Each write operation must complete to both stdout and all specified files before the next chunk of data is processed.

Buffering can cause unexpected behavior with interactive programs. If output appears delayed, try stdbuf to modify buffering:

stdbuf -oL command | tee output.log

The -oL flag sets line-buffered output, ensuring each line appears immediately rather than waiting for the buffer to fill.

For complex session logging, consider the script command instead:

script -c "command" output.log

This captures everything, including terminal control sequences and interactive input, which tee doesn’t handle.

For simple cases where you don’t need real-time viewing, standard redirection with command grouping works:

{ command1; command2; command3; } > output.log 2>&1

However, this doesn’t show output as it happens—you must wait for completion.

The tee command remains the most elegant solution for the common case: watching command output while preserving it. It’s a small utility that punches well above its weight class, and mastering its patterns—especially the sudo tee idiom and process substitution techniques—significantly improves your command-line effectiveness.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.