| | |

LFCA 59 ๐Ÿง Shell Scripting โ€” Loops

A script that does something once is a command. A script that does something to every file in a directory, every line in a log, every user in a list, or every server in a cluster is automation. Loops are what make that possible.

The LFCA exam places shell scripting under Linux Fundamentals, which carries 20% of the total weight. The competencies include “command line” and “basic programming”. Loops sit squarely in both: they are the control-flow structures that turn a single operation into a repeated one. Backup rotation, log parsing, bulk user creation, service health checks across a fleet of machines โ€” every one of these depends on loops.

This chapter covers the for loop, the while loop, the until loop, loop control with break and continue, and the idioms that make loops safe in practice.

Key point: Bash has three loop constructs. for iterates over a list โ€” a series of words, filenames, or ranges. while repeats as long as a condition succeeds. until repeats as long as a condition fails. The choice between them is not stylistic: it depends on whether you know the set of items in advance (use for) or whether you are waiting for a condition to change (use while or until).


Why loops exist

A script without loops is a list of operations. A script with loops is a process.

The repetition problem. You have 47 log files in /var/log/app/. You want to gzip every file older than seven days. Writing 47 commands is absurd. Writing one for loop that visits each file and checks its age is obvious.

The unknown-count problem. You do not know how many lines a log file has until you read it. You do not know how many retries an HTTP request will need until it succeeds. while loops handle conditions that aren’t enumerable in advance.

The streaming problem. Reading a file line by line, parsing each line, and acting on the parsed data is the fundamental data-processing pattern of Unix. while IFS= read -r line is the idiom that makes it safe.

The wait problem. A deployment script starts a service and needs to wait until it responds to health checks. A until loop that polls the health endpoint every two seconds is the standard solution.

The trade-off. Loops introduce the possibility of infinite loops. A while true without a break runs forever. A for loop over a variable that expands to nothing runs zero times โ€” which is usually correct but sometimes surprising. The discipline is to know what the loop’s input is and to make sure the loop terminates.


a. The for Loop: Iterating Over Lists

The for loop iterates over a list of words. Each iteration assigns the next word to a variable and runs the loop body.

#!/bin/bash
for name in Alice Bob Charlie; do
    echo "Hello, $name"
done

The list can be a literal sequence of words, a variable that expands to multiple words, the output of command substitution, or a range expression. The for loop performs word splitting on its list unless the list is quoted, so unquoted variables and command substitutions split at spaces.

for file in /var/log/*.log; do
    echo "Processing $file"
done

for i in $(seq 1 10); do
    echo "Iteration $i"
done

for i in {1..10}; do
    echo "Iteration $i"
done

The brace expansion {1..10} is a Bash feature that generates the sequence without launching a subprocess. {1..10..2} steps by 2. {a..z} generates the alphabet. {01..10} generates zero-padded numbers. It is faster and more readable than seq for simple ranges, but it does not support variables inside the braces โ€” {$start..$end} does not work as expected.

The C-style for loop is also available:

for ((i = 0; i < 10; i++)); do
    echo "i = $i"
done

This form uses arithmetic evaluation instead of word splitting. It is the correct choice when the iteration count is computed rather than enumerated.


b. The while and until Loops: Repeating on Conditions

The while loop repeats as long as its command returns success (0). The until loop repeats as long as its command returns failure (non-zero). They are duals.

#!/bin/bash
count=1
while [ $count -le 5 ]; do
    echo "Count: $count"
    count=$((count + 1))
done

The loop body must change the condition, or the loop never ends. In this example, count=$((count + 1)) increments the counter until the [ $count -le 5 ] test fails.

The until loop is the same logic inverted:

#!/bin/bash
count=1
until [ $count -gt 5 ]; do
    echo "Count: $count"
    count=$((count + 1))
done

Both loops produce identical output. The choice between them is a matter of readability: use while when you are waiting for something to become true, and until when you are waiting for something to become false.

The most important while idiom in shell scripting is reading a file line by line:

#!/bin/bash
while IFS= read -r line; do
    echo "Line: $line"
done < /etc/passwd

This deserves careful attention. IFS= clears the internal field separator for the read command, preventing it from stripping leading and trailing whitespace. -r prevents read from interpreting backslashes as escape characters. Without these, lines containing spaces or backslashes would be mangled. The redirection < /etc/passwd feeds the file into the loop’s standard input.

There is a well-known trap with this pattern: if the loop body contains a command that reads from standard input โ€” like ssh, grep, or another read โ€” it will consume the file’s input and the loop will exit early. The fix is to redirect standard input for the inner command from /dev/null or from a different file descriptor.

Another common form is the infinite loop with a break condition:

while true; do
    if some_condition; then
        break
    fi
    do_something
done

while true is a deliberate infinite loop. The break exits it. This pattern is useful when the termination condition is complex or appears in the middle of the loop body rather than at the top.


c. Loop Control: break, continue, and Safety

The break command exits the loop immediately. The continue command skips the rest of the current iteration and starts the next one. Both accept an optional numeric argument that specifies how many levels of nested loops to affect. break 2 exits two nested loops; continue 2 starts the next iteration of the outer loop.

#!/bin/bash
for file in *.txt; do
    if [ ! -r "$file" ]; then
        echo "Skipping unreadable file: $file"
        continue
    fi
    if grep -q "FATAL" "$file"; then
        echo "Found fatal error in $file, stopping"
        break
    fi
    process "$file"
done

continue skips files that cannot be read, allowing the loop to keep going. break stops the loop entirely when a critical condition is found.

Loops can also be nested. The inner loop runs completely for each iteration of the outer loop. This is the pattern for processing a two-dimensional structure โ€” a list of servers, each with a list of services:

for server in web1 web2 web3; do
    for service in nginx postgres redis; do
        echo "Checking $service on $server"
    done
done

A safety practice: capture the loop’s input into a variable before iterating, especially when the input comes from a command. If you iterate over a glob and the glob matches nothing, Bash by default leaves the pattern unexpanded and the loop runs once with the literal pattern as the “filename”. The nullglob shell option changes this behavior so an unmatched glob expands to nothing and the loop runs zero times. The failglob option makes the shell error out instead. Both options are set with shopt -s nullglob or shopt -s failglob.

Similarly, when iterating over the output of ls or find, prefer the -print0 option of find combined with read -d '' to handle filenames containing newlines. This is beyond the scope of an LFCA exam but worth knowing as the “correct” pattern.


Complete Example Session

This session builds a log rotation script, a bulk user-creation loop, and a service-health poller, demonstrating every loop form.

#!/bin/bash
# loops-demo.sh - Demonstrates for, while, until, break, continue

# ============================================
# PART 1: THE BASIC FOR LOOP
# ============================================

for fruit in apple banana cherry; do
    echo "Fruit: $fruit"
done

# ============================================
# PART 2: THE FOR LOOP OVER FILES
# ============================================

mkdir -p /tmp/loop-demo
touch /tmp/loop-demo/{one,two,three}.log

for logfile in /tmp/loop-demo/*.log; do
    echo "Found log: $logfile"
done

# ============================================
# PART 3: THE FOR LOOP WITH BRACE EXPANSION
# ============================================

for i in {1..5}; do
    echo "Number: $i"
done

for letter in {a..e}; do
    echo "Letter: $letter"
done

# ============================================
# PART 4: THE C-STYLE FOR LOOP
# ============================================

for ((i = 0; i < 5; i++)); do
    echo "C-style: i = $i"
done

# ============================================
# PART 5: THE WHILE LOOP
# ============================================

count=1
while [ $count -le 5 ]; do
    echo "While count: $count"
    count=$((count + 1))
done

# ============================================
# PART 6: THE UNTIL LOOP
# ============================================

count=1
until [ $count -gt 5 ]; do
    echo "Until count: $count"
    count=$((count + 1))
done

# ============================================
# PART 7: THE WHILE READ LINE IDIOM
# ============================================

while IFS= read -r line; do
    echo "User entry: $line"
done < /etc/passwd | head -n 3

# ============================================
# PART 8: THE BREAK STATEMENT
# ============================================

for i in {1..10}; do
    if [ $i -eq 5 ]; then
        echo "Breaking at $i"
        break
    fi
    echo "Iteration: $i"
done

# ============================================
# PART 9: THE CONTINUE STATEMENT
# ============================================

for i in {1..10}; do
    if [ $((i % 2)) -eq 0 ]; then
        continue
    fi
    echo "Odd number: $i"
done

# ============================================
# PART 10: THE BULK USER CREATION PATTERN
# ============================================

# Read users from a file, create each one
while IFS=: read -r username uid gid comment home shell; do
    if [ "$uid" -ge 1000 ] && [ "$uid" -lt 65534 ]; then
        echo "Regular user: $username (UID $uid)"
    fi
done < /etc/passwd

# ============================================
# PART 11: THE SERVICE HEALTH POLLER
# ============================================

max_attempts=10
attempt=1
until curl -sf http://localhost:8080/health > /dev/null 2>&1; do
    if [ $attempt -ge $max_attempts ]; then
        echo "Service failed to respond after $max_attempts attempts"
        exit 1
    fi
    echo "Attempt $attempt: waiting for service..."
    sleep 2
    attempt=$((attempt + 1))
done
echo "Service is healthy"

# ============================================
# PART 12: THE NESTED LOOP
# ============================================

for server in web1 web2; do
    for service in nginx postgres; do
        echo "Checking $service on $server"
    done
done

# ============================================
# PART 13: THE LOG ROTATION LOOP
# ============================================

LOG_DIR="/var/log/app"
MAX_AGE_DAYS=7

for logfile in "$LOG_DIR"/*.log; do
    # Skip if no files matched
    [ -e "$logfile" ] || continue

    # Skip if file is newer than MAX_AGE_DAYS
    if [ "$(find "$logfile" -mtime -$MAX_AGE_DAYS)" ]; then
        continue
    fi

    gzip "$logfile"
    echo "Compressed: $logfile"
done

The thirteen parts cover the basic for, the for over files, brace expansion, the C-style for, the while, the until, the read line idiom, break, continue, reading structured data from /etc/passwd, the health poller, the nested loop, and the log rotation pattern.


Quick Reference

The Loop Forms

LoopSyntaxUse When
for (list)for x in list; do ...; doneIterating a known list
for (brace)for i in {1..10}; do ...; doneIterating a numeric range
for (C-style)for ((i=0; i<10; i++)); do ...; doneComputed iteration counts
whilewhile cmd; do ...; doneRepeating while condition succeeds
untiluntil cmd; do ...; doneRepeating while condition fails
while readwhile IFS= read -r line; do ...; done < fileReading a file line by line

The Loop Control Commands

CommandEffect
breakExit the loop immediately
break NExit N levels of nested loops
continueSkip to next iteration
continue NSkip to next iteration of N-th outer loop

The Safe Read Idiom

ComponentPurpose
IFS=Preserve leading/trailing whitespace
read -rDon’t interpret backslashes
< fileFeed the file as input
read -r a b cSplit line into fields

The Glob Safety Options

OptionEffect
shopt -s nullglobUnmatched globs expand to nothing
shopt -s failglobUnmatched globs cause an error
DefaultUnmatched globs stay literal

Best Practices

โœ… Do This:

for file in *.log; do                                 # โœ… iterate a glob
while IFS= read -r line; do                           # โœ… safe line reading
    echo "$line"
done < file.txt
[ -e "$logfile" ] || continue                         # โœ… skip unmatched globs
for i in {1..10}; do                                  # โœ… brace expansion for ranges
for ((i = 0; i < n; i++)); do                         # โœ… C-style for computed counts
if [ $attempt -ge $max ]; then break; fi              # โœ… break conditions

โŒ Don’t Do This:

for file in $(ls *.txt); do                           # โŒ breaks on spaces
while read line; do                                   # โŒ strips whitespace
    echo $line
done < file.txt
for i in {1..$n}; do                                  # โŒ brace doesn't expand vars
while [ $count -le 5 ]; do                            # โŒ infinite โ€” count never changes
    echo "$count"
done
for i in 1 2 3; do                                    # โŒ unquoted variable in loop
    ssh $i "uptime"
done

Common Pitfalls

PitfallWhy It HappensFix
Loop runs once on literal patternGlob matched nothingAdd shopt -s nullglob or [ -e "$f" ] || continue
Leading spaces strippedMissing IFS=Use IFS= read -r
Backslashes eatenMissing -rUse read -r
Loop body consumes stdinInner command reads from loop’s stdinRedirect inner: cmd < /dev/null
Variable expansion in braces fails{$a..$b} doesn’t workUse seq or C-style for
Infinite loopCondition never changesEnsure body modifies the condition
break only exits inner loopNested loopsUse break 2 for outer

Real-World Examples

1. Basic for Loop

for name in Alice Bob Charlie; do
    echo "Hello, $name"
done

2. For Over Files

for logfile in /var/log/*.log; do
    gzip "$logfile"
done

3. Brace Range

for i in {1..10}; do
    echo "$i"
done

4. C-Style For

for ((i = 0; i < 10; i++)); do
    echo "i=$i"
done

5. While Counter

count=1
while [ $count -le 5 ]; do
    echo "$count"
    count=$((count + 1))
done

6. Until Polling

until curl -sf http://localhost/health; do
    sleep 2
done

7. Read File Line by Line

while IFS= read -r line; do
    echo "$line"
done < file.txt

8. Break on Condition

for i in {1..100}; do
    [ $i -eq 5 ] && break
    echo "$i"
done

9. Continue to Skip

for i in {1..10}; do
    [ $((i % 2)) -eq 0 ] && continue
    echo "$i"
done

10. Nested Loop

for server in web1 web2; do
    for svc in nginx postgres; do
        echo "$svc on $server"
    done
done

Visual: Loop Forms

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  LOOP FORMS                                  โ”‚
โ”‚                                              โ”‚
โ”‚  for x in list; do ... done                  โ”‚
โ”‚    โ””โ”€ iterate over words                     โ”‚
โ”‚                                              โ”‚
โ”‚  for i in {1..10}; do ... done               โ”‚
โ”‚    โ””โ”€ iterate over a range                   โ”‚
โ”‚                                              โ”‚
โ”‚  for ((i=0; i<n; i++)); do ... done          โ”‚
โ”‚    โ””โ”€ iterate with a counter                 โ”‚
โ”‚                                              โ”‚
โ”‚  while [ cond ]; do ... done                 โ”‚
โ”‚    โ””โ”€ repeat while condition succeeds        โ”‚
โ”‚                                              โ”‚
โ”‚  until [ cond ]; do ... done                 โ”‚
โ”‚    โ””โ”€ repeat while condition fails           โ”‚
โ”‚                                              โ”‚
โ”‚  while IFS= read -r line; do ... done < file โ”‚
โ”‚    โ””โ”€ read a file line by line               โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: break and continue Flow

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  BREAK AND CONTINUE FLOW                     โ”‚
โ”‚                                              โ”‚
โ”‚  for i in 1..10:                             โ”‚
โ”‚    i=1 โ†’ body โ†’ echo                         โ”‚
โ”‚    i=2 โ†’ body โ†’ echo                         โ”‚
โ”‚    i=3 โ†’ body โ†’ continue? โ†’ skip to i=4      โ”‚
โ”‚    i=4 โ†’ body โ†’ echo                         โ”‚
โ”‚    i=5 โ†’ body โ†’ break? โ†’ exit loop           โ”‚
โ”‚    i=6..10 never run                         โ”‚
โ”‚                                              โ”‚
โ”‚  continue: skip rest of this iteration       โ”‚
โ”‚  break:    exit the loop entirely            โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: Reading a File Line by Line

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SAFE LINE READING                           โ”‚
โ”‚                                              โ”‚
โ”‚  while IFS= read -r line; do                 โ”‚
โ”‚      echo "$line"                            โ”‚
โ”‚  done < file.txt                             โ”‚
โ”‚                                              โ”‚
โ”‚  IFS=        โ† preserve whitespace           โ”‚
โ”‚  read -r     โ† don't eat backslashes         โ”‚
โ”‚  < file.txt  โ† feed file to loop             โ”‚
โ”‚                                              โ”‚
โ”‚  Without IFS=: leading spaces stripped       โ”‚
โ”‚  Without -r:   backslashes disappear         โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Visual: The Nested Loop Trap

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  NESTED LOOP BREAK                           โ”‚
โ”‚                                              โ”‚
โ”‚  for outer in 1 2 3; do                      โ”‚
โ”‚    for inner in a b c; do                    โ”‚
โ”‚      if [ "$inner" = "b" ]; then             โ”‚
โ”‚        break          โ† exits inner only     โ”‚
โ”‚      fi                                      โ”‚
โ”‚    done                                      โ”‚
โ”‚    echo "outer: $outer"  โ† still runs        โ”‚
โ”‚  done                                        โ”‚
โ”‚                                              โ”‚
โ”‚  To exit both loops:                         โ”‚
โ”‚      break 2                                 โ”‚
โ”‚                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
for (list)for x in a b c; do ...; done
for (brace)for i in {1..10}; do ...; done
for (C-style)for ((i=0;i<n;i++)); do ...; done
whilewhile cmd; do ...; done
untiluntil cmd; do ...; done
safe readwhile IFS= read -r line; do ...; done < file
breakExit loop
continueSkip to next iteration
break NExit N nested loops
glob safetyshopt -s nullglob or [ -e "$f" ] || continue

Key takeaways:

  • for iterates lists; while and until iterate conditions. Use for when you know the items. Use while when you’re waiting for something to become true. Use until when you’re waiting for something to become false.
  • Brace expansion is faster than seq. {1..10} generates the range without launching a subprocess, but it does not expand variables inside the braces โ€” {$start..$end} does not work.
  • The safe read idiom is while IFS= read -r line. Clear IFS to preserve whitespace. Use -r to preserve backslashes. Without both, lines are silently mangled.
  • The loop body must change the condition. A while loop whose condition never changes runs forever. Ensure the body increments counters or updates state.
  • break exits; continue skips. break 2 exits two nested loops. continue 2 skips to the next iteration of the outer loop.
  • Globs fail silently. An unmatched glob expands to the literal pattern, and the loop runs once with that pattern as a “filename”. Guard with [ -e "$f" ] || continue or shopt -s nullglob.
  • Inner commands can consume stdin. If the loop body runs ssh, grep, or another read, it will eat the loop’s input. Redirect the inner command from /dev/null or a separate file descriptor.

Remember: Loops are how a script stops being a single action and becomes a process. for iterates lists. while and until wait for conditions. break and continue control flow inside the loop. The safe read idiom and glob guards are the difference between a script that works on your test data and one that works on real data โ€” with spaces, backslashes, and edge cases. The LFCA exam tests the fundamentals: knowing when to use each loop, and knowing the idioms that make loops safe.

Linux, shell scripting, bash, LFCA


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!