| |

Bash Scripting 10 ๐Ÿง For Loops โ€” Iterating over sequences, lists, and file patterns

โฑ๏ธ Estimated read: 13 min | |

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

FormSyntaxUse For
Word listfor x in a b c; doLiteral items
Positionalfor x in "$@"; doScript arguments
Arrayfor x in "${arr[@]}"; doArrays with spaces
Globfor x in *.txt; doFilenames
Brace rangefor x in {1..10}; doNumeric ranges
C-stylefor ((i=0; i<n; i++)); doArithmetic iteration

Iteration Safety

InputCorrect Construct
Simple tokensfor x in $list
Filenames with spacesfor x in *.txt or "${arr[@]}"
Argumentsfor x in "$@"
Lines from filewhile IFS= read -r line
Command output lineswhile IFS= read -r line < <(cmd)

Loop Control

CommandEffect
breakExit the loop immediately
continueSkip to the next iteration
break NExit N levels of nested loops
continue NContinue 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

PitfallWhy It HappensFix
Filenames with spaces splitUnquoted variable or command substitutionUse glob or "${arr[@]}"
Loop runs once with literal *.txtGlob quoted or no matchesRemove quotes; enable nullglob
Lines split on whitespaceUsed for with command substitutionUse while IFS= read -r
Brace expansion with variable{1..$n} is not expandedUse C-style for ((...))
Loop does not iterate at allEmpty array or unmatched glob with nullglobCheck input or disable nullglob
Infinite loopC-style condition never becomes falseVerify 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

ItemValue
Basic syntaxfor var in list; do ... done
C-style syntaxfor ((init; cond; incr)); do ... done
Word listSplits on whitespace
"$@"Preserves argument boundaries
"${arr[@]}"Preserves array element boundaries
GlobExpands to matching filenames
Brace range{1..10} expands before loop
Line inputUse while IFS= read -r
breakExit loop immediately
continueSkip to next iteration
nullglobUnmatched glob expands to nothing

Key takeaways:

  • The for loop iterates over words, not lines. for x in $list splits 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 *.txt iterates over the matching filenames, preserving spaces. for file in "*.txt" iterates once over the literal string. The guard [ -e "$file" ] || continue handles the no-match case when nullglob is not enabled.
  • Line-oriented input requires while, not for. for line in $(cat file) splits on whitespace and produces the wrong number of iterations. while IFS= read -r line; do ...; done < file reads 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-style for ((...)) form is required.
  • break and continue control the loop. break exits immediately, and continue skips to the next iteration. break N and continue N operate 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.


Was this helpful?


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!