Looping through an array is a fundamental skill for anyone writing Bash or shell scripts. Even so, whether you are processing a list of filenames, iterating over user inputs, or managing configuration parameters, understanding the syntax and nuances of array iteration separates fragile scripts from solid automation tools. This guide covers everything from basic syntax to advanced techniques for handling associative arrays and complex data structures in Bash.
Understanding Bash Arrays Before Looping
Before diving into loops, Understand how Bash defines arrays — this one isn't optional. Unlike many programming languages, Bash supports two distinct types: indexed arrays (ordered lists with numeric keys starting at zero) and associative arrays (key-value pairs similar to dictionaries or hash maps in other languages) That's the part that actually makes a difference..
Declaring an indexed array is straightforward:
my_array=("apple" "banana" "cherry")
Declaring an associative array requires the declare -A command:
declare -A my_assoc_array
my_assoc_array["fruit"]="apple"
my_assoc_array["color"]="red"
A critical detail involves referencing the array. Because of that, using $my_array only expands to the first element. To access all elements, you must use the @ or * subscripts within parameter expansion: ${my_array[@]} or ${my_array[*]}. The distinction between @ and * becomes vital inside loops, particularly when array elements contain spaces.
The Standard For Loop Syntax
The most common and portable way to iterate over an indexed array in Bash is the C-style for loop or the standard for ... in loop. The standard syntax is preferred for readability and correct handling of whitespace.
Basic Iteration Over Values
To perform an action on every element, use the in keyword with the [@] expansion. Always quote the expansion ("${array[@]}") to preserve elements containing spaces as single entities.
fruits=("Green Apple" "Banana" "Cherry Pie")
for fruit in "${fruits[@]}"; do
echo "Processing: $fruit"
done
Output:
Processing: Green Apple
Processing: Banana
Processing: Cherry Pie
If you omit the quotes (for fruit in ${fruits[@]}), "Green Apple" splits into two iterations ("Green" and "Apple"), which is rarely the desired behavior.
Iterating Over Indices
Often, you need the index (position) of the element rather than just the value—perhaps to modify the original array or access a parallel array. Because of that, bash provides a specific parameter expansion syntax for this: ${! array[@]}.
fruits=("Apple" "Banana" "Cherry")
for index in "${!fruits[@]}"; do
echo "Index $index: ${fruits[$index]}"
done
Output:
Index 0: Apple
Index 1: Banana
Index 2: Cherry
This technique is indispensable when you need to unset an element during iteration or update the value in place: fruits[$index]="Modified ${fruits[$index]}".
The C-Style For Loop Alternative
Bash supports a C-style three-expression for loop (for (( init; condition; increment ))). This is useful when you need fine-grained control over the counter, such as stepping by two or counting backward.
fruits=("Apple" "Banana" "Cherry" "Date" "Elderberry")
length=${#fruits[@]}
# Iterate backwards
for (( i = length - 1; i >= 0; i-- )); do
echo "Reverse Index $i: ${fruits[i]}"
done
While powerful, this syntax is specific to Bash (and Ksh/Zsh) and is not POSIX compliant. If you are writing scripts for /bin/sh (strict POSIX mode), avoid this syntax. For modern Bash scripting (targeting Bash 4+), it is perfectly acceptable and often faster for numerical-heavy iterations because it avoids creating a temporary list of words.
Looping Through Associative Arrays
Associative arrays (hash maps) require a slightly different approach. Since they are unordered, "looping through" implies iterating over keys, values, or both.
Iterating Over Keys
Use the ${!assoc_array[@]} syntax (note the exclamation mark) to expand the list of keys.
declare -A user_info
user_info["name"]="Alex"
user_info["role"]="Developer"
user_info["shell"]="bash"
for key in "${!user_info[@]}"; do
echo "Key: $key, Value: ${user_info[$key]}"
done
Output (order may vary):
Key: name, Value: Alex
Key: role, Value: Developer
Key: shell: bash
Iterating Over Values Only
If you only need values and do not care about keys, use the standard ${assoc_array[@]} expansion.
for value in "${user_info[@]}"; do
echo "Value: $value"
done
Remember that associative arrays have no guaranteed order. If you need sorted output, pipe the keys through sort:
for key in $(printf '%s\n' "${!user_info[@]}" | sort); do
echo "$key -> ${user_info[$key]}"
done
Handling Multi-line Strings and Complex Data
A common pitfall occurs when array elements contain newlines or complex whitespace. Which means the for loop splits input based on the IFS (Internal Field Separator) variable, but the "${array[@]}" expansion protects elements from word splitting after expansion. On the flip side, if you populate an array from a command output (like mapfile or readarray), you must ensure the delimiter is handled correctly.
Using mapfile (or readarray) for Safe Population
Instead of command substitution array=($(command))—which splits on whitespace and globbing—use mapfile (available in Bash 4+). It reads lines directly into an array index No workaround needed..
# Read lines from a file or command into an array
mapfile -t file_lines < <(ls -1 /etc)
for line in "${file_lines[@]}"; do
echo "Config file: $line"
done
The -t flag strips the trailing newline delimiter, keeping the data clean.
The While Loop with read for Streaming Data
For extremely large datasets or streams where loading everything into memory is inefficient, a while read loop is superior. This avoids creating the array in memory entirely.
# Process a file line-by-line without loading it all into RAM
while IFS= read -r line; do
echo "Processing line: $line"
done < large_log_file.txt
If you must build an array first but want to process it in a while loop (perhaps to modify the array structure dynamically), you can feed the array expansion into the loop via a here-string or process substitution, though a for loop is usually cleaner for in-memory arrays That's the part that actually makes a difference..
# Less common pattern: feeding array to while loop
arr=("one" "two" "three")
printf '%s\n' "${arr[@]}" | while IFS= read -r item; do
echo "Item: $item"
done
Warning: The pipe | creates a subshell. Any variable modifications inside the while loop (e.g., counter=$((counter+1))) will be lost after the loop finishes. Use for loops for in-memory arrays to avoid this scope trap And that's really what it comes down to..
Advanced Techniques and Best Practices
1. Modifying Arrays During Iteration
Modifying an array (adding/removing elements) while iterating over it with a for loop is dangerous because the iterator relies on the array's state
at the moment the loop starts. In Bash, the for item in "${arr[@]}" construct expands the entire list of elements before the first iteration runs. That said, if you unset elements or append new ones inside the loop, the iterator will not see the new elements, and unsetting elements already iterated over is harmless but unsetting future elements can lead to index confusion if you are using index-based iteration (for i in "${! arr[@]}").
Safe Pattern: Filter into a New Array Instead of mutating the array in-place, build a new array containing only the desired elements.
declare -a filtered=()
for item in "${original[@]}"; do
# Example: keep only non-empty lines that don't start with '#'
[[ -n "$item" && ! "$item" =~ ^# ]] && filtered+=("$item")
done
original=("${filtered[@]}") # Replace original if needed
Safe Pattern: Iterate Backwards for Deletion (Index-Based) If you must delete by index (e.g., removing specific indices stored in a separate list), iterate backwards so that removing an element doesn't shift the indices of elements yet to be processed.
# Indices to delete (unsorted)
to_delete=(2 5 1)
# Sort indices descending
IFS=