Files, redirection and here-documents
File descriptors, redirecting stdout and stderr, appending and truncating, here-documents and here-strings, process substitution, and reading files line by line.
File descriptors and redirection
cmd > out.txt 2> err.txt # stdout and stderr to separate files
cmd > both.txt 2>&1 # merge into one file (order matters)
cmd 2>&1 > both.txt # WRONG: stderr still points at the terminal
cmd &> both.txt # bash shorthand for the merge above
cmd >> out.txt 2>&1 # append instead of truncating
exec 3> "$logfile" # open descriptor 3 for writing
printf 'started
' >&3
exec 3>&- # close it again
exec >> "$logfile" 2>&1 # from here on, the whole script logs to the file
grep -q ERROR app.log 2>/dev/null # silence noise, keep the exit status
printf 'silent
' >/dev/null| Descriptor | Name | Typical destination |
|---|---|---|
| 0 | stdin | The terminal, or a file with < |
| 1 | stdout | Data a caller wants to parse |
| 2 | stderr | Diagnostics, progress and errors |
| 3–9 | Free for scripts | Extra files or locks you manage yourself |
- Order is evaluated left to right:
2>&1copies wherever stdout points at that moment. >truncates before the command starts. If that command is also reading the file, it will read an empty file.- Diagnostics belong on stderr so that
result=$(tool)captures data and the user still sees warnings.
Here-documents, here-strings and process substitution
# quoted delimiter: nothing expands, the text is copied literally
cat <<'CONFIG' > /etc/app.ini
[server]
host = ${HOST}
CONFIG
# unquoted delimiter: variables and command substitution expand
cat <<EOF
generated $(date -Is) on $(hostname)
EOF
# indented document (the dash allows leading TABs only)
cat <<-EOF
indented body
EOF
# here-string: feed one value to stdin, no echo needed
grep -c 'ERROR' <<< "$log_text"
read -r first rest <<< "alpha beta gamma"
# process substitution: a command's output appears as a readable file
diff <(sort new.txt) <(sort old.txt)
while IFS= read -r line; do
count=$((count + 1))
done < <(grep -h 'ERROR' ./*.log)- A here-document is the readable way to embed multi-line text: config files, SQL, JSON bodies and usage messages.
- Only
<<-strips indentation, and only tabs — if your editor inserts spaces the delimiter will not match. - Use process substitution rather than a pipe when the loop must set variables that survive afterwards, because a pipe runs the loop in a subshell.
Reading files line by line
while IFS= read -r line; do
printf '%s
' "$line"
done < "$file"
# tolerate a file that does not end with a newline
while IFS= read -r line || [[ -n $line ]]; do
printf '%s
' "$line"
done < "$file"
# fields, not lines
while IFS=: read -r user _ uid _; do
printf '%s %s
' "$user" "$uid"
done < /etc/passwd
# never do this with data that can contain spaces
for line in $(cat "$file"); do echo "$line"; doneIFS=stops leading and trailing whitespace being eaten;-rstops backslashes being interpreted.- Reading a file with
< "$file"lets the loop run in the current shell, so counters and accumulators keep their values. mapfile -t arr < "$file"is the fastest way to get every line when you do not need a loop at all.
⚠️
read strips leading whitespace unless IFS is cleared, and without -r a trailing backslash swallows the newline and merges two lines. Getting into the habit of writing while IFS= read -r line removes both bugs permanently.FAQ
How do I send both output and errors to a file and see them too?
Tee the merge:
cmd 2>&1 | tee run.log, or with process substitution keep the status: cmd > >(tee run.log) 2>&1.Why did my here-document expand a variable I wanted literal?
The delimiter was unquoted. Write
<<'EOF' to disable all expansion inside the block.Related
Text processing: grep, sed and awk in scripts Shells, startup files and running scripts
Last refreshed 2026-09-18.