PracticeVideosBlogFlashcards

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

Only the caret form ^(hostname)

B

Legacy sole reliance on piping every command into /dev/null

C

The $(hostname) dollar-parentheses form rather than nested backticks

D

Replacing all substitutions with Windows %VAR% expansion

Test Your Knowledge

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

A

Assume success whenever $out is longer than 100 characters

B

Ignore status because substitution always forces exit code 0

C

Parse /etc/shadow for the package name instead

D

Inspect $? immediately after the assignment (or test rpm -q in an if) because the variable holds stdout text, not the exit code

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

Because find cannot be used inside $() on RHEL 10

B

Because unquoted $(find ...) is split on whitespace, so names with spaces break, whereas line-oriented read handles each line of output more appropriately

C

Because read deletes files that contain spaces

D

Because for loops cannot run when SELinux is enforcing

Test Your Knowledge

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

A

It discards the inner command’s stderr so error messages are not mixed into the terminal, while stdout is still captured into kern

B

It prevents stdout from being captured into kern

C

It deletes /dev/null after the script exits

D

It forces the script to run only as UID 0

Sections you finish are checked off in the contents.