Bash Scripting 12 ๐ง Indexed Arrays โ Declaring, appending, and iterating array elements
An indexed array is a variable that holds multiple values under a single name. Each value is accessed by a numeric index, and the indices are not required to be contiguous. Bash arrays are not a separate type; they are a Bash feature that extends the variable syntax with subscripts. The previous chapters covered scalars and positional parameters; this chapter covers the array type: how to declare it, how to append to it, how to iterate over its elements, and how to pass it to a function without losing the element boundaries.
The array syntax is built on parameter expansion. ${arr[0]} reads the first element, ${arr[@]} expands to all elements, ${#arr[@]} is the number of elements, and ${!arr[@]} is the list of indices. The distinction between "${arr[@]}" and "${arr[*]}" is the same as the distinction between "$@" and "$*": the first preserves the element boundaries, and the second joins the elements with the first character of IFS. The quoted form is almost always the correct choice.
Key point: Bash arrays are zero-indexed and can be sparse. The ${arr[@]} expansion, when quoted, produces one word per element, which preserves spaces inside the elements. The ${arr[*]} expansion joins the elements with the first character of IFS. The for element in "${arr[@]}" loop is the correct way to iterate, and for index in "${!arr[@]}" iterates over the indices when the index is needed. Associative arrays, which use string keys, are a separate feature covered in the next chapter.
Why arrays require the quoted expansion
The word-splitting problem.
An array element can contain spaces. A filename like my file.txt is a single element, but the unquoted $arr expansion splits on whitespace, producing two iterations. The quoted "${arr[@]}" expansion produces one word per element, so the filename remains intact. This is the same distinction as the "$@" positional-parameter expansion, and the quoted form is almost always what the script needs .
The sparse-array problem.
Bash arrays are not required to have contiguous indices. arr[0]=a; arr[5]=b is a valid array with two elements and indices 0 and 5. The for i in "${!arr[@]}" loop iterates over the actual indices, while for ((i=0; i<${#arr[@]}; i++)) assumes contiguous indices and would miss element 5. When the array is sparse, the index-based loop is incorrect, and the ${!arr[@]} expansion is the correct way to enumerate the indices .
The function-argument problem.
Passing an array to a function requires "${arr[@]}", and the function receives the elements as "$@". The quoted form preserves the element boundaries, and the function can iterate with for item in "$@". Passing $arr without the subscript passes only the first element. Passing "${arr[*]}" joins the elements into a single string, which loses the boundaries.
a. Declaring and initializing arrays
An indexed array is created by assigning to a subscripted variable. The declare -a command declares an array explicitly, and the compound assignment arr=(a b c) creates an array with the given elements.
#!/usr/bin/env bash
# Compound assignment: three elements
fruits=("apple" "banana" "cherry")
# Individual assignment
colors[0]="red"
colors[1]="green"
colors[2]="blue"
# Explicit declaration
declare -a numbers
numbers=(1 2 3 4 5)
# Assignment with indices
sparse[0]="first"
sparse[5]="sixth"
# Read a specific element
echo "${fruits[0]}" # apple
echo "${fruits[1]}" # banana
The fruits=(...) compound assignment creates the array with three elements at indices 0, 1, and 2. The colors[0]=... assignments create the array one element at a time. The declare -a numbers declaration creates an empty array, and the subsequent assignment fills it. The sparse array has indices 0 and 5, with no elements in between .
The array length and the list of indices are available through parameter expansion.
# Number of elements
echo "${#fruits[@]}" # 3
# List of indices
echo "${!fruits[@]}" # 0 1 2
# All elements as separate words
printf '%s\n' "${fruits[@]}"
# All elements joined by the first IFS character
echo "${fruits[*]}" # apple banana cherry
The ${#fruits[@]} expansion is the number of elements, not the highest index. For the sparse array, ${#sparse[@]} is 2, even though the highest index is 5 .
b. Appending and modifying elements
Elements can be appended with the += operator, which adds one element to the end of the array, or with an explicit index assignment.
#!/usr/bin/env bash
# Append with +=
fruits=("apple" "banana")
fruits+=("cherry")
# fruits is now (apple banana cherry)
# Append multiple elements
fruits+=("date" "elderberry")
# fruits is now (apple banana cherry date elderberry)
# Modify an element
fruits[1]="blueberry"
# fruits is now (apple blueberry cherry date elderberry)
# Remove an element
unset 'fruits[1]'
# fruits has indices 0, 2, 3, 4; length is 4
The fruits+=("cherry") syntax appends one element. The fruits+=("date" "elderberry") syntax appends multiple elements in one operation. The fruits[1]="blueberry" assignment modifies the existing element. The unset 'fruits[1]' removes the element, leaving a gap in the indices . The array is now sparse, and the ${!fruits[@]} expansion shows the remaining indices, not a contiguous range.
The unset command requires the subscript to be quoted, or the shell tries to expand the brackets as a glob pattern. unset fruits[1] may work in some shells, but unset 'fruits[1]' is the portable form.
c. Iterating over array elements
The two iteration patterns are over the elements and over the indices. The element loop is the correct choice when the script does not need the index.
#!/usr/bin/env bash
fruits=("apple" "banana" "cherry")
# Iterate over elements: preserves spaces
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
# Iterate over indices: needed when the index matters
for i in "${!fruits[@]}"; do
echo "Index $i: ${fruits[$i]}"
done
# C-style loop: only correct for contiguous indices
for ((i = 0; i < ${#fruits[@]}; i++)); do
echo "Position $i: ${fruits[i]}"
done
The for fruit in "${fruits[@]}" loop iterates over the elements and preserves the spaces inside each element. The for i in "${!fruits[@]}" loop iterates over the indices, and ${fruits[$i]} reads the element at each index. The C-style loop assumes the indices are contiguous from 0 to the length minus one, which is true for a freshly created array but not for a sparse one . For a sparse array, the C-style loop would read empty elements at the missing indices and miss the elements after the gap.
The "${fruits[@]}" expansion inside the loop body can be used to pass the entire array to a command.
# Pass the entire array to a command
printf '%s\n' "${fruits[@]}"
# Pass the array to a function
process_fruits() {
local -a received=("$@")
for fruit in "${received[@]}"; do
echo "Processing: $fruit"
done
}
process_fruits "${fruits[@]}"
The local -a received=("$@") line creates a local array from the function’s arguments. The "$@" inside the function is the list of arguments that were passed, and "${fruits[@]}" at the call site passes the array elements as separate arguments. The quoted form is what preserves the boundaries .
Complete Example Session
#!/usr/bin/env bash
# ============================================
# PART 1: DECLARE AN ARRAY
# ============================================
servers=("web1.example.com" "web2.example.com" "db1.example.com")
# ============================================
# PART 2: LENGTH AND INDICES
# ============================================
echo "Number of servers: ${#servers[@]}"
echo "Indices: ${!servers[@]}"
# ============================================
# PART 3: ITERATE OVER ELEMENTS
# ============================================
for server in "${servers[@]}"; do
echo "Checking $server"
done
# ============================================
# PART 4: ITERATE OVER INDICES
# ============================================
for i in "${!servers[@]}"; do
echo "Server $((i + 1)): ${servers[$i]}"
done
# ============================================
# PART 5: APPEND ELEMENTS
# ============================================
servers+=("cache1.example.com")
echo "After append: ${#servers[@]} servers"
# ============================================
# PART 6: MODIFY AN ELEMENT
# ============================================
servers[2]="db2.example.com"
echo "Modified: ${servers[2]}"
# ============================================
# PART 7: REMOVE AN ELEMENT
# ============================================
unset 'servers[1]'
echo "After unset: ${#servers[@]} servers"
echo "Remaining indices: ${!servers[@]}"
# ============================================
# PART 8: SLICE THE ARRAY
# ============================================
# Elements from index 0, two elements
subset=("${servers[@]:0:2}")
printf 'Subset: %s\n' "${subset[@]}"
# ============================================
# PART 9: PASS TO A FUNCTION
# ============================================
check_all() {
local -a hosts=("$@")
for host in "${hosts[@]}"; do
echo "Pinging $host"
# ping -c 1 -W 1 "$host" > /dev/null 2>&1 && echo " UP" || echo " DOWN"
done
}
check_all "${servers[@]}"
# ============================================
# PART 10: READ INTO AN ARRAY
# ============================================
# Read lines from a file into an array
mapfile -t lines < /etc/hostname
echo "Read ${#lines[@]} lines"
for line in "${lines[@]}"; do
echo "Line: $line"
done
The ten parts demonstrate the array surface: declaration, length, index enumeration, element iteration, append, modify, unset, slice, function passing, and file reading. The critical pattern is Part 8: the "${servers[@]:0:2}" slice takes two elements starting at index 0, and the quoted form preserves the element boundaries in the resulting array.
Quick Reference
Array Operations
| Operation | Syntax |
|---|---|
| Create with elements | arr=(a b c) |
| Create empty | declare -a arr |
| Read element | ${arr[0]} |
| All elements | "${arr[@]}" |
| Element count | ${#arr[@]} |
| All indices | ${!arr[@]} |
| Append | arr+=("new") |
| Modify | arr[1]="new" |
| Remove | unset 'arr[1]' |
| Slice | "${arr[@]:start:count}" |
Iteration Patterns
| Pattern | Use Case |
|---|---|
for x in "${arr[@]}" | Iterate over elements |
for i in "${!arr[@]}" | Iterate over indices |
for ((i=0; i<${#arr[@]}; i++)) | Contiguous indices only |
printf '%s\n' "${arr[@]}" | Print one per line |
Passing to Functions
| Operation | Syntax |
|---|---|
| Pass array | func "${arr[@]}" |
| Receive as array | local -a received=("$@") |
| Receive as positional | "$@" inside the function |
Best Practices
โ Do This:
# Quote the expansion
for x in "${arr[@]}"; do echo "$x"; done # โ
# Use the index expansion for sparse arrays
for i in "${!arr[@]}"; do echo "${arr[$i]}"; done # โ
# Quote the unset subscript
unset 'arr[1]' # โ
# Append with +=
arr+=("new element") # โ
# Pass arrays to functions with quotes
func "${arr[@]}" # โ
# Use mapfile for file input
mapfile -t lines < file # โ
โ Don’t Do This:
# Unquoted expansion splits elements with spaces
for x in ${arr[@]}; do echo "$x"; done # โ
# Pass the array without the subscript
func "$arr" # Passes only the first element # โ
# Use ${arr[*]} when boundaries matter
for x in "${arr[*]}"; do ...; done # โ
# Use the C-style loop on a sparse array
for ((i=0; i<${#arr[@]}; i++)); do ...; done # โ
# Unset without quoting
unset arr[1] # May be interpreted as a glob # โ
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Element with spaces splits | Unquoted ${arr[@]} | Use "${arr[@]}" |
| Only first element passed | $arr without subscript | Use "${arr[@]}" |
| Sparse array misses elements | C-style loop assumes contiguous | Use "${!arr[@]}" |
| Unset removes the wrong element | Brackets expanded as glob | unset 'arr[1]' |
| Array lost in a function | No local array declaration | local -a received=("$@") |
Real-World Examples
1. Create an array
arr=("one" "two" "three")
2. Length
echo "${#arr[@]}"
3. Indices
echo "${!arr[@]}"
4. Iterate over elements
for x in "${arr[@]}"; do echo "$x"; done
5. Iterate over indices
for i in "${!arr[@]}"; do echo "$i: ${arr[$i]}"; done
6. Append
arr+=("four")
7. Modify
arr[0]="zero"
8. Remove
unset 'arr[1]'
9. Slice
echo "${arr[@]:1:2}"
10. Read lines into an array
mapfile -t lines < file
Visual
Array Indexing
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ INDICES ARE NOT REQUIRED TO BE CONTIGUOUS โ
โ โ
โ arr=("a" "b" "c") โ
โ โโโโโโโโโฌโโโโโโโโฌโโโโโโโโ โ
โ โ a โ b โ c โ โ
โ โ [0] โ [1] โ [2] โ โ
โ โโโโโโโโโดโโโโโโโโดโโโโโโโโ โ
โ Length: 3 Indices: 0 1 2 โ
โ โ
โ unset 'arr[1]' โ
โ โโโโโโโโโฌโโโโโโโโฌโโโโโโโโ โ
โ โ a โ โ c โ โ
โ โ [0] โ โ [2] โ โ
โ โโโโโโโโโดโโโโโโโโดโโโโโโโโ โ
โ Length: 2 Indices: 0 2 โ
โ โ
โ The index-based loop assumes 0 and 1 and misses [2]. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Quoted vs Unquoted
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ QUOTING PRESERVES ELEMENT BOUNDARIES โ
โ โ
โ arr=("my file" "your file") โ
โ โ
โ Unquoted ${arr[@]}: โ
โ for x in ${arr[@]} โ my, file, your, file (4 iterations) โ
โ โ
โ Quoted "${arr[@]}": โ
โ for x in "${arr[@]}" โ "my file", "your file" (2 iterations)โ
โ โ
โ The quoted form is the correct choice. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Iteration Patterns
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THREE WAYS TO ITERATE โ
โ โ
โ 1. Over elements: โ
โ for x in "${arr[@]}"; do ...; done โ
โ โ x is the element โ
โ โ
โ 2. Over indices: โ
โ for i in "${!arr[@]}"; do ...; done โ
โ โ i is the index, ${arr[$i]} is the element โ
โ โ
โ 3. C-style: โ
โ for ((i=0; i<${#arr[@]}; i++)); do ...; done โ
โ โ assumes contiguous indices โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Summary
| Item | Value |
|---|---|
| Array creation | arr=(a b c) |
| Element read | ${arr[0]} |
| All elements | "${arr[@]}" |
| Element count | ${#arr[@]} |
| Index list | ${!arr[@]} |
| Append | arr+=("new") |
| Modify | arr[i]="new" |
| Remove | unset 'arr[i]' |
| Slice | "${arr[@]:start:count}" |
| Function pass | func "${arr[@]}" |
| File read | mapfile -t arr < file |
Key takeaways:
- Quote the
"${arr[@]}"expansion. The quoted form preserves the element boundaries, so an element with spaces is one iteration, not several. The unquoted form splits on whitespace, and"${arr[*]}"joins the elements into a single string . - Iterate over indices with
"${!arr[@]}". The C-style loop assumes contiguous indices, which is false for a sparse array. The index expansion enumerates the actual indices, so the loop reads every element regardless of the gaps. - Append with
+=. Thearr+=("new")syntax adds one element to the end of the array. Thearr+=("a" "b")syntax adds multiple elements in one operation. Thearr[i]="new"syntax modifies an existing element. - Unset requires quoting. The
unset 'arr[1]'form prevents the shell from interpreting the brackets as a glob. The unquoted form may remove the wrong element or fail silently. - Pass arrays to functions with
"${arr[@]}". The function receives the elements as"$@"and can recreate the array withlocal -a received=("$@"). Passing$arrwithout the subscript passes only the first element . mapfile -treads a file into an array. The-tflag strips the trailing newline from each line. The resulting array has one element per line, and the quoted expansion preserves the line contents.
Remember: An indexed array is a variable with subscripts. The "${arr[@]}" expansion is the correct way to iterate and to pass the array, because it preserves the element boundaries. The "${!arr[@]}" expansion is the correct way to enumerate the indices when the array is sparse or the index is needed. Append with +=, modify with an index assignment, and remove with a quoted unset. The array is the construct for a list of values, and the quoted expansion is what keeps the list intact.
Stop using slow, ad-bloated tool sites! ๐คฎ
๐ Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
โ
Finance (Mortgage, Interest, Inflation)
โ
Tech (Base64, JSON, Dev Suite, IP)
โ
Health (BMI, BMR, TDEE)
โ
Productivity (Timer, Workspace, QR)
โก๏ธ Fast & Private
๐ No data leaves your device
๐ 100% Free
๐ Use it now: https://tools.kandz.me
๐ Bookmark itโyouโll need it later!