tee — Stream Splitting & Script Capture
tee splits a stream — writing to both a file and stdout simultaneously. Essential for build log capture, script staging via heredoc, and the sudo redirect pattern.
Stream Splitting
tee reads stdin and writes to both stdout AND one or more files simultaneously. The name comes from a T-shaped pipe fitting — one input, two outputs.
Basic — write to file while continuing the pipeline
echo "hello world" | tee /tmp/tee-demo.txt | wc -c
# Output: 12
# /tmp/tee-demo.txt now contains: hello world
# wc sees the same data tee wrote to the file
Append mode (-a) — don’t overwrite
echo "line 1" | tee /tmp/log.txt > /dev/null
echo "line 2" | tee -a /tmp/log.txt > /dev/null
cat /tmp/log.txt
# Output:
# line 1
# line 2
# Without -a, the second tee would overwrite line 1
Multiple output files
echo "broadcast" | tee /tmp/a.txt /tmp/b.txt /tmp/c.txt > /dev/null
# All three files contain "broadcast"
# stdout is suppressed with > /dev/null
Capture stderr + stdout together
make 2>&1 | tee /tmp/build.log | grep -E 'WARN|ERROR'
# 2>&1 merges stderr into stdout
# tee captures EVERYTHING to build.log
# grep filters what you see on screen
# Full log available later: cat /tmp/build.log
Script Capture and Staging
The safe scripting workflow: write to /tmp/, inspect, execute, promote.
Write a script via heredoc
tee /tmp/safe-delete.sh << 'EOF'
#!/usr/bin/env bash
for f in tobe*.adoc; do
echo "Found: $f ($(wc -c < "$f") bytes)"
head -1 "$f"
read -p "Delete $f? [y/N] " reply
[[ "$reply" == "y" ]] && rm "$f" && echo "Deleted." || echo "Kept."
done
EOF
# tee writes the script AND shows it on screen so you can verify
# 'EOF' (single-quoted) prevents $f and $(wc) from expanding at write time
The full workflow: write → inspect → execute → promote
# 1. Write
tee /tmp/xref-fix.sh << 'EOF'
#!/usr/bin/env bash
grep -rlP 'xref:codex/cli/' --include='*.adoc' docs/ | \
xargs sed -i -e 's|xref:codex/cli/grep\.adoc|xref:codex/grep/index.adoc|g'
grep -rnP 'xref:codex/cli/' --include='*.adoc' docs/ # verify
EOF
# 2. Inspect
cat /tmp/xref-fix.sh
# 3. Execute
bash /tmp/xref-fix.sh
# 4. Promote (if it worked and you'll reuse it)
cp /tmp/xref-fix.sh ~/bin/xref-fix.sh && chmod +x ~/bin/xref-fix.sh
Why single-quoted 'EOF' matters
# Unquoted EOF — shell expands variables NOW (at write time)
tee /tmp/broken.sh << EOF
echo "User is $USER" # writes: echo "User is evan" — baked in, not portable
echo "PWD is $(pwd)" # writes: echo "PWD is /home/evan/..." — snapshot, not live
EOF
# Single-quoted 'EOF' — variables preserved for runtime
tee /tmp/correct.sh << 'EOF'
echo "User is $USER" # writes literally — expands when script RUNS
echo "PWD is $(pwd)" # writes literally — evaluates at runtime
EOF
Session capture — log everything you do
# Capture an entire terminal session to review later
script /tmp/session-$(date +%Y%m%d-%H%M).log
# ... do work ...
# exit to stop recording
# Or capture a single command's full output
bash -x /tmp/xref-fix.sh 2>&1 | tee /tmp/xref-fix-trace.log
# -x shows every command before execution (trace mode)
Write config files with sudo
# tee with sudo — write to root-owned paths without sudo echo
echo "nameserver 10.50.1.90" | sudo tee /etc/resolv.conf.d/lab.conf
# echo runs as you, tee runs as root — the correct sudo pattern
# NEVER: sudo echo "..." > /root/file (redirect runs as YOU, not root)
Gotchas
tee overwrites by default — use -a to append
# WRONG — second write destroys first
echo "line 1" | tee /tmp/log.txt
echo "line 2" | tee /tmp/log.txt
cat /tmp/log.txt # only "line 2"
# CORRECT — append mode
echo "line 1" | tee /tmp/log.txt
echo "line 2" | tee -a /tmp/log.txt
cat /tmp/log.txt # both lines
sudo redirect trap — tee is the fix
# WRONG — redirect runs as YOUR user, not root
sudo echo "data" > /etc/protected.conf
# Permission denied — the > is evaluated by your shell
# CORRECT — tee runs as root, receives data via pipe
echo "data" | sudo tee /etc/protected.conf > /dev/null
# echo runs as you, sudo tee writes as root
# > /dev/null suppresses the stdout echo if you don't need it
tee sees stdout only — stderr goes elsewhere
# WRONG — tee only captures stdout, errors go to terminal
make | tee /tmp/build.log
# Errors appear on screen but NOT in build.log
# CORRECT — merge stderr into stdout first
make 2>&1 | tee /tmp/build.log
# Now both streams go to tee → file AND screen
Suppress stdout when you only want the file
# tee always writes to stdout — redirect to /dev/null if unwanted
echo "quiet" | tee /tmp/data.txt > /dev/null
# File written, nothing on screen
See Also
-
Bash Streams — file descriptors, redirection, stderr handling
-
Bash Pipes — pipelines and process substitution
-
Safe Workflows — the full validate-before-act pattern