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.
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"
doneflowchart 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"
doneDo 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 nullglobls 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"
doneThis 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 ...; doneUnderstanding 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
# ...
# doneCommand 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
donecontinue 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 1A 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 nullglobVerification 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.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this address.