Redirecting Output to Both a File and stdout in Bash
When you're debugging a script, running a build, or logging output from a long-running process, it's often useful to both see output live in the terminal and save it to a file for later. Bash doesn't do this by default, but there are reliable ways to make it work.
Let's walk through some methods to redirect output to both a file and stdout, with examples you can drop into real workflows.
Prerequisites
- Bash shell (
bash --version≥ 4.0) - Basic understanding of standard streams (
stdout,stderr) - You're working on a Unix-like system (Linux, macOS, WSL)
Using tee to Duplicate stdout to a File
The tee command reads from standard input and writes to both standard output and one or more files.
Example: Save test results while still seeing them
pytest tests/ | tee test-results.log
This runs your test suite, prints all output to your terminal, and also writes it to test-results.log.
If the file already exists, tee will overwrite it by default. Use -a to append instead:
pytest tests/ | tee -a test-results.log
This is useful when you're collecting logs across multiple runs.
Capturing stdout and stderr Together
By default, tee only captures stdout. If you also want stderr (useful for errors and warnings), you need to redirect it manually.
Example: Build logs with error output included
make 2>&1 | tee build.log
Explanation:
2>&1redirectsstderr(file descriptor 2) tostdout(1)- The combined output is piped to
tee, which logs and displays it
This helps when you're compiling software or running CI jobs and want a full picture of what happened.
Redirecting Output from Within a Script
Let's say you have a script that produces both standard output and error messages. You can use a function to handle logging consistently.
Example: Bash script with logged output
#!/bin/bash
logfile="deploy.log"
# Redirect all output from this block
{
echo "Starting deployment at $(date)"
./build.sh
./deploy.sh staging
echo "Deployment finished at $(date)"
} 2>&1 | tee -a "$logfile"
This setup:
- Logs everything (including errors)
- Appends to
deploy.log - Still prints to the console for visibility
If you wrap your operations in a logging block like this, you don't have to repeat redirections for each command.
Advanced: Redirect Output from Inside Functions
Sometimes you want specific functions or blocks to log independently. You can wrap the same pattern locally.
Example: Function-specific logging
log_build() {
{
echo "=== Build Start: $(date) ==="
make all
echo "=== Build End: $(date) ==="
} 2>&1 | tee -a build.log
}
This keeps log files clean and contextual, especially handy in CI/CD scripts.
What About script?
If you're logging an entire terminal session (not just a single command), check out the script command:
script -q -c "./run-heavy-task.sh" session.log
This records everything from the shell session into session.log, including prompts and interactive input.
A Note on Exit Codes
When piping into tee, you only get the exit status of the last command in the pipeline (tee). If you care about the actual command's exit code, capture it with this pattern:
my_command 2>&1 | tee output.log
exit_code=${PIPESTATUS[0]}
This ensures your script behaves correctly if something fails upstream.
Try This Next
- Integrate these patterns into your shell scripts
- Log deployment, build, or test outputs in real-time
- Use timestamps in filenames to archive runs (e.g.
log_$(date +%F_%T).log)
Found an issue?