RED HAT ENTERPRISE LINUX
Looping Constructs
in Bash Scripts
Use looping constructs (for, etc.) to process file, command line input
CIS126RH | RHEL System Administration 1
Mesa Community College
When an administration task must be repeated across dozens of users, files, or servers,
doing it manually is error-prone and slow. Loops let a script apply the same action
to every item in a list — reading from a file, processing command output, or iterating
over command line arguments. This module covers the for, while,
and until loops with real RHEL administration examples. These constructs
are tested on the RHCSA exam.
Learning Objectives
-
Use for loops to iterate over lists —
Process a fixed list of values, a glob pattern of files, command substitution
output, and command line arguments with
for -
Use while loops to process input —
Read a file line by line and loop while a condition is true using
while readandwhile [ ] -
Use until loops —
Loop until a condition becomes true — the logical complement of
while -
Control loop flow with break and continue —
Exit a loop early with
breakand skip iterations withcontinue
The for Loop: Basic Structure
The for loop iterates over a list, assigning each item to a variable
in turn and executing the loop body once per item.
# Syntax
for VARIABLE in LIST; do
# commands using $VARIABLE
done
# Iterate over a fixed list of words
for COLOR in red green blue; do
echo "Color: $COLOR"
done
Color: red
Color: green
Color: blue
# Iterate over a list of servers
for HOST in servera serverb serverc; do
echo "Checking $HOST..."
ping -c1 -W2 "$HOST" &> /dev/null && echo " UP" || echo " DOWN"
done
Every for, while, and until loop must end
with done. Missing done is the most common loop syntax
error — bash waits for more input or reports an error on the next line.
for Loop: Iterating Over Files
Glob patterns in the list let a for loop process every matching file in a directory.
# Process every .conf file in /etc/ssh/
for FILE in /etc/ssh/*.conf; do
echo "Config file: $FILE"
ls -lh "$FILE"
done
# Back up every config file before a change
for FILE in /etc/ssh/*.conf; do
cp "$FILE" "${FILE}.bak"
echo "Backed up $FILE"
done
# Set permissions on every script in a directory
for SCRIPT in /usr/local/bin/*.sh; do
chmod 755 "$SCRIPT"
echo "Set 755 on $SCRIPT"
done
Quoting "$FILE" ensures the loop handles filenames that contain
spaces or special characters without errors. This is a professional habit that
prevents hard-to-debug failures on real systems.
for Loop: Command Substitution
Using $(command) in the list position feeds the output of a command
as the items to iterate over — one word per line becomes one iteration.
# Iterate over users who have login shells
for USER in $(grep -v nologin /etc/passwd | cut -d: -f1); do
echo "Login user: $USER"
done
# Check disk usage for each filesystem
for FS in $(df -h | awk '{print $6}' | tail -n +2); do
echo "Filesystem: $FS"
done
# Restart a list of services from a file
for SVC in $(cat services.txt); do
systemctl restart "$SVC"
echo "Restarted $SVC"
done
When $(command) produces output with spaces, bash splits it into
separate words. This is useful for lists but breaks for filenames with spaces.
For filenames, use while read or find with -print0
instead of command substitution in a for loop.
for Loop: Command Line Arguments
When the list is omitted from a for loop, bash automatically iterates over
the script's positional parameters — $1, $2, etc.
#!/bin/bash
# Usage: ./check-users.sh user1 user2 user3
# $@ expands to all arguments as separate quoted words
for USERNAME in "$@"; do
if id "$USERNAME" &> /dev/null; then
echo "$USERNAME: exists"
else
echo "$USERNAME: NOT FOUND"
fi
done
# Omitting "in LIST" iterates over $@ automatically
for USERNAME; do
echo "Processing: $USERNAME"
done
| Variable | Expands to |
|---|---|
$@ | All positional parameters as separate quoted words — preserves spaces in arguments |
$* | All positional parameters as a single string — spaces in arguments are lost |
$# | The count of positional parameters |
$1, $2… | Individual positional parameters |
C-Style for Loop
Bash supports a C-style numeric for loop using the double-parentheses arithmetic form — useful when you need to count, index into arrays, or repeat something a specific number of times.
# Count from 1 to 5
for (( i=1; i<=5; i++ )); do
echo "Iteration $i"
done
# Create ten numbered directories
for (( n=1; n<=10; n++ )); do
mkdir -p "/srv/project/host${n}"
done
# Equivalent using brace expansion — often cleaner
for n in {1..10}; do
mkdir -p "/srv/project/host${n}"
done
# Brace expansion with step size
for n in {0..100..10}; do
echo "$n%"
done
Use brace expansion {1..10} when the range is fixed and known at write
time. Use the C-style form (( i=1; i<=N; i++ )) when the limit
is a variable: (( i=1; i<=$COUNT; i++ )).
The while Loop
The while loop executes its body as long as a condition remains true
— that is, as long as the test command exits with 0.
# Syntax
while COMMAND; do
# body runs as long as COMMAND exits 0
done
# Count down from 5
COUNT=5
while [ "$COUNT" -gt 0 ]; do
echo "$COUNT..."
(( COUNT-- ))
done
echo "Done"
# Wait for a service to become active
while ! systemctl is-active --quiet httpd; do
echo "Waiting for httpd..."
sleep 2
done
echo "httpd is up"
If the condition never becomes false, the loop runs forever. Always include a mechanism to make the condition false: a counter that decrements, a file that eventually appears, or a maximum iteration limit with a counter guard.
while read: Processing Files Line by Line
The while read pattern is the standard way to read a file one line at
a time — safer than command substitution for lines that may contain spaces.
# Read /etc/passwd line by line
while IFS= read -r LINE; do
echo "$LINE"
done < /etc/passwd
# Extract the username field from each line
while IFS=: read -r USER PASS UID GID GECOS HOME SHELL; do
echo "User: $USER Shell: $SHELL"
done < /etc/passwd
# Process a list of hostnames from a file
while IFS= read -r HOST; do
ping -c1 -W1 "$HOST" &> /dev/null \
&& echo "$HOST UP" || echo "$HOST DOWN"
done < hosts.txt
IFS= (empty) prevents leading and trailing whitespace from being stripped.
-r (raw) prevents backslash sequences from being interpreted.
Together they preserve each line exactly as it appears in the file.
while read: Piped Input
A while read loop can also receive input from a pipe — processing each line of a command's output.
# Process each running process from ps output
ps aux | tail -n +2 | while IFS= read -r LINE; do
PROC=$(echo "$LINE" | awk '{print $11}')
echo "Process: $PROC"
done
# Find large files and report them
find /var/log -size +10M | while IFS= read -r FILE; do
SIZE=$(du -sh "$FILE" | cut -f1)
echo "$SIZE $FILE"
done
When a while loop runs after a pipe (cmd | while read),
it runs in a subshell. Variables set inside the loop are not visible after
done. Use input redirection (while read; done < file)
or process substitution (while read; done < <(command)) to
keep variables in scope.
The until Loop
until is the logical complement of while — it runs the
body as long as the condition is false and stops when it becomes
true.
# Syntax — runs until COMMAND exits 0 (success)
until COMMAND; do
# body runs while COMMAND exits non-zero
done
# Wait until a file appears
until [ -f /tmp/ready.flag ]; do
echo "Waiting for ready flag..."
sleep 5
done
echo "Ready flag found — proceeding"
# The same logic written with while (both are correct)
while [ ! -f /tmp/ready.flag ]; do
sleep 5
done
until COND; do ... done is exactly equivalent to
while ! COND; do ... done. Choose whichever reads more naturally
for the task — "wait until the file exists" reads more clearly as
until than as while not exists.
Loop Control: break and continue
Two keywords modify the normal flow of a loop without ending the script.
| Keyword | Effect | When to use it |
|---|---|---|
break |
Exit the loop immediately — skip remaining iterations and done | Stop searching once a match is found; exit on an error condition |
continue |
Skip the rest of the current iteration and go to the next | Skip items that do not meet a criteria without stopping the loop |
# break — stop after finding the first match
for FILE in /var/log/*.log; do
if grep -q "CRITICAL" "$FILE"; then
echo "Found CRITICAL in: $FILE"
break
fi
done
# continue — skip lines that are comments or blank
while IFS= read -r LINE; do
[[ "$LINE" =~ ^[[:space:]]*# ]] && continue
[[ -z "$LINE" ]] && continue
echo "Active setting: $LINE"
done < /etc/ssh/sshd_config
Nested Loops
Loops can be nested inside each other. The inner loop completes all its iterations for each single iteration of the outer loop.
# Create a grid of directories: host1/data, host1/logs, etc.
for HOST in host1 host2 host3; do
for DIR in data logs config; do
mkdir -p "/srv/${HOST}/${DIR}"
echo "Created /srv/$HOST/$DIR"
done
done
# break 2 exits two levels of nesting at once
for HOST in host1 host2 host3; do
for PORT in 22 80 443; do
if ! nc -z "$HOST" "$PORT" &> /dev/null; then
echo "ALERT: $HOST port $PORT unreachable"
break 2 # exit both loops immediately
fi
done
done
break 2 exits two nested loops at once. continue 2 skips
to the next iteration of the outer loop. These are useful but can make code harder
to read — use them sparingly.
Real Administration Loop Scripts
Create multiple users from a list file
#!/bin/bash
# users.txt contains one username per line
while IFS= read -r USERNAME; do
[[ -z "$USERNAME" ]] && continue
if id "$USERNAME" &> /dev/null; then
echo "$USERNAME already exists — skipping"
else
useradd -m "$USERNAME" && echo "Created $USERNAME"
fi
done < users.txt
Archive old log files
#!/bin/bash
for LOG in /var/log/*.log; do
if [ -s "$LOG" ]; then
gzip -k "$LOG"
echo "Archived: $LOG"
fi
done
Loop Comparison Reference
| Loop type | Best for | Continues while | Closes with |
|---|---|---|---|
for VAR in LIST |
Fixed list, files, command output, arguments | Items remain in the list | done |
for (( i=0; i<N; i++ )) |
Numeric counting, array indexing | Arithmetic condition is true | done |
while COMMAND |
Unknown iteration count, waiting for condition | Command exits 0 (true) | done |
while IFS= read -r VAR |
Reading a file or command output line by line | There are more lines to read | done < file |
until COMMAND |
Waiting until a condition becomes true | Command exits non-zero (false) | done |
The exam most commonly tests for VAR in LIST and while IFS= read -r.
Know both patterns well. The C-style for and until loops are less frequently tested
but may appear as part of a larger script task.
Common Mistakes
| Mistake | What goes wrong | Correct form |
|---|---|---|
Missing done |
Syntax error or bash waits for more input | Every loop must end with done |
Unquoted variable in loop body: $FILE |
Filenames with spaces are split — cp fails or acts on wrong path | Always quote: "$FILE" |
Using $* instead of "$@" for arguments |
Arguments with spaces are split into multiple words | Use "$@" to preserve each argument as one word |
Omitting IFS= and -r in while read |
Leading/trailing whitespace stripped; backslash sequences processed | Use the full pattern: while IFS= read -r LINE |
Setting variables inside a piped while read |
Variables are in a subshell — values lost after done |
Use while read; done < <(command) instead of a pipe |
| Infinite while loop with no exit condition | Script runs forever — must be killed with Ctrl+C or kill | Include a counter guard or a break when a limit is reached |
Knowledge Check
Answer these before moving to the next slide.
- Write a for loop that prints the filename and size of every
.logfile in/var/log/. - Write a while read loop that reads
/etc/passwdand prints only lines where the shell field (the last field, separated by colons) is/bin/bash. - What is the difference between
breakandcontinueinside a loop? - A script receives a list of usernames as command line arguments. Write a for loop that prints whether each user exists or not.
- What is wrong with this loop, and how do you fix it?
for FILE in /etc/ssh/*.conf; do cp $FILE /tmp/; done - Write a while loop that waits until the file
/tmp/done.flagexists, checking every 3 seconds.
Knowledge Check — Answers
-
for FILE in /var/log/*.log; do ls -lh "$FILE" done -
while IFS=: read -r USER P U G C H SHELL; do [[ "$SHELL" == "/bin/bash" ]] && echo "$USER" done < /etc/passwd breakexits the loop entirely — no more iterations run.continueskips only the current iteration and moves on to the next item in the list.-
for U in "$@"; do id "$U" &> /dev/null && echo "$U exists" || echo "$U NOT FOUND" done - The variable
$FILEis unquoted. If any filename contains spaces or special characters,cpwill fail or act on the wrong path. Fix:cp "$FILE" /tmp/ -
until [ -f /tmp/done.flag ]; do echo "Waiting..." sleep 3 done echo "Flag found"
Key Takeaways
-
Use
for VAR in LIST; do ... donefor known lists. The list can be literal words, a glob pattern, command substitution output, or"$@"for command line arguments. Always quote"$VAR"inside the loop body. -
Use
while IFS= read -r VAR; do ... done < filefor files. This is the standard pattern for reading any file or command output line by line.IFS=preserves whitespace;-rprevents backslash interpretation. -
whileloops while condition is true;untilloops until it is. Usewhilewith a counter or a service state. Useuntilwhen waiting for something to appear or become ready. Every loop ends withdone. -
breakexits the loop;continueskips one iteration. Usebreakto stop after finding a match. Usecontinueto skip comments, blanks, or invalid items without stopping the entire loop.
Graded Lab
- Write a for loop script that iterates over every file in
/etc/ssh/and prints its name, permissions, and whether it is a regular file or a directory. - Create a text file
users.txtwith five usernames (one per line). Write a while read script that checks whether each user exists and prints "exists" or "missing" for each. Skip any blank lines. - Write a for loop using
"$@"that accepts service names as arguments and prints whether each service is active, inactive, or unknown. - Write a script that uses a for loop with brace expansion to create ten
directories named
project-01throughproject-10under/tmp/. - Write a while loop that counts from 1 to 20, using
continueto skip even numbers and printing only odd numbers. - Run
bash -n scriptname.shon each script to verify there are no syntax errors before executing it.
"Use looping constructs (for, etc.) to process file, command line input." Script tasks on the exam frequently involve iterating over a list of files, users, or services. Combining loops with if statements and file tests produces complete, production-quality scripts.
Next: Process scripts inputs