Flow Control: Looping With For

3 min read

Core idea

for is bash's sequence-processing loop. It comes in two complementary flavours: the traditional for VAR in LIST; do … done, which walks a list of words one at a time, and the C-style for ((init; cond; step)); do … done, which counts numerically. Where while loops on a condition, for loops on a known collection — and that collection can come from brace expansion, pathname expansion, command substitution, or the positional parameters.

Why it matters

"Do this once per thing" is the most common loop shape in shell programming: per file in a directory, per argument on the command line, per line of output from another command. for matches that shape directly, and its tight integration with shell expansion is what makes one-liners like for f in *.log; do gzip "$f"; done natural. The C-style form fills the remaining gap — numeric counters, fixed iteration counts, and the rare time you need an integer index. Together they cover essentially every iteration pattern that isn't a stream of unknown length (which is while read's job).

Mental model

Two forms, one keyword

The two for flavours look different but share intent — they both bind a variable to a sequence of values and run the body once per value. The first form expresses what values; the second expresses how to enumerate them.

Two forms, one keyword

Where the list comes from

The traditional for is powerful because bash hands it the result of any expansion. The four common sources:

  • Literalfor color in red green blue. Static and explicit.
  • Bracefor n in {01..12}. Generates the sequence at parse time.
  • Pathname (glob)for f in *.mdx. Filename expansion; results sorted by the locale.
  • Command substitutionfor line in $(grep -l TODO *.md). Splits on $IFS.

The fifth, less obvious source is the positional parameters themselves. Drop in LIST entirely and for x iterates over "$@" — and does so with the same quoting safety you'd want.

Quoting and IFS — the silent foot-gun

Command substitution is the only source that involves word-splitting after expansion. If a filename contains a space, for f in $(ls) will treat it as two words. The fix is almost never "quote the substitution" — quoting collapses the result to one word. The fix is to switch to pathname expansion (for f in *), avoid ls for scripting altogether, or use a while read loop with IFS= and -r. Reach for find … -print0 | xargs -0 if you need to traverse arbitrary trees.

Practical application

  1. Pick the form first. If you have a list of things, traditional. If you have a number of times, C-style. Mixing them muddies the intent.

  2. Prefer globs over command substitution. for f in *.log is safer than for f in $(ls *.log) because globs preserve filenames verbatim — no IFS splitting, no surprises with spaces.

  3. Guard against empty globs. When a glob matches nothing, bash returns the glob itself. Either set shopt -s nullglob once at the top of the script, or test [[ -e "$f" ]] inside the loop.

  4. Quote the iteration variable. Use "$f" (not $f) in every body line. This is non-negotiable for paths.

  5. Use C-style for numeric work. for ((i=0; i<${#arr[@]}; i++)) is the canonical "walk an array by index" pattern.

Example

Suppose we want to rename a batch of dated photos into a year/month folder layout. The traditional form pulls files via a glob; the C-style form would be overkill. We use brace expansion to scaffold the destination directories first:

#!/usr/bin/env bash
shopt -s nullglob

# Pre-create the year/month dirs we'll need this year.
for m in {01..12}; do
  mkdir -p "2026/$m"
done

# Move each timestamped photo into its bucket.
for photo in IMG_2026*.jpg; do
  [[ -e "$photo" ]] || continue
  # Filename shape: IMG_YYYYMMDD_HHMMSS.jpg
  month=${photo:8:2}
  mv -- "$photo" "2026/$month/"
done

# C-style loop: pad the month dirs with a README listing their count.
for ((i=1; i<=12; i++)); do
  m=$(printf '%02d' "$i")
  count=$(ls -1 "2026/$m" 2>/dev/null | wc -l)
  echo "Month $m: $count photo(s)" >"2026/$m/README.txt"
done

Three loops, three different sources: brace expansion for the calendar, glob expansion for the photos, and C-style counting for the summary pass. Each form matches its source exactly.

Continue exploring

Tags