$linuxjunkies
>

Bash Arrays and Associative Arrays

Master bash indexed and associative arrays: declaration, element access, looping, mapfile, namerefs, and practical patterns for real scripting work.

IntermediateUbuntuDebianFedoraArch9 min readUpdated June 7, 2026

Before you start

  • Bash 4.0 or later (bash --version to confirm; required for associative arrays)
  • Basic familiarity with bash variables and quoting rules
  • A text editor and a terminal to test scripts

Bash arrays let you group related values under a single variable name and manipulate them with built-in syntax. Indexed arrays work like numbered lists; associative arrays (bash 4+) work like dictionaries or hash maps. Both are essential for writing scripts that handle real-world data without spawning subshells or abusing string splitting.

Indexed Arrays

Declaration and Assignment

You can declare an array explicitly or let bash infer it from the assignment syntax.

# Explicit declaration
declare -a fruits

# Inline assignment (most common)
fruits=(apple banana cherry)

# Assign individual elements by index
fruits[3]=date
fruits[10]=elderberry   # sparse arrays are fine

Indexes start at 0. Sparse arrays (gaps in the index sequence) are valid — bash does not allocate memory for the missing slots.

Reading Elements

echo "${fruits[0]}"      # apple
echo "${fruits[@]}"      # all elements, each as a separate word
echo "${fruits[*]}"      # all elements joined by IFS (usually a space)
echo "${#fruits[@]}"     # number of elements
echo "${!fruits[@]}"     # all indexes (useful for sparse arrays)

Always quote ${fruits[@]} when passing it to another command or loop — unquoted, word splitting will break elements that contain spaces.

Slicing and Substring Expansion

files=(a.log b.log c.log d.log e.log)

# Elements 1 and 2 (offset 1, length 2)
echo "${files[@]:1:2}"   # b.log c.log

# From index 2 to end
echo "${files[@]:2}"     # c.log d.log e.log

Adding and Removing Elements

# Append one or more elements
fruits+=(fig grape)

# Remove element at index 2 (leaves a gap in a sparse array)
unset 'fruits[2]'

# Re-index to close gaps
fruits=("${fruits[@]}")

After unset, the index is gone but the array is not re-numbered automatically. Re-assigning via ("${fruits[@]}") collapses the gaps into a clean 0-based sequence.

Associative Arrays

Declaration

Associative arrays require an explicit declare -A. This is a bash 4.0+ feature; macOS ships bash 3.2 by default, so check your version with bash --version if targeting mixed environments.

declare -A config

config[host]=db.example.com
config[port]=5432
config[user]=appuser

# Or all at once
declare -A config=([host]=db.example.com [port]=5432 [user]=appuser)

Reading Associative Array Data

echo "${config[host]}"     # db.example.com
echo "${!config[@]}"       # all keys (order not guaranteed)
echo "${config[@]}"        # all values
echo "${#config[@]}"       # number of pairs

Key order in associative arrays is not preserved in bash. If order matters, maintain a separate indexed array of keys.

Checking for Key Existence

if [[ -v config[port] ]]; then
    echo "port is set: ${config[port]}"
else
    echo "port key missing"
fi

-v tests whether the variable (or array element) is set. It was introduced in bash 4.2. Avoid testing with -z alone — it cannot distinguish an unset key from a key whose value is an empty string.

Looping Over Arrays

Basic For Loop

servers=(web1 web2 db1 db2)

for server in "${servers[@]}"; do
    echo "Pinging $server"
    ping -c1 -W1 "$server" &>/dev/null && echo "  up" || echo "  down"
done

Loop With Index

for i in "${!servers[@]}"; do
    printf "[%d] %s\n" "$i" "${servers[$i]}"
done

Looping Over Associative Arrays

declare -A colours=([red]='#FF0000' [green]='#00FF00' [blue]='#0000FF')

for key in "${!colours[@]}"; do
    printf "%-10s %s\n" "$key" "${colours[$key]}"
done

C-Style Loop for Indexed Arrays

for (( i=0; i<${#servers[@]}; i++ )); do
    echo "Server $i: ${servers[$i]}"
done

Common Patterns

Building an Array from Command Output

# mapfile (readarray) reads lines into an indexed array — bash 4.0+
mapfile -t log_files < <(find /var/log -name '*.log' -maxdepth 2)

echo "Found ${#log_files[@]} log files"
for f in "${log_files[@]}"; do
    echo "$f"
done

mapfile -t strips the trailing newline from each line. This is far safer than IFS splitting because it handles filenames with spaces correctly.

Deduplicating with an Associative Array

input=(alpha beta alpha gamma beta delta)
declare -A seen
unique=()

for item in "${input[@]}"; do
    if [[ ! -v seen[$item] ]]; then
        seen[$item]=1
        unique+=("$item")
    fi
done

echo "${unique[@]}"   # alpha beta gamma delta

Passing Arrays to Functions

Bash does not pass arrays by value. Use a nameref (bash 4.3+) or expand the array when calling the function.

print_all() {
    local -n arr=$1   # nameref: arr is an alias for the caller's variable
    for elem in "${arr[@]}"; do
        echo "  - $elem"
    done
}

colours=(red green blue)
print_all colours

Array as a Stack

stack=()

# push
stack+=(item1)
stack+=(item2)
stack+=(item3)

# pop
last_index=$(( ${#stack[@]} - 1 ))
top=${stack[$last_index]}
unset 'stack[$last_index]'
echo "Popped: $top"   # item3

Expansion Tricks

words=(hello world foo bar)

# Uppercase all elements (bash 4.0+)
echo "${words[@]^^}"        # HELLO WORLD FOO BAR

# Substring replacement across all elements
echo "${words[@]/o/0}"      # hell0 w0rld f00 bar

# Count characters in one element
echo "${#words[1]}"         # 5  (length of 'world')

Verification

After writing an array-heavy script, use bash -x to trace execution and confirm elements are expanding as expected:

bash -x your_script.sh 2>&1 | head -40

You can also dump the entire array state with declare -p:

declare -p fruits
# Output resembles: declare -a fruits=([0]="apple" [1]="banana" [3]="date")

Troubleshooting

Script fails with "declare: -A: invalid option"

Your bash is older than 4.0. On macOS, install a current bash via Homebrew (brew install bash) and update the shebang to #!/usr/bin/env bash. On Linux this is rarely an issue with any current LTS release.

Loop processes only one word of a multi-word element

You forgot to quote ${array[@]}. Always write "${array[@]}" (with double quotes) in loops and command arguments.

unset leaves gaps; length looks wrong

${#array[@]} returns the count of set elements, not the highest index plus one. For a sparse array these differ. Re-index with array=("${array[@]}") if you need a contiguous sequence.

Associative array keys with spaces

Quoting the key is required: config["my key"]="value". Unquoted keys with spaces cause a syntax error.

tested on:Ubuntu 24.04Fedora 40Debian 12Arch rolling

Frequently asked questions

What is the difference between ${array[@]} and ${array[*]}?
When double-quoted, "${array[@]}" expands to separate words (one per element), while "${array[*]}" joins all elements into a single word using the first character of IFS. Use [@] in loops and when passing to commands.
Can I use bash arrays in a POSIX sh script?
No. Arrays are a bash (and ksh/zsh) extension. POSIX sh has no array support. If your shebang is #!/bin/sh, array syntax will cause a syntax error on strict sh implementations.
How do I sort an array?
Pipe the expansion through sort and read it back with mapfile: mapfile -t sorted < <(printf '%s\n' "${array[@]}" | sort). There is no built-in sort for arrays.
Why is the order of keys in an associative array unpredictable?
Bash stores associative array keys in a hash table with no guaranteed insertion order. If you need ordered keys, maintain a separate indexed array listing them in the desired sequence.
How do I check if a value exists in an indexed array?
Bash has no built-in contains test. A common pattern is to loop and compare, or load elements as keys of an associative array and use [[ -v assoc[$value] ]] for O(1) lookup.

Related guides