| |

Bash Scripting 12 ๐Ÿง Indexed Arrays โ€” Declaring, appending, and iterating array elements

โฑ๏ธ Estimated read: 11 min | |

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

OperationSyntax
Create with elementsarr=(a b c)
Create emptydeclare -a arr
Read element${arr[0]}
All elements"${arr[@]}"
Element count${#arr[@]}
All indices${!arr[@]}
Appendarr+=("new")
Modifyarr[1]="new"
Removeunset 'arr[1]'
Slice"${arr[@]:start:count}"

Iteration Patterns

PatternUse 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

OperationSyntax
Pass arrayfunc "${arr[@]}"
Receive as arraylocal -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

PitfallWhy It HappensFix
Element with spaces splitsUnquoted ${arr[@]}Use "${arr[@]}"
Only first element passed$arr without subscriptUse "${arr[@]}"
Sparse array misses elementsC-style loop assumes contiguousUse "${!arr[@]}"
Unset removes the wrong elementBrackets expanded as globunset 'arr[1]'
Array lost in a functionNo local array declarationlocal -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

ItemValue
Array creationarr=(a b c)
Element read${arr[0]}
All elements"${arr[@]}"
Element count${#arr[@]}
Index list${!arr[@]}
Appendarr+=("new")
Modifyarr[i]="new"
Removeunset 'arr[i]'
Slice"${arr[@]:start:count}"
Function passfunc "${arr[@]}"
File readmapfile -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 +=. The arr+=("new") syntax adds one element to the end of the array. The arr+=("a" "b") syntax adds multiple elements in one operation. The arr[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 with local -a received=("$@"). Passing $arr without the subscript passes only the first element .
  • mapfile -t reads a file into an array. The -t flag 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.


Was this helpful?


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!