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
commanddo 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?
Only the caret form ^(hostname)
Legacy sole reliance on piping every command into /dev/null
The $(hostname) dollar-parentheses form rather than nested backticks
Replacing all substitutions with Windows %VAR% expansion
After running out=$(rpm -q httpd 2>/dev/null), how should you determine whether the package query succeeded?
Assume success whenever $out is longer than 100 characters
Ignore status because substitution always forces exit code 0
Parse /etc/shadow for the package name instead
Inspect $? immediately after the assignment (or test rpm -q in an if) because the variable holds stdout text, not the exit code
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?
Because find cannot be used inside $() on RHEL 10
Because unquoted $(find ...) is split on whitespace, so names with spaces break, whereas line-oriented read handles each line of output more appropriately
Because read deletes files that contain spaces
Because for loops cannot run when SELinux is enforcing
What does the 2>/dev/null redirection inside kern=$(uname -r 2>/dev/null) affect?
It discards the inner command’s stderr so error messages are not mixed into the terminal, while stdout is still captured into kern
It prevents stdout from being captured into kern
It deletes /dev/null after the script exits
It forces the script to run only as UID 0
Sections you finish are checked off in the contents.