Linux Basic — Associative Array CRUD
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