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"
done
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
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
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
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.
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.