Linux Basics: Save Pipeline Output Without Losing It with GNU tee
Sometimes you want to save a command’s output and continue processing it. GNU tee copies its input to named files while also forwarding it to standard output. Its position in a pipeline determines which version of the data you save.
These examples were tested on Linux with GNU Bash 5.2.37 and GNU coreutils 9.7. All log messages are invented fixtures. Use Bash rather than sh; the final example uses Bash’s pipeline-status array.
Prepare a scratch directory
You need Bash, GNU tee, a text editor, and the sort, cat, cmp, mktemp, rm, and rmdir utilities. Keep using the same terminal. Save each named source file here, then run it once in order. No administrator access or system-file changes are involved.
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 11. Save the raw stream and display a sorted view
Save this as tee-capture-stream.sh:
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
printf 'Sorted view:\n'
printf '%s\n' pear apple banana | tee raw-fruit.txt | sort
printf 'Captured original:\n'
cat -- raw-fruit.txtRun it:
bash tee-capture-stream.shOutput:
Sorted view:
apple
banana
pear
Captured original:
pear
apple
bananaThe saved file contains the producer’s original order because tee appears before sort. The terminal receives the sorted stream. Moving tee after sort would save the sorted version instead.
The labels printed by the separate printf commands do not enter that pipeline and are not saved in the file. This separation is useful when a human-readable progress message should not become part of a machine-readable dataset. The GNU tee manual describes the copying behavior and output-error options.
2. Append rather than replace
Save this as tee-append-log.sh:
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
printf 'job: started\n' > session.log
printf 'job: finished\n' | tee --append session.log > /dev/null
cat -- session.logRun it:
bash tee-append-log.shOutput:
job: started
job: finishedWithout --append, tee replaces an existing destination’s contents. Here it preserves the initial line and adds the second. Redirecting standard output to /dev/null suppresses the forwarded copy, so the script displays the complete file only once.
The first command deliberately resets this practice log. In a real logging script, decide separately when to create a new log and when to append. Appending is not log rotation and does not by itself coordinate multiple writers.
3. Keep two copies and preserve the producer’s failure
Save this as tee-capture-failure.sh:
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
run_job() {
printf 'job: started\n'
printf 'job: fixture error\n' >&2
return 7
}
if run_job 2>&1 | tee job.log job-copy.log; then
printf 'Unexpected success\n' >&2
exit 1
else
statuses=("${PIPESTATUS[@]}")
fi
printf 'Producer status: %s\n' "${statuses[0]}"
printf 'tee status: %s\n' "${statuses[1]}"
cmp -s job.log job-copy.log
printf 'Copies match: yes\n'Run it:
bash tee-capture-failure.shOutput:
job: started
job: fixture error
Producer status: 7
tee status: 0
Copies match: yesThe job intentionally fails with status 7. tee successfully copies its output to both files and the terminal, so its own status is 0. With pipefail enabled, the pipeline enters the failure branch despite that successful copy.
2>&1 sends the job’s standard error into the same pipe as its standard output. Immediately capturing PIPESTATUS preserves both command statuses before another command overwrites them. The script itself finishes successfully because this failure is expected and handled. It demonstrates the distinction between a job failing and its log being captured successfully. The Bash manual documents PIPESTATUS and pipefail.
Keep in mind
A normal pipe carries only standard output. Merge standard error deliberately, especially when the next stage expects structured data rather than diagnostic text. Separate processes can also buffer their output differently; combining streams does not promise a perfect real-time event timeline.
Do not send output through tee back into the file an upstream command is reading. Truncation and concurrent reading make that unsafe. Use a separate output path and verify it first.
By default, a broken output pipe can make GNU tee exit early. These examples consume the full stream. If a downstream consumer may stop early, review tee’s --output-error modes rather than assuming every file copy will finish. Check statuses when retaining a complete log matters, and avoid putting secrets into logs you intend to share.
Clean up
Remove only the practice files, then return to your original directory:
rm -- tee-capture-stream.sh tee-append-log.sh tee-capture-failure.sh \
raw-fruit.txt session.log job.log job-copy.log
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dirAcknowledgements
This post was created with assistance from AI.