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.
Last updated: August 2026

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 prefer while read for 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:

  1. The shell runs the command in a subshell.
  2. Stdout is captured (trailing newlines are stripped from the overall substitution in typical use).
  3. Stderr still goes to the script’s stderr unless redirected.
  4. 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
GoalPattern
Need textvar=$(cmd) then use "$var"
Need only success/failif cmd; then or cmd && ...
Need bothcapture, 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)
  • for loop over glob
  • if/test guards
  • $(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

  1. Using backticks nested clumsily — switch to $().
  2. for x in $(find ...) on arbitrary names — breaks on spaces; use find -print0 + read -d '' only if you must; for exams, constrain to safe names or use find -exec.
  3. Forgetting quotes around "$var" after capture.
  4. Checking $? too late — another command already replaced it.
  5. Assuming capture includes stderr — it does not unless 2>&1.
  6. Parsing ls output — prefer globs, find, or stat.
  7. 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.

Test Your Knowledge

Which form is preferred in modern bash scripts for command substitution and nests cleanly?

A
B
C
D
Test Your Knowledge

After running out=$(rpm -q httpd 2>/dev/null), how should you determine whether the package query succeeded?

A
B
C
D
Test Your Knowledge

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?

A
B
C
D
Test Your Knowledge

What does the 2>/dev/null redirection inside kern=$(uname -r 2>/dev/null) affect?

A
B
C
D