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

  1. 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
  2. Use while loops to process input — Read a file line by line and loop while a condition is true using while read and while [ ]
  3. Use until loops — Loop until a condition becomes true — the logical complement of while
  4. Control loop flow with break and continue — Exit a loop early with break and skip iterations with continue

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
done closes every loop

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
Always quote $FILE inside the loop

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
Word splitting with command substitution

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, $2Individual 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
Brace expansion vs C-style

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"
Avoid infinite loops

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= and -r explained

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
Variables set in a piped loop are lost

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 is while with a negated condition

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 N exits N levels

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
RHCSA Exam Focus

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.

  1. Write a for loop that prints the filename and size of every .log file in /var/log/.
  2. Write a while read loop that reads /etc/passwd and prints only lines where the shell field (the last field, separated by colons) is /bin/bash.
  3. What is the difference between break and continue inside a loop?
  4. A script receives a list of usernames as command line arguments. Write a for loop that prints whether each user exists or not.
  5. What is wrong with this loop, and how do you fix it?
    for FILE in /etc/ssh/*.conf; do cp $FILE /tmp/; done
  6. Write a while loop that waits until the file /tmp/done.flag exists, checking every 3 seconds.

Knowledge Check — Answers

  1. for FILE in /var/log/*.log; do
        ls -lh "$FILE"
    done
  2. while IFS=: read -r USER P U G C H SHELL; do
        [[ "$SHELL" == "/bin/bash" ]] && echo "$USER"
    done < /etc/passwd
  3. break exits the loop entirely — no more iterations run. continue skips only the current iteration and moves on to the next item in the list.
  4. for U in "$@"; do
        id "$U" &> /dev/null && echo "$U exists" || echo "$U NOT FOUND"
    done
  5. The variable $FILE is unquoted. If any filename contains spaces or special characters, cp will fail or act on the wrong path. Fix: cp "$FILE" /tmp/
  6. until [ -f /tmp/done.flag ]; do
        echo "Waiting..."
        sleep 3
    done
    echo "Flag found"

Key Takeaways

  1. Use for VAR in LIST; do ... done for 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.
  2. Use while IFS= read -r VAR; do ... done < file for files. This is the standard pattern for reading any file or command output line by line. IFS= preserves whitespace; -r prevents backslash interpretation.
  3. while loops while condition is true; until loops until it is. Use while with a counter or a service state. Use until when waiting for something to appear or become ready. Every loop ends with done.
  4. break exits the loop; continue skips one iteration. Use break to stop after finding a match. Use continue to 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.txt with 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-01 through project-10 under /tmp/.
  • Write a while loop that counts from 1 to 20, using continue to skip even numbers and printing only odd numbers.
  • Run bash -n scriptname.sh on each script to verify there are no syntax errors before executing it.
RHCSA Objective

"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.