Chapter 05Lesson 01~60 minutes

for Loops over Words, Files, and Sequences

A for loop is Bash's most direct way to repeat work over a known collection. The subtlety is that Bash iterates over shell words produced by expansion, so quoting and collection design determine whether your loop is reliable.

BeginnerLoops & functionsHands-on lab

Learning objectives

By the end of this lesson

  • Explain how a Bash for loop obtains its iteration words.
  • Iterate safely over quoted arrays and filename globs.
  • Use arithmetic for loops for runtime numeric counters.
  • Avoid fragile command-substitution and ls-parsing loops.
  • Use break and continue deliberately.

1. A for loop iterates over shell words

The basic form is for name in words; do ...; done. Each resulting word becomes one iteration.

for environment in dev staging prod; do
  printf 'environment=%s\n' "$environment"
done
for-loop flow
flowchart LR
  W["word list"] --> A["dev"]
  A --> B["staging"]
  B --> C["prod"]
  C --> D["loop complete"]

2. Quoting determines element boundaries

Unquoted variable expansion can split one value into several words and can also trigger pathname expansion. Quote data unless you intentionally want those transformations.

value="release candidate"

for item in "$value"; do
  printf 'quoted=<%s>\n' "$item"
done

for item in $value; do
  printf 'unquoted=<%s>\n' "$item"
done
Default rule

Do not use unquoted expansion as an informal list format. Use arrays or a structured stream.

3. Arrays preserve in-memory collection boundaries

For Bash-native collections, arrays are usually the safest source for a loop.

services=("api gateway" "worker" "cache*")

for service in "${services[@]}"; do
  printf 'service=<%s>\n' "$service"
done

"${services[@]}" expands to one quoted word per array element, even when an element contains whitespace or wildcard characters.

4. Use globs when pathname expansion is the desired operation

A glob lets Bash enumerate matching paths directly. Enable nullglob when zero matches should mean zero iterations.

shopt -s nullglob
for file in ./logs/*.log; do
  printf 'processing %s\n' "$file"
done
shopt -u nullglob
Do not parse ls

ls is presentation output. Filenames can contain spaces, tabs, newlines, and other characters that make text parsing ambiguous.

5. for name is shorthand for iterating over "$@"

Inside functions or scripts, omitting the in list iterates over positional parameters while preserving argument boundaries.

show_args() {
  local arg
  for arg; do
    printf 'arg=<%s>\n' "$arg"
  done
}

show_args "one" "two words" "*.log"

6. Use arithmetic for loops for dynamic counters

When iteration is fundamentally numeric, Bash's arithmetic form is clearer than manufacturing a string sequence.

limit=5
for ((i=1; i<=limit; i++)); do
  printf 'attempt=%d\n' "$i"
done

This works with runtime values. Brace expansion such as {1..5} is useful for literal ranges but is not the right tool for a variable upper bound.

7. Brace expansion creates literal sequences before parameter expansion

for n in {1..5}; do
  printf '%s\n' "$n"
done

# Do not expect this to use runtime $limit:
# for n in {1..$limit}; do ...; done

Understanding expansion order prevents scripts that look dynamic but are actually generating literal text.

8. Avoid for loops over arbitrary command output

This common pattern destroys record boundaries:

# Fragile:
# for file in $(find . -type f); do
#   ...
# done

Command substitution plus unquoted expansion performs word splitting and glob expansion. Use globs, arrays, or a safe while read stream instead.

9. break and continue control iteration

for env in dev staging prod; do
  [[ $env == dev ]] && continue

  printf 'checking %s\n' "$env"

  if [[ $env == prod ]]; then
    break
  fi
done

continue skips the current iteration; break exits the loop. If nested loops need numeric break levels, consider whether functions would make the structure clearer.

10. Define what a failed item means

failed=0

for service in api worker cache; do
  if deploy_service "$service"; then
    printf 'deployed %s\n' "$service"
  else
    printf 'failed %s\n' "$service" >&2
    ((failed += 1))
  fi
done

(( failed == 0 )) || exit 1
Batch semantics

A loop should state whether one failed item aborts the batch, is accumulated, or is retried. Do not leave this to accidental shell-option behavior.

11. Hands-on lab: iterate artifact paths safely

mkdir -p "$HOME/devops-academy/bash/chapter05/lesson01/artifacts"
cd "$HOME/devops-academy/bash/chapter05/lesson01"

printf 'a\n' > "artifacts/api build.tar"
printf 'b\n' > "artifacts/worker.tar"
printf 'c\n' > "artifacts/cache.tar"

shopt -s nullglob
files=(artifacts/*.tar)

printf 'found=%d\n' "${#files[@]}"

for file in "${files[@]}"; do
  bytes=$(wc -c < "$file")
  printf 'artifact=%s bytes=%s\n' "$file" "$bytes"
done

shopt -u nullglob

Verification checklist

12. Knowledge check

Question 1. What does "${array[@]}" produce?

Question 2. Why is for file in $(ls) unsafe?

Question 3. When should you prefer for ((...))?

Question 4. What does continue do?

13. Summary

Bash for loops operate on shell words. Safe iteration therefore depends on preserving boundaries: quoted arrays for in-memory values, direct globs for paths, positional parameters for arguments, and arithmetic loops for counters.

14. Further reading

  • GNU Bash Reference Manual — Looping Constructs.
  • GNU Bash Reference Manual — Arrays and Filename Expansion.
  • GNU Bash Reference Manual — Brace Expansion.
  • ShellCheck documentation — iteration and word-splitting diagnostics.
Next lesson

while and until Loops

Continue Chapter 5 by building the next layer of reusable shell logic.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.