Linux Basic — Associative Array CRUD

3 minute read

Associative Array CRUD

Associative arrays (Bash 4+) are string-keyed hash maps. They’re perfect for lookup tables (status → message), counters (word → count), and config bags (key → value).


Declare

Associative arrays must be declared explicitly:

declare -A user

user[name]=alice
user[role]=admin
user[email]="alice@example.com"

Forgetting declare -A is the #1 bug — Bash silently treats it as an indexed array and user[name] becomes user[0]:

# ❌ Without declare -A
oops=()
oops[name]=alice            # name evaluated as arithmetic → 0
oops[role]=admin            # role → 0 → overwrites
echo "${oops[@]}"
# admin

declare -A first, every time.

You can also create literal:

declare -A user=(
    [name]=alice
    [role]=admin
    [email]="alice@example.com"
)

Set & Get

declare -A cfg=([host]=example.com [port]=8080)

# Get
echo "${cfg[host]}"          # example.com

# Set / overwrite
cfg[port]=9090

# Add a new key
cfg[debug]=true

# Get with default (works on assoc too)
echo "${cfg[timeout]:-30}"   # 30 — key not set

Keys can have any characters except ]. If they contain spaces or special chars, quote them:

cfg["my key"]="value"
echo "${cfg["my key"]}"

Iterate

declare -A cfg=([host]=example.com [port]=8080 [debug]=true)

# Keys
for k in "${!cfg[@]}"; do
    echo "$k = ${cfg[$k]}"
done
# host = example.com
# port = 8080
# debug = true

No order guarantee. Associative arrays are hash tables — iteration order is implementation-defined. If you need predictable order, sort:

for k in $(echo "${!cfg[@]}" | tr ' ' '\n' | sort); do
    printf '%-8s = %s\n' "$k" "${cfg[$k]}"
done

Or maintain a parallel indexed array of insertion order — Bash has no built-in “ordered dict.”


Check If Key Exists

declare -A cfg=([host]=example.com [debug]="")

# ✅ Right — distinguishes "missing" from "empty"
[[ -v cfg[debug] ]] && echo "debug key is set"
[[ -v cfg[port]  ]] || echo "port not set"

# ❌ Wrong — fails for keys with empty value
[[ -n "${cfg[debug]}" ]] && echo "exists"

-v key is the canonical “is this set?” test.


Delete

declare -A cfg=([host]=example.com [port]=8080)

unset 'cfg[port]'
[[ -v cfg[port] ]] || echo "gone"

# Clear all entries
cfg=()

Quote the target ('cfg[port]') to avoid glob expansion issues.


Count

echo "${#cfg[@]}"            # number of keys

Pattern 1 — Counter / Histogram

declare -A count
for word in apple banana apple cherry apple banana; do
    (( count[$word]++ ))
done

for k in "${!count[@]}"; do
    echo "$k: ${count[$k]}"
done
# apple: 3
# banana: 2
# cherry: 1

(( count[$word]++ )) works even when the key is missing — arithmetic context treats undefined vars as 0. No need to initialise.


Pattern 2 — Set (Unique Values)

A set is just an associative array where the values don’t matter:

declare -A seen
for x in "${items[@]}"; do
    seen[$x]=1
done

unique=( "${!seen[@]}" )         # keys are the unique values

Add sort if order matters:

mapfile -t unique < <(printf '%s\n' "${!seen[@]}" | sort)

Pattern 3 — Lookup Table

declare -A status_msg=(
    [200]="OK"
    [301]="Moved Permanently"
    [404]="Not Found"
    [500]="Internal Server Error"
)

code=404
echo "$code ${status_msg[$code]:-Unknown}"
# 404 Not Found

The :-Unknown fallback handles “unknown code” cleanly.


Pattern 4 — Group-By

# Group filenames by extension
declare -A by_ext

for f in *; do
    [[ -f "$f" ]] || continue
    ext=${f##*.}
    by_ext[$ext]+="$f "          # space-separated string-of-strings (limited)
done

for ext in "${!by_ext[@]}"; do
    echo "$ext: ${by_ext[$ext]}"
done

Caveat: the value is a single string with spaces — you can’t put arrays inside arrays in Bash. If you need lists per key, generate filenames into separate per-ext temp files or use a different language.


Iteration With Both Keys and Values

declare -A cfg=([host]=example.com [port]=8080)

while IFS= read -r k; do
    printf '%-8s = %s\n' "$k" "${cfg[$k]}"
done < <(printf '%s\n' "${!cfg[@]}" | sort)
# host     = example.com
# port     = 8080

The sort pipe gives you alphabetical key order — usually what you want for human-readable output.


A Real Example — Word Frequency in a File

declare -A freq
while read -r word; do
    (( freq[$word]++ ))
done < <(tr -cs '[:alpha:]' '\n' < /usr/share/dict/words | tr A-Z a-z)

# Top 10 by count
for k in "${!freq[@]}"; do
    printf '%d %s\n' "${freq[$k]}" "$k"
done | sort -rn | head -10

awk would be faster for this specific job, but the pattern (build a hash, then summarise) is identical.


Common Mistakes

❌ Wrong ✅ Right Issue
arr=(); arr[key]=val (no -A) declare -A arr; arr[key]=val indexed array treats key as arithmetic = 0
Iterating in expected order no guarantee — sort if needed hash tables are unordered
[[ -n "${m[k]}" ]] to check existence [[ -v m[k] ]] empty value vs missing key
unset m[k] (unquoted) unset 'm[k]' bracket-glob expansion risk
Nesting arrays flatten to one level, or use temp files Bash has no nested arrays

Next → Array Slicing & Copying

Updated: