Linux Basic — Capturing Output
Capturing Output
Before we can write sed and awk into a script, we have to capture their output. That sounds trivial — and most of the time it is — but there are three gotchas that bite every shell programmer: trailing newlines, exit codes, and multi-line results.
$(...) — The Modern Form
date_str=$(date +%F)
echo "$date_str"
# 2026-06-17
hostname=$(hostname)
files_count=$(ls | wc -l)
Always prefer $(...) over backticks:
| Feature | `cmd` |
$(cmd) |
|---|---|---|
| Nesting | painful — must escape ` |
trivial: $(a $(b)) |
| Readability | poor | good |
| Quoting inside | error-prone | normal |
Use backticks only when forced by an ancient /bin/sh environment.
The Trailing-Newline Trap
Bash strips all trailing newlines from a command substitution result.
greet=$(printf "hello\n\n\n")
echo "[$greet]"
# [hello] ← three newlines vanished
Usually that’s exactly what you want. But it’s a problem when the output legitimately ends in a newline that matters:
# Capture a file with a trailing blank line
text=$(cat /tmp/note.txt)
echo "$text" > /tmp/copy.txt
# Trailing blank line is lost in /tmp/copy.txt
Workaround: append a sentinel before capturing, strip it after:
text=$(cat /tmp/note.txt; printf x)
text=${text%x}
For binary data, don’t use command substitution at all — pipe directly or use mapfile.
Capturing Output AND Exit Code
output=$(some_cmd 2>&1)
exit_code=$?
if (( exit_code != 0 )); then
echo "command failed: $output" >&2
exit "$exit_code"
fi
Note the order: $? is set by $(...), so grab it on the very next line. Anything in between (like an echo) overwrites it.
Common combined form:
if output=$(some_cmd 2>&1); then
echo "OK: $output"
else
echo "FAIL: $output" >&2
fi
Capturing Multi-Line Output
Naive:
files=$(ls)
echo "$files"
# a.txt
# b.txt
# c.txt
That’s a single string with embedded newlines — fine for echo, but to iterate safely (handles spaces, special chars), use mapfile:
mapfile -t files < <(ls)
for f in "${files[@]}"; do
echo "[$f]"
done
<(...) is process substitution — covered in detail in Process Substitution (Chapter 11). For now, treat it as “use this command’s output as if it were a file.”
For paths specifically, prefer NUL-delimited:
mapfile -d '' -t files < <(find . -type f -print0)
echo "Found ${#files[@]} files"
Why for f in $(ls) Is Wrong
You’ll see this everywhere. It’s broken:
# ❌ Splits on whitespace — breaks on filenames with spaces
for f in $(ls); do
echo "[$f]"
done
If a filename is "my doc.txt", you’ll iterate over [my] and [doc.txt] separately. The correct form:
# ✅ Don't parse ls; use a glob
for f in *; do
[[ -e "$f" ]] || continue # in case the glob doesn't match
echo "[$f]"
done
Or for a recursive case:
while IFS= read -r -d '' f; do
echo "[$f]"
done < <(find . -type f -print0)
Read One Line into a Variable
When the output is a single line (or you only want the first):
ip=$(hostname -I | awk '{print $1}')
echo "$ip"
# 192.168.1.42
For purely “first line” use:
read -r first_line < <(generate_output)
For “first word only”:
read -r first _ < <(echo "alice bob carol")
echo "$first"
# alice
Performance Note — Avoid Substitution in Tight Loops
Every $(...) forks a subshell. That’s free for one-shot scripts, expensive in loops:
# ❌ 10,000 forks
for i in {1..10000}; do
val=$(date +%s%N)
done
# real 0m12.4s
# ✅ Use printf -v (no fork) when possible
printf -v ts '%(%s)T' -1 # Bash 4.2+ built-in time formatter
For sed/awk inside a loop, see if you can move the loop inside awk instead — covered in awk in Scripts.
Capturing into a Variable AND Streaming to Stdout
Use tee:
output=$(some_cmd | tee /dev/tty)
/dev/tty is the user’s terminal — handy when you want them to see progress and you also want to keep the data.
A Real Example — Wrap a Command Safely
run() {
local name=$1
shift
local output exit_code
if ! output=$("$@" 2>&1); then
exit_code=$?
printf '[FAIL] %s (exit %d)\n%s\n' "$name" "$exit_code" "$output" >&2
return "$exit_code"
fi
printf '[OK] %s\n' "$name"
[[ -n "$output" ]] && printf ' %s\n' "$output"
}
run "Ping" ping -c1 example.com
run "Disk free" df -h /
run "Bad cmd" false
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
`cmd` (backticks) |
$(cmd) |
nesting and readability |
for f in $(ls) |
for f in * or mapfile … < <(find … -print0) |
breaks on whitespace |
output=$(cmd) then echo … then check $? |
capture both in one line | $? is overwritten by echo |
text=$(cat file) when trailing newline matters |
use mapfile or the x sentinel trick |
Bash strips trailing \n |
var=$(cmd) in a 10k-iteration loop |
rewrite to a single awk/sed |
fork overhead dominates |
Next → sed in Scripts