RED HAT ENTERPRISE LINUX
Processing Command
Output in Scripts
Processing output of shell commands within a script
CIS126RH | RHEL System Administration 1
Mesa Community College
Scripts become truly powerful when they can interrogate the running system — capturing the current date for a filename, counting log entries, extracting a field from command output, or making decisions based on what a command returns. This module covers command substitution, arithmetic expansion, the tools used to extract and transform text output, and patterns for checking command success inside scripts. These skills are tested on the RHCSA exam.
Learning Objectives
-
Capture command output with command substitution —
Use
$(command)to assign command output to variables and embed it in strings -
Check and use command exit codes —
Test whether commands succeeded using
$?,if, and the&&and||operators -
Extract fields from command output —
Use
cut,awk,grep, andsedinside scripts to isolate specific data from command output -
Perform arithmetic on command output —
Use
$(( ))andbcto compute values derived from command output
Command Substitution: $( )
Command substitution runs a command and replaces the $() expression
with the command's stdout output. The trailing newline is automatically stripped.
# Assign command output to a variable
TODAY=$(date +%F)
echo "Today is: $TODAY"
Today is: 2026-05-25
# Embed command output directly in a string
echo "Hostname: $(hostname)"
echo "Kernel: $(uname -r)"
# Use substitution in a filename
ARCHIVE="/tmp/backup-$(date +%F-%H%M).tar.gz"
tar -czvf "$ARCHIVE" /etc/ssh
# Nest command substitutions
ROOTDEV=$(df / | awk 'NR==2{print $1}')
echo "Root device: $ROOTDEV"
$(command) is the modern preferred form. The older backtick form
`command` is equivalent but cannot be nested, is harder to read,
and confuses editors. Always use $( ) in new scripts.
Storing and Using Command Output
Assigning command output to a named variable makes it reusable and keeps scripts readable. The variable can then be used anywhere a string is valid.
#!/bin/bash
# Gather system information into variables
HOSTNAME=$(hostname -s)
IPADDR=$(hostname -I | awk '{print $1}')
OSVER=$(rpm -q --qf "%{VERSION}" redhat-release)
UPTIME=$(uptime -p)
USERS=$(who | wc -l)
DISK_ROOT=$(df -h / | awk 'NR==2{print $5}')
echo "=== System Report: $HOSTNAME ==="
echo "IP Address: $IPADDR"
echo "RHEL version: $OSVER"
echo "Uptime: $UPTIME"
echo "Users online: $USERS"
echo "Root disk: $DISK_ROOT used"
Gathering all values into variables at the top of the script makes the output section clean and easy to read. It also ensures all values are collected from the same point in time.
Checking Exit Codes Inside Scripts
Every command in a script exits with a code. Checking that code is how a script knows whether an operation succeeded before proceeding.
# Check $? immediately after the command
useradd -m student
if [ "$?" -ne 0 ]; then
echo "Error: useradd failed" >&2
exit 1
fi
# Cleaner: use the command directly in the if condition
if ! useradd -m student; then
echo "Error: useradd failed" >&2
exit 1
fi
# && and || for inline exit code checking
mkdir -p /srv/data || { echo "mkdir failed" >&2; exit 1; }
cp config.conf /srv/data/ && echo "Config copied"
The value of $? changes after every command runs. Capture it
immediately if you need to use it more than once:
STATUS=$?; echo "Exit: $STATUS"
set -e and set -u: Automatic Safety
These shell options change how bash responds to errors and unset variables — making scripts fail safely by default rather than continuing silently.
#!/bin/bash
set -e # exit immediately if any command exits non-zero
set -u # treat unset variables as errors
set -o pipefail # return non-zero if any command in a pipe fails
# Common: combine all three
set -euo pipefail
# With -e: script stops here if mkdir fails
mkdir -p /srv/data
# With -u: this line causes an error if DEST is not set
cp file.txt "$DEST"
# Override -e for one command expected to fail
grep optional-pattern file.txt || true
# The "|| true" always exits 0 so -e does not trigger
These three options together prevent the most common silent failures: commands that fail unnoticed, variables that are empty because they were never set, and pipelines where only the last command's exit code is checked.
Extracting Fields with cut
cut extracts specific fields or character positions from each line
of input — the fastest tool for fixed-delimiter files like /etc/passwd.
# Extract field 1 (username) from colon-delimited /etc/passwd
USERS=$(cut -d: -f1 /etc/passwd)
# Extract multiple fields — username and shell
cut -d: -f1,7 /etc/passwd
# Extract a specific field from command output
FREE_MEM=$(free -m | grep Mem | cut -f4 -d' ')
echo "Free memory: ${FREE_MEM} MB"
# Extract characters by position — first 8 characters
SHORT_DATE=$(date +%F | cut -c1-7) # 2026-05
# Get the username who owns a process
HTTPD_USER=$(ps -o user= -p $(pgrep httpd | head -1))
-d DELIM sets the field delimiter (default is tab).
-f N selects field N (1-indexed).
-c N-M selects characters N through M.
Extracting Fields with awk
awk splits each line into fields on whitespace by default and provides
a mini-language for filtering and transforming text. It handles irregular spacing
better than cut.
# Print the second field of df output (1K blocks)
df -h / | awk 'NR==2 {print $2}'
# Get disk usage percentage as a number
USAGE=$(df -h / | awk 'NR==2 {gsub(/%/,""); print $5}')
echo "Root disk: ${USAGE}% used"
# Filter lines and extract a field in one step
ROOT_UID=$(awk -F: '$1=="root" {print $3}' /etc/passwd)
echo "root UID: $ROOT_UID"
# Sum the second column
TOTAL=$(df -m | awk 'NR>1 {sum += $2} END {print sum}')
echo "Total disk: ${TOTAL} MB"
NR is the current line number (1-indexed).
NF is the number of fields on the current line.
NR==2 processes only the second line.
$NF refers to the last field on the line.
Filtering with grep Inside Scripts
grep inside a script serves two purposes: filtering lines of output
to pass to the next stage, and testing whether a pattern exists (using the exit code).
# Filter command output before extracting a field
SSHD_PORT=$(grep -E '^Port ' /etc/ssh/sshd_config | awk '{print $2}')
SSHD_PORT="${SSHD_PORT:-22}" # default 22 if not configured
echo "sshd listening on port: $SSHD_PORT"
# Test whether a package is installed
if rpm -q httpd &> /dev/null; then
echo "httpd is installed"
fi
# Count lines matching a pattern
FAIL_COUNT=$(grep -c 'Failed password' /var/log/secure)
echo "Failed login attempts: $FAIL_COUNT"
# Extract matching text from output
KERNEL=$(uname -r | grep -oE '[0-9]+\.[0-9]+' | head -1)
Transforming Output with sed
sed — the stream editor — modifies text as it flows through a pipeline.
Inside scripts it is used to clean up, reformat, or extract specific lines from
command output.
# Remove comment lines from config output
ACTIVE_SETTINGS=$(grep -vE '^(#|$)' /etc/ssh/sshd_config)
# Strip leading whitespace from a value
VERSION=$(cat /etc/redhat-release | sed 's/.*release //' | sed 's/ .*//')
echo "RHEL version: $VERSION"
# Extract just the number from " 9 packages" output
PKG_COUNT=$(dnf list updates 2>/dev/null | wc -l)
# Replace all colons with commas in a line
CSV_LINE=$(grep '^root:' /etc/passwd | sed 's/:/,/g')
# Extract the nth line of output
SECOND_LINE=$(df -h | sed -n '2p')
s/old/new/ — substitute first occurrence per line
s/old/new/g — substitute all occurrences
-n 'Np' — print only line N
/pattern/d — delete matching lines
Arithmetic Expansion: $(( ))
The $(( )) form evaluates integer arithmetic and returns the result.
Variables do not need a dollar sign inside the expression.
# Basic arithmetic with captured values
TOTAL=$(df -m / | awk 'NR==2{print $2}')
USED=$(df -m / | awk 'NR==2{print $3}')
FREE=$(( TOTAL - USED ))
echo "Free space: ${FREE} MB"
# Calculate a percentage
PCT=$(( USED * 100 / TOTAL ))
echo "Used: ${PCT}%"
# Use in a comparison
if (( PCT > 90 )); then
echo "WARNING: disk over 90% full"
fi
# Count iterations
COUNT=0
for USER in $(cut -d: -f1 /etc/passwd); do
(( COUNT++ ))
done
echo "Total users: $COUNT"
bc for Floating-Point Arithmetic
Bash integer arithmetic truncates decimals. When precise floating-point results
are needed, pipe the expression to bc.
# bc evaluates expressions from stdin
RESULT=$(echo "7 / 2" | bc)
echo "$RESULT"
3 # integer division — same as bash
# Use scale= to set decimal places
RESULT=$(echo "scale=2; 7 / 2" | bc)
echo "$RESULT"
3.50
# Calculate percentage with two decimal places
USED=45231
TOTAL=51200
PCT=$(echo "scale=1; $USED * 100 / $TOTAL" | bc)
echo "Usage: ${PCT}%"
Usage: 88.3%
# Use bc for comparison when decimals are involved
if (( $(echo "$PCT > 85" | bc -l) )); then
echo "High disk usage"
fi
Process Substitution: <( )
Process substitution makes the output of a command available as if it were a file. This lets you compare or loop over command output in places that expect a filename.
# Compare the output of two commands without temp files
diff <(ls /etc/ssh) <(ls /etc/ssl)
# while read with a command — keeps variables in scope
while IFS= read -r LINE; do
echo "Processing: $LINE"
done < <(grep -v nologin /etc/passwd)
# Sort and compare two lists for differences
DIFF=$(diff \
<(rpm -qa | sort) \
<(cat expected-packages.txt | sort))
Using while read; done < <(command) keeps the loop in the current
shell — variables set inside the loop are visible after done. Using
command | while read; done runs the loop in a subshell — variables are
lost. This is why process substitution is preferred for while-read loops that need
to accumulate values.
A Complete Data-Processing Script
#!/bin/bash
set -euo pipefail
# disk-check.sh — warn if any filesystem exceeds a threshold
THRESHOLD="${1:-80}" # default: warn at 80%
WARNINGS=0
while IFS= read -r LINE; do
PCT=$(echo "$LINE" | awk '{gsub(/%/,""); print $5}')
MOUNT=$(echo "$LINE" | awk '{print $6}')
if (( PCT >= THRESHOLD )); then
echo "WARNING: $MOUNT is ${PCT}% full"
(( WARNINGS++ ))
fi
done < <(df -h | tail -n +2)
if (( WARNINGS == 0 )); then
echo "All filesystems below ${THRESHOLD}%"
fi
exit $(( WARNINGS > 0 ? 1 : 0 ))
Common Patterns Quick Reference
| Task | Pattern |
|---|---|
| Capture command output | VAR=$(command) |
| Use output in a string | echo "Date: $(date +%F)" |
| Check command succeeded | if command; then ... fi |
| Exit on any failure | set -euo pipefail at top of script |
| Extract column N (whitespace separated) | awk '{print $N}' |
| Extract field N (delimiter separated) | cut -dDELIM -fN |
| Extract field from specific line | awk 'NR==LINE {print $N}' |
| Count matching lines | grep -c 'pattern' file |
| Integer arithmetic on output | RESULT=$(( VAR1 + VAR2 )) |
| Floating-point arithmetic | RESULT=$(echo "scale=2; A/B" | bc) |
| Loop over command output | while IFS= read -r L; do ... done < <(cmd) |
| Test exit code explicitly | STATUS=$?; [ "$STATUS" -eq 0 ] |
Common Mistakes
| Mistake | What goes wrong | Correct approach |
|---|---|---|
Not quoting $VAR when it holds multi-word output |
Word splitting breaks the value — commands receive too many arguments | Always quote: "$VAR" |
Reading $? after another command has run |
$? holds the wrong command's exit code |
Check $? immediately or use if command; then |
| Using integer arithmetic on floating-point values from commands | Bash truncates silently — 3.7 becomes 3 |
Use bc for decimal arithmetic |
| Setting variables inside a piped while loop | Variables are in a subshell — lost after done |
Use while read; done < <(command) instead |
Using cut -d' ' on multi-space-separated output |
Multiple spaces count as multiple empty fields — wrong field numbers | Use awk '{print $N}' for whitespace-separated output |
Forgetting set -e — running with failed intermediate commands |
Script continues after a command fails — produces wrong results silently | Add set -euo pipefail at the top of every script |
Knowledge Check
Answer these before moving to the next slide.
- Write the command substitution that captures the current hostname (short form)
into a variable named
HOST. - Write a script line that runs
useradd -m alice, and if it fails, prints an error to stderr and exits with code 1. - The output of
df -h /has headers on line 1. Write the awk command to extract the percentage used (field 5) from line 2, with the percent sign removed. - The variable
USEDis 3750 andTOTALis 5000. Write the bash arithmetic expression that calculates the percentage used as an integer and stores it inPCT. - Why should you use
while read; done < <(command)instead ofcommand | while read; donewhen you need to use a counter after the loop? - What does
set -euo pipefaildo, and why should it appear at the top of every production script?
Knowledge Check — Answers
HOST=$(hostname -s)-
Also accepted:if ! useradd -m alice; then echo "Error: useradd failed" >&2 exit 1 fiuseradd -m alice || { echo "Error" >&2; exit 1; } df -h / | awk 'NR==2 {gsub(/%/,""); print $5}'PCT=$(( USED * 100 / TOTAL ))
This gives integer division: 3750 * 100 / 5000 = 75.- The pipe form runs the while loop in a subshell — any variables
set inside the loop are lost when the loop ends. The process substitution form
< <(command)keeps the while loop in the current shell, so a counter or accumulated results remain accessible afterdone. set -eexits immediately if any command exits non-zero.set -utreats unset variables as errors.set -o pipefailmakes a pipeline fail if any command in it fails. Together they prevent the three most common silent failures in bash scripts: unnoticed command errors, typo'd variable names, and failed pipeline stages.
Key Takeaways
-
Command substitution
$(command)captures stdout into a variable. The trailing newline is stripped automatically. Quote the variable:"$VAR". Nest substitutions for multi-step transformations. Always use$( ), not backticks. -
Check exit codes explicitly — do not assume commands succeed.
Use
if command; thendirectly, orcommand || { echo "Error"; exit 1; }for inline guards. Addset -euo pipefailto catch unchecked failures automatically. -
Use the right extraction tool for the output format.
awk '{print $N}'for whitespace-separated output.cut -dDELIM -fNfor fixed-delimiter files.grep -cto count.sed 's/old/new/'to transform. -
Use
$(( ))for integer arithmetic andbcfor decimals. Usewhile read; done < <(command)instead of a pipe so variables set inside the loop are visible afterdone.
Graded Lab
- Write a script with
set -euo pipefailthat captures hostname, kernel version, current user, and uptime into named variables, then prints a formatted system summary report. - Write a script that captures the percentage used for the root filesystem
using
dfandawk, then exits with code 1 and a warning message if usage is over 80%. - Write a script that reads
/etc/passwdline by line using process substitution, counts the total number of accounts, and counts only those with/bin/bashas their shell. Print both counts after the loop. - Write a script that runs
useradd -m testuser, captures whether it succeeded or failed, and prints an appropriate message either way. Run it twice to test both code paths. - Write a script that calculates the percentage of failed SSH login attempts from
/var/log/secure: capture the total line count and the count of "Failed password" lines, then usebcto calculate the percentage with one decimal place.
"Processing output of shell commands within a script." Exam script tasks require capturing command output, testing its value, and making decisions. Command substitution, awk field extraction, and exit code checking appear in nearly every script task.