In a bash script, how do you send every command in one section to a log file and then restore stdout to where it was before, and when is redirecting a `{ ...; }` block the better choice?
answer
- overwriting entry 1 loses the reference
- park a copy on a spare number
- restore before you close it
- a group restores itself
basics
~20 sSave the current stdout on a spare descriptor first: exec 3>&1, then exec 1>run.log, and restore afterwards with exec 1>&3 3>&-. Without the saved copy the original destination is unrecoverable. Redirecting a { ...; } group instead is safer because the shell restores stdout automatically when the group ends.
solid answer
~50 s`exec 1>run.log` moves the shell's own stdout, and once it moves, the old destination is gone unless you kept a copy — so the idiom is `exec 3>&1` to duplicate the current stdout onto a spare descriptor, then `exec 1>run.log`, then `exec 1>&3 3>&-` to duplicate it back and close the spare. Both redirections on that last `exec` are applied left to right, which is why restore-then-close works in one line. The alternative, and usually the better one, is to redirect a compound command: `{ step_one; step_two; } > run.log 2>&1`. The shell sets the descriptors up for the group and restores them when the group ends, so there is no saved copy to leak and no path where an early `return` or an error skips the restore. Reach for the `exec` form when the section is not a single lexical block — for example when the whole rest of the script should be logged, which is the common `exec > >(tee -a run.log) 2>&1` pattern.
code
bash · 16 lines#!/usr/bin/env bash
set -euo pipefail
# exec form: explicit save, move, restore
exec 3>&1
exec 1>/tmp/section.log
echo "this line is logged"
exec 1>&3 3>&-
echo "this line is back on the terminal"
# group form: the shell restores for you
{
echo "also logged"
echo "and so is this"
} >>/tmp/section.log 2>&1
echo "still on the terminal"go deeper
Know that { commands; } > file sends everything in the braces to that file, and that this is the simple way to log a whole section without touching the rest of the script.
Explain why a bare exec 1>file is one-way: it overwrites the shell's descriptor entry, so the previous destination is lost unless it was duplicated first. Give the save-and-restore pair in the right order.
Argue the tradeoff. Prefer the group form because the shell restores descriptors automatically and no early exit or set -e trip can skip a restore line; keep exec for whole-script redirection, and know the tee variant's flush caveat.
Set the house pattern for script output: whether scripts log for themselves at all or write to stdout and let the supervisor capture it. A supervisor-captured stream removes this entire class of restore bug from every script at once.
## Why a save is needed at all A redirection on a command is scoped to that command: bash sets the descriptors up, runs the program, and the shell's own table is untouched. `exec` with redirections and no command word is the exception — it edits the shell's table itself. That is what makes it powerful and what makes it dangerous: ```bash exec 1>run.log # the shell's stdout is now the file echo hi # goes to run.log # ...and there is no way back: the terminal that fd 1 pointed at is not # recorded anywhere the shell can reach. ``` A descriptor is an index into a table of open files. Overwriting entry 1 discards the only reference the shell had to the previous open file. Unless you first duplicated it somewhere, it is unrecoverable — you cannot "undo" a redirection, you can only restore from a copy. ## The save / redirect / restore idiom ```bash exec 3>&1 # fd 3 = a duplicate of the current stdout exec 1>run.log # fd 1 = the log file run_migrations # everything here writes to run.log seed_database exec 1>&3 3>&- # fd 1 = the saved copy, then close fd 3 ``` The last line is two redirections on one `exec`, applied left to right: first fd 1 is made a duplicate of fd 3, then fd 3 is closed with the `n>&-` form. Order matters — closing first would leave nothing to restore from. The same shape works for stderr with descriptor 4, or for both at once: ```bash exec 3>&1 4>&2 exec 1>run.log 2>&1 # ... exec 1>&3 2>&4 3>&- 4>&- ``` ## Why the block form is usually better ```bash { run_migrations seed_database } > run.log 2>&1 ``` The braces form a compound command; the redirection applies to the group as a unit, so every command inside inherits fd 1 and fd 2 pointing at the log — and when the group ends, the shell's own descriptors are back exactly as they were. There is nothing to remember to restore. That is not a style preference, it is an error-handling property. With the `exec` pair, any path that leaves the section early — an `exit` inside a function, `set -e` firing, a `return` from the enclosing function, a signal — skips the restore line, and the rest of the script keeps writing into the log file while the operator's terminal sits silent. The block form cannot get that wrong. The same applies to a loop or an `if`, both of which take a redirection on the closing keyword: ```bash while read -r host; do ping -c1 "$host" done < hosts.txt > ping.log 2>&1 ``` Here `< hosts.txt` feeds the loop's stdin once for the whole loop — the loop body's `read` consumes from it — and the output redirections apply to every iteration without reopening the file each time. ## When `exec` is the right tool anyway The block form needs the section to be one lexical unit. When it is not, `exec` wins: - **"Log everything from here on."** Near the top of a script, `exec > run.log 2>&1` redirects the entire remainder with one line, and no restore is intended. - **Tee to both places.** `exec > >(tee -a run.log) 2>&1` sends the rest of the script to the terminal *and* the file. The `>( )` is process substitution — the shell hands `tee` a `/dev/fd` path — and it is bash-only. - **Restoring around an unpredictable region**, such as sourcing a third-party file whose contents you cannot wrap. One caveat on the `tee` form: `tee` is a separate process, so its writes are not synchronised with the script's exit. A script that ends immediately after can lose the last lines unless it waits for the writer. That is a real cost of the pattern, and a reason to prefer plain `> run.log` when a live terminal copy is not needed. ## Diagnosing it after the fact If a running script has stopped printing, the descriptor table tells you why: `ls -l /proc/<pid>/fd` on Linux shows where 0, 1, 2 and any extras currently point. Seeing fd 1 pointing at a log file and fd 3 pointing at a pty is the fingerprint of exactly this idiom mid-flight. ## The answer to give "Duplicate stdout to a spare descriptor before moving it, restore and close it afterwards in one `exec`. But prefer redirecting a `{ ...; }` group, because the shell restores the descriptors for you and no early exit can skip that."
- What goes wrong if you write `exec 3>&- 1>&3` to finish the idiom?The redirections are applied left to right, so fd 3 is closed first and then fd 1 is told to duplicate a descriptor that no longer exists. Bash reports "Bad file descriptor" and stdout is left pointing at the log file. The restore must come before the close: `exec 1>&3 3>&-`.
- Why does `exec > >(tee -a run.log) 2>&1` sometimes lose the last few lines?`tee` runs as a separate process reading from a pipe. When the script exits, the shell does not wait for it, so anything still buffered in `tee` can be dropped and lines can arrive after the shell's exit. If you need the full log guaranteed, redirect straight to the file, or capture `tee`'s PID and `wait` for it before exiting.
- How would you redirect the input of a whole `while read` loop rather than one command inside it?Put the redirection on the loop's closing keyword: `while read -r line; do ...; done < input.txt`. The file is opened once for the entire loop, so `read` consumes successive lines from the same descriptor and offset. Redirecting inside the body instead would reopen the file every iteration and read line one forever.
- Does a redirection on a function definition or a function call behave differently?You can attach a redirection to a function *definition*, and it then applies every time the function runs — a way to give one function a permanent output destination. A redirection on the call site applies to that invocation only. Both are scoped to the function's execution and neither changes the shell's own descriptors, unlike `exec`.
saying these in an interview costs you the question
- Believing a redirection can simply be undone
- Closing the saved descriptor before restoring from it
- Assuming exec 1>file is scoped to the current function
- Leaving the restore on a path that set -e can skip
- Redirecting inside a read loop instead of on done