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
| Loop | Syntax | Use When |
|---|---|---|
| for (list) | for x in list; do ...; done | Iterating a known list |
| for (brace) | for i in {1..10}; do ...; done | Iterating a numeric range |
| for (C-style) | for ((i=0; i<10; i++)); do ...; done | Computed iteration counts |
| while | while cmd; do ...; done | Repeating while condition succeeds |
| until | until cmd; do ...; done | Repeating while condition fails |
| while read | while IFS= read -r line; do ...; done < file | Reading a file line by line |
The Loop Control Commands
| Command | Effect |
|---|---|
break | Exit the loop immediately |
break N | Exit N levels of nested loops |
continue | Skip to next iteration |
continue N | Skip to next iteration of N-th outer loop |
The Safe Read Idiom
| Component | Purpose |
|---|---|
IFS= | Preserve leading/trailing whitespace |
read -r | Don’t interpret backslashes |
< file | Feed the file as input |
read -r a b c | Split line into fields |
The Glob Safety Options
| Option | Effect |
|---|---|
shopt -s nullglob | Unmatched globs expand to nothing |
shopt -s failglob | Unmatched globs cause an error |
| Default | Unmatched 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Loop runs once on literal pattern | Glob matched nothing | Add shopt -s nullglob or [ -e "$f" ] || continue |
| Leading spaces stripped | Missing IFS= | Use IFS= read -r |
| Backslashes eaten | Missing -r | Use read -r |
| Loop body consumes stdin | Inner command reads from loop’s stdin | Redirect inner: cmd < /dev/null |
| Variable expansion in braces fails | {$a..$b} doesn’t work | Use seq or C-style for |
| Infinite loop | Condition never changes | Ensure body modifies the condition |
break only exits inner loop | Nested loops | Use 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
| Item | Value |
|---|---|
| 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 |
| while | while cmd; do ...; done |
| until | until cmd; do ...; done |
| safe read | while IFS= read -r line; do ...; done < file |
| break | Exit loop |
| continue | Skip to next iteration |
| break N | Exit N nested loops |
| glob safety | shopt -s nullglob or [ -e "$f" ] || continue |
Key takeaways:
foriterates lists;whileanduntiliterate conditions. Useforwhen you know the items. Usewhilewhen you’re waiting for something to become true. Useuntilwhen 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. ClearIFSto preserve whitespace. Use-rto preserve backslashes. Without both, lines are silently mangled. - The loop body must change the condition. A
whileloop whose condition never changes runs forever. Ensure the body increments counters or updates state. breakexits;continueskips.break 2exits two nested loops.continue 2skips 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" ] || continueorshopt -s nullglob. - Inner commands can consume stdin. If the loop body runs
ssh,grep, or anotherread, it will eat the loop’s input. Redirect the inner command from/dev/nullor 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!