Linux Basic — Looping Array Elements

3 minute read

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

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

Updated: