Bash Arrays: Indexed and Associative Arrays

Bash Arrays: Indexed and Associative Arrays

What You'll Learn

  • The difference between bash indexed arrays and associative arrays
  • The core operations: declaring, accessing, looping, and adding/removing elements
  • How to avoid classic traps like missing quotes on "${arr[@]}" and sparse arrays

Quick Summary

  • Ordered list of values -> indexed array (arr=(a b c))
  • Key/value lookup table -> associative array (declare -A map is required)
  • Always double-quote "${arr[@]}" when looping or expanding

Prerequisites (target environment)

  • Shell: bash 4.0+ (associative arrays need 4.0+, negative indices need 4.3+)
  • sh / dash do not support arrays. Use an explicit #!/bin/bash shebang

What is an array?

Conclusion: An array stores multiple values in one variable. Bash has two kinds: ordered indexed arrays and key-based associative arrays.

A plain shell variable holds a single value. When you need to handle several filenames or usernames together, cramming them into one space-separated string breaks as soon as a value contains a space. An array keeps each element as an independent, safely-quoted value.

Bash offers two array types:

  • Indexed array: elements are addressed by integer indices starting at 0
  • Associative array: elements are looked up by an arbitrary string key (like a hash or dictionary)

How do you use indexed arrays?

Conclusion: Declare with arr=(a b c), read one element with ${arr[0]}, and all elements with "${arr[@]}". Omitting the index refers to the first element.

Declaring and assigning

# Declare all at once
fruits=(apple banana cherry)

# Assign by index
fruits[3]=durian

# Append to the end (+= operator)
fruits+=(elderberry fig)

Accessing

echo "${fruits[0]}"      # apple (first element)
echo "${fruits[-1]}"     # fig (last, bash 4.3+)
echo "${fruits[@]}"      # all elements, space-separated
echo "${#fruits[@]}"     # element count (5)
echo "${!fruits[@]}"     # all indices (0 1 2 3 4 5)

Referencing an array without an index, like $fruits, is the same as ${fruits[0]} (first element only). Watch for this mix-up when you meant to use every element.

Slicing (taking a subset)

echo "${fruits[@]:1:2}"  # banana cherry (2 elements from index 1)

How do you use associative arrays?

Conclusion: Always declare an associative array with declare -A before use. Skip it and bash treats it as an indexed array, collapsing keys to 0 and corrupting the data.

An associative array lets you use arbitrary strings as keys. The key difference from indexed arrays is that you must declare it with declare -A first.

declare -A capital          # forget this and it breaks
capital[japan]=Tokyo
capital[france]=Paris
capital["united states"]=Washington

echo "${capital[japan]}"    # Tokyo
echo "${!capital[@]}"       # all keys (order is undefined)
echo "${capital[@]}"        # all values (order is undefined)
echo "${#capital[@]}"       # element count (3)

Associative arrays do not preserve order. Neither insertion order nor key order is guaranteed on output, so pipe through sort if you need ordering.

How do you loop over an array?

Conclusion: Loop values with for x in "${arr[@]}", and keys/indices with for k in "${!arr[@]}". Both require double quotes.

# Process each value in turn
for f in "${fruits[@]}"; do
    echo "fruit: $f"
done

# Loop keys (associative) and look up values
for country in "${!capital[@]}"; do
    echo "$country -> ${capital[$country]}"
done

Dropping the quotes, as in for f in ${fruits[@]}, causes word splitting on elements that contain spaces: a single element apple pie splits into apple and pie. Always use "${arr[@]}" to keep values intact.

What is the difference between "${arr[@]}" and "${arr[*]}"?

Conclusion: [@] expands each element as a separate word; [*] joins them into one string using the first character of IFS. Use [@] for loops.

Notation Expands to Typical use
"${arr[@]}" one separate word per element loops, passing args
"${arr[*]}" a single string joined by IFS[0] display, stringify
arr=(one two three)

printf '[%s]\n' "${arr[@]}"   # [one] [two] [three] on separate lines
printf '[%s]\n' "${arr[*]}"   # [one two three] on one line

# Join with a custom separator
IFS=,
echo "${arr[*]}"              # one,two,three
unset IFS

How do you add or remove elements?

Conclusion: Append with arr+=(...) and delete with unset 'arr[2]'. Note that unsetting from an indexed array leaves it sparse - indices are not renumbered.

arr=(a b c d)

arr+=(e)            # append -> a b c d e
unset 'arr[1]'      # delete index 1 (b)

echo "${arr[@]}"    # a c d e
echo "${!arr[@]}"   # 0 2 3 4 <- index 1 is missing (not renumbered)

After unset, the array is no longer contiguous (a sparse array). The count ${#arr[@]} no longer matches the highest index, so any loop assuming indices 0..N-1 breaks. Reassign to renumber:

arr=("${arr[@]}")   # reindex to 0,1,2...

What are the common pitfalls?

Conclusion: Arrays are bash-only, quoting is mandatory, and associative arrays need declare -A. Miss any of these and you get a classic failure.

To capture command output into an array, mapfile (also called readarray) reads safely line by line.

mapfile -t lines < <(ls -1)   # each line becomes one element of lines
echo "${#lines[@]}"           # number of lines

The -t option strips the trailing newline from each line; without it, elements keep a \n.

Summary

  • Indexed arrays arr=(a b c) are ordered; associative arrays declare -A map are key-based
  • All elements: "${arr[@]}", count: ${#arr[@]}, indices/keys: ${!arr[@]}
  • Always double-quote in loops and expansions to avoid word-splitting accidents
  • Associative arrays require declare -A, and arrays go sparse after unset

Next reading: