Linux Basic — Looping Array Elements
Looping Array Elements
Arrays + for is the bread and butter of every Bash script. Done right, it handles any value cleanly. Done wrong, it breaks on whitespace and metacharacters.
The Three Loop Forms
1. By value
fruits=(apple banana cherry)
for f in "${fruits[@]}"; do
echo "[$f]"
done
# [apple]
# [banana]
# [cherry]
Always quote, always use @. The quotes prevent word-splitting; @ (not *) keeps each element separate.
2. By index
for i in "${!fruits[@]}"; do
echo "$i → ${fruits[$i]}"
done
# 0 → apple
# 1 → banana
# 2 → cherry
Use this when you need the position, or when the array might be sparse (and for f in "${arr[@]}" would silently skip holes by value):
arr=(a b c d)
unset 'arr[1]'
for f in "${arr[@]}"; do echo "$f"; done
# a
# c
# d ← no clue index 1 was missing
for i in "${!arr[@]}"; do echo "$i $f"; done
# 0 a
# 2 c
# 3 d ← positions visible
3. C-style
for ((i=0; i<${#fruits[@]}; i++)); do
echo "$i → ${fruits[$i]}"
done
Use C-style when you need arithmetic on the index — steps of 2, counting down, etc.:
for ((i=${#fruits[@]}-1; i>=0; i--)); do
echo "${fruits[$i]}" # reversed
done
Loop Over an Associative Array
declare -A cfg=([host]=example.com [port]=8080 [debug]=true)
for k in "${!cfg[@]}"; do
printf '%-8s = %s\n' "$k" "${cfg[$k]}"
done
No order guarantee — sort the keys if you need predictable output:
for k in $(printf '%s\n' "${!cfg[@]}" | sort); do
printf '%-8s = %s\n' "$k" "${cfg[$k]}"
done
Parallel Iteration — Two Related Arrays
When two arrays share indexes (e.g. names + ages):
names=(alice bob carol)
ages=( 30 25 40)
for i in "${!names[@]}"; do
printf '%-6s is %d\n' "${names[$i]}" "${ages[$i]}"
done
# alice is 30
# bob is 25
# carol is 40
Bash has no native zip. The index loop is the idiomatic form.
For a hash-table style join across two unrelated arrays, build an associative array:
names=(alice bob carol)
ages=(30 25 40)
declare -A age_of
for i in "${!names[@]}"; do
age_of[${names[$i]}]=${ages[$i]}
done
echo "${age_of[bob]}" # 25
break and continue
for f in "${files[@]}"; do
[[ "$f" == .* ]] && continue # skip hidden
[[ "$f" == STOP ]] && break # stop on marker
process "$f"
done
break N / continue N break or continue out of N levels of nested loops:
for outer in 1 2 3; do
for inner in a b c; do
if [[ "$inner" == b ]]; then
continue 2 # back to outer loop
fi
echo "$outer $inner"
done
done
# 1 a
# 2 a
# 3 a
for x in {1..N} — Range Loops
for i in {1..5}; do
echo "$i"
done
# 1 2 3 4 5
for i in {10..1..2}; do # 10..1 step 2 (descending)
echo "$i"
done
# 10 8 6 4 2
for c in {a..f}; do
echo "$c"
done
# a b c d e f
Brace expansion happens before variable expansion. So this does not work:
n=10
for i in {1..$n}; do echo $i; done
# Output: {1..10} ← literal, not expanded
Use C-style for variable bounds:
for ((i=1; i<=n; i++)); do echo "$i"; done
Or seq (Linux) / jot (BSD):
for i in $(seq 1 "$n"); do echo "$i"; done
A Real Example — Process Files in Batches
files=( *.csv )
# Process in batches of 5
batch_size=5
for ((i=0; i<${#files[@]}; i+=batch_size)); do
batch=( "${files[@]:i:batch_size}" )
echo "Batch starting at $i:"
printf ' %s\n' "${batch[@]}"
# process_batch "${batch[@]}"
done
Slicing inside an arithmetic-driven loop — the C-style for shines here.
Common Mistakes
| ❌ Wrong | ✅ Right | Issue |
|---|---|---|
for x in ${arr[@]} (unquoted) |
for x in "${arr[@]}" |
unquoted splits on IFS |
for x in ${arr[*]} |
for x in "${arr[@]}" |
* joins; @ separates |
for ((i=0; i<${#arr}; i++)) |
for ((i=0; i<${#arr[@]}; i++)) |
${#arr} is byte-length of element 0 |
for i in {1..$n} |
for ((i=1; i<=n; i++)) |
brace expansion can’t see variables |
| Iterating sparse array by value | use for i in "${!arr[@]}" |
by-value loop hides the holes |
Next → Looping Directory Listings