7.4 Process Shell Command Output in Scripts
Key Takeaways
- Command substitution $(command) captures stdout into a string for variables, tests, and loops; legacy backticks `command` do the same but nest poorly—prefer $().
- Combine substitution with if/test, for/while, and positional parameters to build complete simple EX200 scripts.
- Watch word-splitting: quote "$var" after capture; use while read for multi-line output instead of unquoted for x in $(cmd).
- Separate stdout from stderr: 2>/dev/null or 2>&1 when appropriate; check exit status of commands with $? or if cmd directly.
- Exam scripts should capture package queries, path checks, counts, and filtered lists, then act—leaving results and exit codes the grader can verify.
7.4 Process Shell Command Output in Scripts
Quick Answer: Capture a command’s stdout with
$(command)(preferred) or legacy`command`, store it in a variable or use it inline, then branch or loop on that data. Always consider exit status separately from text output, quote expansions, and preferwhile readfor multi-line results.
The missing piece of simple scripts
You can write if and for without talking to other programs, but EX200 tasks expect scripts that use the system:
- Is package X installed? (
rpm -q) - How many lines? (
wc -l) - What is today’s date stamp? (
date +%F) - Which files match? (
find,ls,grep -l)
Command substitution is how bash turns command output into script data.
$(...) modern form
HOST=$(hostname)
NOW=$(date +%F)
COUNT=$(wc -l < /etc/passwd)
echo "host=$HOST date=$NOW users_file_lines=$COUNT"
Rules:
- The shell runs the command in a subshell.
- Stdout is captured (trailing newlines are stripped from the overall substitution in typical use).
- Stderr still goes to the script’s stderr unless redirected.
- The substitution’s text is inserted on the command line—quote it when it must stay one word.
FILE=$(ls /etc/hosts) # fine for single path without spaces
echo "file is $FILE"
# Quote when used as a single argument later:
TARGET="$(pwd)/data"
mkdir -p "$TARGET"
Inline use
if [ "$(id -u)" -eq 0 ]; then
echo "running as root"
else
echo "not root" >&2
exit 1
fi
for pkg in $(rpm -qa 'kernel*'); do
echo "kernel package: $pkg"
done
Package NVRs usually lack spaces, so this for is acceptable. For arbitrary filenames, do not use unquoted $(find ...).
Backticks: legacy form
HOST=`hostname`
Backticks work on RHEL bash but:
- Nesting requires ugly escaping:
`cmd \`inner\`` $()nests cleanly:$(cmd $(inner))- Easy to misread with quotes
Prefer $() in all new EX200 scripts. Recognize backticks when reading older examples.
Exit status vs captured output
Substitution captures text; it does not put the exit code into the variable. Check status carefully:
#!/bin/bash
OUT=$(rpm -q httpd 2>/dev/null)
status=$?
if [ "$status" -eq 0 ]; then
echo "installed: $OUT"
else
echo "httpd not installed"
exit 1
fi
Alternatively, test the command in if without caring about the text:
if rpm -q httpd &>/dev/null; then
echo "httpd present"
fi
| Goal | Pattern |
|---|---|
| Need text | var=$(cmd) then use "$var" |
| Need only success/fail | if cmd; then or cmd && ... |
| Need both | capture, then inspect $? immediately |
Trap: var=$(false) sets var empty-ish and $? is non-zero—check $? right away before running other commands that overwrite it.
Redirecting inside substitution
# Discard errors from the inner command
USERS=$(getent passwd 2>/dev/null | wc -l)
# Merge stderr into capture (usually avoid unless intentional)
MIXED=$(ls /no/such 2>&1)
Silencing stderr can hide useful diagnostics; on exams, hide noise when grepping optional data, but show errors when failure must be obvious.
Trimming and normalizing output
# Avoid accidental newlines/spaces issues
NAME=$(echo -n "$NAME" | tr -d '\r')
# First field only
KERN=$(uname -r)
# Lowercase (bash)
MODE=$(echo "$1" | tr '[:upper:]' '[:lower:]')
wc -l < file avoids embedding the filename in wc output (which wc -l file would include).
Multi-line output: do it right
Bad (word-splitting)
for line in $(cat /root/list.txt); do # splits on any IFS whitespace
echo "$line"
done
Good (line-oriented)
while IFS= read -r line; do
[ -z "$line" ] && continue
echo "got $line"
done < /root/list.txt
From a command pipeline
rpm -qa | sort | while IFS= read -r pkg; do
case "$pkg" in
bash-*|coreutils-*) echo "core: $pkg" ;;
esac
done
mapfile / readarray (bash convenience)
mapfile -t lines < <(systemctl list-unit-files --type=service --no-pager --no-legend)
for line in "${lines[@]}"; do
echo "$line"
done
Process substitution < <(cmd) feeds mapfile without a subshell pipeline issue. Nice to know; not mandatory if while read is clearer under pressure.
Feeding substitution into tests and arithmetic
LOAD=$(cut -d. -f1 /proc/loadavg)
if [ "$LOAD" -ge 8 ]; then
echo "high load $LOAD" >&2
exit 1
fi
FILES=$(find /var/log -type f -name '*.log' 2>/dev/null | wc -l)
echo "log files: $FILES"
Ensure numeric variables really are numeric before -ge—empty strings break integer tests.
if [ -z "$LOAD" ]; then
echo "could not read load" >&2
exit 1
fi
Complete exam-style scripts
A. Report free space for a path argument
#!/bin/bash
if [ $# -ne 1 ]; then
echo "Usage: $0 mountpoint" >&2
exit 2
fi
if [ ! -d "$1" ]; then
echo "Not a directory: $1" >&2
exit 1
fi
# df output processing: print available KB (field varies; use df -P for portability)
avail=$(df -P "$1" | awk 'NR==2 {print $4}')
echo "$1 available_kb=$avail"
B. Ensure a package is installed; capture NVR
#!/bin/bash
PKG="${1:-httpd}"
if rpm -q "$PKG" &>/dev/null; then
nvr=$(rpm -q "$PKG")
echo "OK $nvr"
exit 0
else
echo "MISSING $PKG" >&2
exit 1
fi
C. Build a timestamped backup name from command output
#!/bin/bash
SRC="$1"
if [ ! -f "$SRC" ]; then
echo "Usage: $0 existing-file" >&2
exit 2
fi
ts=$(date +%Y%m%d%H%M%S)
dest="${SRC}.bak.${ts}"
cp -a "$SRC" "$dest" || exit 1
echo "backed up to $dest"
D. Loop over users with UID ≥ 1000 from command output
#!/bin/bash
while IFS=: read -r name _ uid _rest; do
if [ "$uid" -ge 1000 ] 2>/dev/null; then
echo "human-ish user: $name uid=$uid"
fi
done < /etc/passwd
Here the “command output” is effectively the file content; the same pattern applies to getent passwd.
getent passwd | while IFS=: read -r name _ uid _; do
[ "$uid" -ge 1000 ] 2>/dev/null && echo "$name"
done
E. Conditional on grep match count
#!/bin/bash
file="/var/log/messages"
pattern="${1:-error}"
if [ ! -r "$file" ]; then
echo "cannot read $file" >&2
exit 1
fi
matches=$(grep -c -i -- "$pattern" "$file" || true)
echo "matches=$matches"
if [ "$matches" -gt 0 ]; then
exit 0
else
exit 1
fi
|| true keeps set -e (if used) from aborting when grep finds nothing (exit 1). Many exam scripts omit set -e; still know grep’s exit codes.
set -e and substitutions (awareness)
set -e
var=$(false) # may exit the script depending on context/version/options
For EX200, explicit if checks are clearer than relying on set -e edge cases. If you enable set -e, test thoroughly with bash -x.
Putting all four skills together
A single graded script often combines this whole chapter:
#!/bin/bash
# inventory.sh DIR
# Lists regular files, counts them, fails if DIR missing
if [ $# -ne 1 ]; then
echo "Usage: $0 directory" >&2
exit 2
fi
dir="$1"
if [ ! -d "$dir" ]; then
echo "Not a directory: $dir" >&2
exit 1
fi
count=0
for f in "$dir"/*; do
[ -f "$f" ] || continue
size=$(stat -c%s "$f" 2>/dev/null || echo 0)
echo "$f $size"
count=$((count + 1))
done
echo "TOTAL_FILES=$count"
if [ "$count" -eq 0 ]; then
exit 1
fi
exit 0
Features used:
- Shebang + (you still
chmod +x) - Input validation (
$#,$1) forloop over globif/testguards$(stat ...)command substitution- Meaningful exit codes
Debugging command substitution
bash -x ./script.sh /data
# Temporarily:
result=$(find /data -type f | wc -l)
printf 'DEBUG result=%q status=%s\n' "$result" "$?" >&2
If result is empty, check whether the command wrote only to stderr or failed.
Common traps
- Using backticks nested clumsily — switch to
$(). for x in $(find ...)on arbitrary names — breaks on spaces; usefind -print0+read -d ''only if you must; for exams, constrain to safe names or usefind -exec.- Forgetting quotes around
"$var"after capture. - Checking
$?too late — another command already replaced it. - Assuming capture includes stderr — it does not unless
2>&1. - Parsing
lsoutput — prefer globs,find, orstat. - Leaving the script non-executable after a perfect body.
Section checkpoint
You should capture stdout with $(...), recognize backticks, quote results, combine output with if/for/while and positional parameters, handle multi-line data with read, check exit status separately when needed, and deliver complete simple scripts that process real RHEL command output for EX200 tasks.
Which form is preferred in modern bash scripts for command substitution and nests cleanly?
After running out=$(rpm -q httpd 2>/dev/null), how should you determine whether the package query succeeded?
Why is while IFS= read -r line; do ...; done < <(find /data -type f) safer than for line in $(find /data -type f); do for arbitrary file names?
What does the 2>/dev/null redirection inside kern=$(uname -r 2>/dev/null) affect?