Bash Scripting 10 ๐ง For Loops โ Iterating over sequences, lists, and file patterns
A for loop is the construct that repeats a command list once for each item in a sequence. In Bash, the sequence can be a literal list of words, a range of numbers, the output of a command, an array’s elements, or the files matched by a glob pattern. The loop variable takes on each item in turn, and the body executes with that value in scope. The construct is simple, but the detailsโhow the list is split, how filenames with spaces are handled, and how the loop interacts with arithmetic and command substitutionโdetermine whether the script processes its input correctly or breaks on the first unusual filename.
The previous chapters covered conditional logic, arithmetic, and string manipulation. The for loop is where those tools combine: a condition checks each item, arithmetic tracks an index or accumulates a total, and string operations extract or transform each value. It is the workhorse of script iteration, and the C-style variant provides numeric iteration for cases where the item list is generated by arithmetic rather than enumerated.
Key point: The default for loop iterates over a word list that is subject to word splitting and pathname expansion. for file in *.txt expands the glob to matching filenames; for item in $list splits the variable’s value on whitespace. To iterate over filenames with spaces, use a glob directly without quoting it, or use "$@" for positional parameters, or use an array with "${array[@]}". Never use for line in $(command); use while IFS= read -r line instead.
Why for loops require attention to word splitting
The word-splitting problem.
A for loop iterates over words, not lines. When the list is a b c, the loop runs three times. When the list is $var containing "a b c" unquoted, the loop still runs three times, because the variable’s value is split on whitespace before the loop begins. This behavior is correct for lists of simple tokens and wrong for filenames, paths, and any value that may contain spaces. The shell offers no way to distinguish between “three separate items” and “one item with spaces” without the quoting and array mechanisms that the loop must be given explicitly.
The command-substitution problem.
A loop that reads for line in $(grep pattern file) splits the command output on whitespace and iterates over the words, not the lines. A line containing hello world produces two iterations, not one. The idiomatic solution is a while loop with read, which reads one line at a time and preserves the line’s internal spaces. The for loop is the wrong construct for line-oriented input.
The glob problem.
for file in *.txt is correct and safe. The glob is expanded by the shell before the loop begins, and each matching filename is a separate word. The expansion preserves filenames with spaces because the glob mechanism does not perform word splitting on the filenames themselves. A loop that quotes the globโfor file in "*.txt"โiterates once over the literal string *.txt, which is almost never what the script intends.
a. Iterating over lists and positional parameters
The simplest form of the for loop enumerates a literal list. The loop variable takes each word in turn.
#!/usr/bin/env bash
# Iterate over a literal list
for color in red green blue; do
echo "Color: $color" # Runs three times
done
# Iterate over positional parameters with "$@"
for arg in "$@"; do
echo "Argument: $arg" # Preserves spaces in each argument
done
# Iterate over a variable containing a list (word splitting intended)
servers="web1 web2 web3"
for server in $servers; do
echo "Checking $server" # Splits on whitespace; three iterations
done
The "$@" form is the correct way to iterate over a script’s arguments. Quoting preserves the boundaries between arguments, so an argument containing a space is one iteration, not two. The unquoted $servers form is acceptable only when the variable is known to contain a space-separated list of simple tokens with no embedded spaces.
An array is the safe way to store a list that may contain spaces. The ${array[@]} expansion, when quoted, produces one word per element.
# Array iteration preserves element boundaries
files=("my file.txt" "another file.txt" "third.txt")
for file in "${files[@]}"; do
echo "Processing: $file" # Three iterations, spaces preserved
done
The ${array[@]} form is the array equivalent of "$@". It is the only expansion that preserves element boundaries without word splitting, and it is the correct way to iterate over an array whose elements may contain spaces.
b. Iterating over file patterns and command output
A glob pattern in a for loop expands to the matching filenames. The shell expands the pattern before the loop starts, so each iteration receives one filename.
#!/usr/bin/env bash
# Iterate over all .conf files in the current directory
for conf in *.conf; do
# Guard against no matches: the glob remains literal if nothing matches
[ -e "$conf" ] || continue
echo "Loading $conf" # One iteration per matching file
done
# Iterate over files in a directory with a glob
for log in /var/log/*.log; do
[ -f "$log" ] || continue # Skip non-files (e.g., directories)
echo "$(basename "$log"): $(wc -l < "$log") lines"
done
The [ -e "$conf" ] || continue guard handles the case where no files match the pattern. Without nullglob enabled, the glob remains as the literal string *.conf, and the loop runs once with that literal value. The guard skips it. An alternative is to enable shopt -s nullglob, which makes an unmatched glob expand to nothing and the loop run zero times.
Command output should not be iterated with for unless the output is a list of simple tokens with no spaces. The correct construct for line-oriented input is a while loop with read.
# โ INCORRECT: splits on whitespace, breaks on spaces in lines
for line in $(cat file.txt); do
echo "$line"
done
# โ
CORRECT: reads one line at a time, preserves spaces
while IFS= read -r line; do
echo "$line"
done < file.txt
The while loop with IFS= read -r reads one line per iteration, preserves leading and trailing whitespace, and does not interpret backslashes. The for loop with command substitution splits the output on whitespace and is incorrect for line-oriented data.
c. The C-style for loop and ranges
The C-style for (( init; condition; increment )) loop provides numeric iteration. It is equivalent to a while loop with an arithmetic condition and is the idiomatic way to iterate over a numeric range when the range is computed rather than enumerated.
#!/usr/bin/env bash
# C-style loop: count from 1 to 5
for (( i = 1; i <= 5; i++ )); do
echo "Iteration $i" # Runs five times
done
# Brace expansion: numeric range
for i in {1..5}; do
echo "Number $i" # Runs five times; i is a string
done
# Brace expansion with step (Bash 4+)
for i in {0..10..2}; do
echo "Even: $i" # 0, 2, 4, 6, 8, 10
done
# C-style is required when the bounds are variables
start=3
end=7
for (( i = start; i <= end; i++ )); do
echo "Value: $i" # 3, 4, 5, 6, 7
done
The brace expansion {1..5} is expanded by the shell before the loop begins. It produces a literal list of strings, and the loop variable is a string, not a number. The C-style form evaluates arithmetic expressions, so i is an integer and the bounds can be variables or expressions.
The C-style form is also the correct way to iterate backward or with a non-unit step, because brace expansion with a negative step is not supported and a variable step requires arithmetic evaluation.
Complete Example Session
#!/usr/bin/env bash
# ============================================
# PART 1: LITERAL LIST ITERATION
# ============================================
for env in dev staging prod; do
echo "Deploying to $env"
done
# ============================================
# PART 2: POSITIONAL PARAMETER ITERATION
# ============================================
for arg in "$@"; do
echo "Processing argument: $arg"
done
# ============================================
# PART 3: ARRAY ITERATION WITH SPACES
# ============================================
configs=("app config.yml" "db config.yml" "cache config.yml")
for cfg in "${configs[@]}"; do
echo "Config: $cfg" # Spaces preserved
done
# ============================================
# PART 4: GLOB ITERATION WITH GUARD
# ============================================
shopt -s nullglob # Unmatched globs expand to nothing
for file in /etc/*.conf; do
echo "Found config: $file"
done
shopt -u nullglob
# ============================================
# PART 5: NUMERIC RANGE WITH BRACE EXPANSION
# ============================================
for port in {8000..8005}; do
echo "Checking port $port"
done
# ============================================
# PART 6: C-STYLE LOOP WITH VARIABLE BOUNDS
# ============================================
start=1
end=3
for (( i = start; i <= end; i++ )); do
echo "Worker $i"
done
# ============================================
# PART 7: C-STYLE LOOP WITH STEP
# ============================================
for (( i = 0; i <= 10; i += 2 )); do
echo "Even: $i"
done
# ============================================
# PART 8: ACCUMULATING IN A LOOP
# ============================================
total=0
for n in 1 2 3 4 5; do
(( total += n ))
done
echo "Total: $total" # 15
# ============================================
# PART 9: LINE-ORIENTED INPUT (WHILE, NOT FOR)
# ============================================
while IFS= read -r line; do
echo "Line: $line" # Preserves spaces in each line
done < /etc/hostname
# ============================================
# PART 10: LOOP WITH BREAK AND CONTINUE
# ============================================
for file in *.log; do
[ -s "$file" ] || continue # Skip empty files
if grep -q "FATAL" "$file"; then
echo "Fatal error in $file"
break # Stop at the first fatal file
fi
done
The ten parts demonstrate the full range of iteration: literal lists, positional parameters, arrays with spaces, globs with guards, numeric ranges, C-style loops with variable bounds and steps, accumulation, line-oriented input with while, and early exit with break. The critical distinction is between the items that are safe to iterate with for and the lines that must be read with while.
Quick Reference
Loop Forms
| Form | Syntax | Use For |
|---|---|---|
| Word list | for x in a b c; do | Literal items |
| Positional | for x in "$@"; do | Script arguments |
| Array | for x in "${arr[@]}"; do | Arrays with spaces |
| Glob | for x in *.txt; do | Filenames |
| Brace range | for x in {1..10}; do | Numeric ranges |
| C-style | for ((i=0; i<n; i++)); do | Arithmetic iteration |
Iteration Safety
| Input | Correct Construct |
|---|---|
| Simple tokens | for x in $list |
| Filenames with spaces | for x in *.txt or "${arr[@]}" |
| Arguments | for x in "$@" |
| Lines from file | while IFS= read -r line |
| Command output lines | while IFS= read -r line < <(cmd) |
Loop Control
| Command | Effect |
|---|---|
break | Exit the loop immediately |
continue | Skip to the next iteration |
break N | Exit N levels of nested loops |
continue N | Continue the Nth enclosing loop |
Best Practices
โ Do This:
# Use "$@" for arguments
for arg in "$@"; do echo "$arg"; done # โ
# Use "${array[@]}" for arrays
for item in "${array[@]}"; do echo "$item"; done # โ
# Use a glob directly
for file in *.txt; do echo "$file"; done # โ
# Guard against unmatched globs
shopt -s nullglob; for f in *.txt; do ...; done # โ
# Use while/read for line-oriented input
while IFS= read -r line; do echo "$line"; done < file # โ
# Use C-style for variable bounds
for ((i=0; i<count; i++)); do echo "$i"; done # โ
โ Don’t Do This:
# Use command substitution in a for loop for lines
for line in $(cat file); do echo "$line"; done # โ
# Quote the glob
for file in "*.txt"; do echo "$file"; done # Literal # โ
# Iterate over unquoted array
for item in ${array[@]}; do echo "$item"; done # Splits # โ
# Forget the guard for unmatched globs
for f in *.nonexistent; do echo "$f"; done # One literal # โ
# Use brace expansion with variable bounds
for i in {1..$n}; do echo "$i"; done # Does not expand # โ
# Modify the loop variable inside a C-style loop
for ((i=0; i<10; i++)); do i=$((i+1)); done # Skips values # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Filenames with spaces split | Unquoted variable or command substitution | Use glob or "${arr[@]}" |
Loop runs once with literal *.txt | Glob quoted or no matches | Remove quotes; enable nullglob |
| Lines split on whitespace | Used for with command substitution | Use while IFS= read -r |
| Brace expansion with variable | {1..$n} is not expanded | Use C-style for ((...)) |
| Loop does not iterate at all | Empty array or unmatched glob with nullglob | Check input or disable nullglob |
| Infinite loop | C-style condition never becomes false | Verify increment and condition |
Real-World Examples
1. Iterate over arguments
for arg in "$@"; do echo "$arg"; done
2. Iterate over array with spaces
for f in "${files[@]}"; do echo "$f"; done
3. Iterate over glob
for f in *.log; do echo "$f"; done
4. Numeric range
for i in {1..10}; do echo "$i"; done
5. C-style with variable bounds
for ((i=0; i<${#arr[@]}; i++)); do echo "${arr[$i]}"; done
6. C-style with step
for ((i=0; i<=100; i+=10)); do echo "$i"; done
7. Accumulate total
total=0; for n in 1 2 3; do ((total+=n)); done
8. Read lines from file
while IFS= read -r line; do echo "$line"; done < file
9. Read from command output
while IFS= read -r line; do echo "$line"; done < <(grep pattern file)
10. Break on condition
for f in *.log; do grep -q FATAL "$f" && break; done
Visual
For Loop Iteration
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE FOR LOOP TAKES EACH WORD IN TURN โ
โ โ
โ for color in red green blue; do โ
โ โ โ
โ โผ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Iteration 1: color=red โ run body โ โ
โ โ Iteration 2: color=green โ run body โ โ
โ โ Iteration 3: color=blue โ run body โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ The list is expanded before the loop begins. โ
โ Each word is one iteration. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Word Splitting
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ QUOTING DETERMINES ITERATION BOUNDARIES โ
โ โ
โ list="a b c" โ
โ for x in $list; do ... โ
โ โ 3 iterations: a, b, c โ
โ โ
โ list="a b c" โ
โ for x in "$list"; do ... โ
โ โ 1 iteration: "a b c" โ
โ โ
โ arr=("a b" "c") โ
โ for x in ${arr[@]}; do ... โ
โ โ 3 iterations: a, b, c (splits) โ
โ โ
โ arr=("a b" "c") โ
โ for x in "${arr[@]}"; do ... โ
โ โ 2 iterations: "a b", "c" (preserved) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
For vs While for Input
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ FOR IS FOR TOKENS; WHILE IS FOR LINES โ
โ โ
โ File contents: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ hello world โ โ
โ โ goodbye moon โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ for line in $(cat file); do echo "$line"; done โ
โ โ hello, world, goodbye, moon (4 iterations, WRONG) โ
โ โ
โ while IFS= read -r line; do echo "$line"; done < file โ
โ โ "hello world", "goodbye moon" (2 iterations, CORRECT) โ
โ โ
โ Use for for tokens. Use while for lines. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Basic syntax | for var in list; do ... done |
| C-style syntax | for ((init; cond; incr)); do ... done |
| Word list | Splits on whitespace |
"$@" | Preserves argument boundaries |
"${arr[@]}" | Preserves array element boundaries |
| Glob | Expands to matching filenames |
| Brace range | {1..10} expands before loop |
| Line input | Use while IFS= read -r |
break | Exit loop immediately |
continue | Skip to next iteration |
nullglob | Unmatched glob expands to nothing |
Key takeaways:
- The for loop iterates over words, not lines.
for x in $listsplits the variable’s value on whitespace. For lists of simple tokens, this is correct. For filenames, paths, or any value that may contain spaces, it is a bug. "$@"and"${arr[@]}"preserve boundaries. These are the only expansions that produce one iteration per argument or array element without word splitting. An unquoted array expansion splits on whitespace and breaks on spaces.- Globs are safe; quoted globs are not.
for file in *.txtiterates over the matching filenames, preserving spaces.for file in "*.txt"iterates once over the literal string. The guard[ -e "$file" ] || continuehandles the no-match case whennullglobis not enabled. - Line-oriented input requires
while, notfor.for line in $(cat file)splits on whitespace and produces the wrong number of iterations.while IFS= read -r line; do ...; done < filereads one line at a time and preserves the line’s contents. - Brace expansion is static; C-style loops are dynamic.
{1..$n}does not expand because brace expansion happens before variable expansion. When the bounds are variables or expressions, the C-stylefor ((...))form is required. breakandcontinuecontrol the loop.breakexits immediately, andcontinueskips to the next iteration.break Nandcontinue Noperate on nested loops.
Remember: The for loop is the iterator for lists. The list can be literal words, positional parameters, array elements, matching filenames, or a numeric range. The boundaries between items are determined by quoting: "$@" and "${arr[@]}" preserve them, and unquoted expansions split them. Globs are expanded by the shell before the loop starts, so they are safe for filenames. Line-oriented input is not a list of words and must be read with while, not for. Use the C-style loop when the range is computed, and use break and continue when the loop needs early exit or skip logic. The for loop is simple, but the quoting rules determine whether it processes the input correctly.
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!