| Tested on | RHEL 10.2 (Coughlan) |
|---|---|
| Package | bash 5.2.26-6.el10 |
| Applies to | Linux distributions with Bash installed |
| Privilege | Normal user; no root required for the examples |
| Scope | Bash 4+ associative arrays: declare -A, compound initialization, add/read/update/delete key-value pairs, key existence tests, passing maps to functions with local -n (Bash 4.3+), loop keys with "${!map[@]}", spaces in keys and values, iteration order, set membership, frequency counting, and declare -p debugging. Does not cover indexed-array CRUD, full config-file parsing, or string-splitting workflows. |
| Related guides | Bash shell script tutorial Shell scripting course hub Remove duplicate array elements |
Bash associative arrays store values under string keys, giving shell scripts a dictionary- or hash-map-like data structure. Declare one with declare -A, then assign values with map[key]=value and read them with "${map[key]}".
They are useful when a name, hostname, status code, or other string is a better lookup key than a numeric array index. Bash associative arrays require Bash 4 or later; the next section shows how to confirm your shell version.
What Is a Bash Associative Array?
Unlike an indexed array such as ports[0], an associative array uses a string key such as ports[http]. You look up values by name, code, or label instead of position.
Typical uses include:
- Service name → port number
- Environment name → config file path
- Hostname → deployment status
- HTTP status code → occurrence count
- Deduplicating a list (set membership)
Each key maps to one value. Assigning the same key again replaces the previous value.
Bash Version Requirement
Check the shell version before you rely on declare -A:
printf '%s\n' "$BASH_VERSION"5.2.26(1)-releaseAssociative arrays require Bash 4.0 or later and are not part of POSIX sh; see Bash vs sh when a script must run under /bin/sh. A script run with sh script.sh may therefore use another shell such as dash, which does not understand declare -A. Use a Bash shebang for directly executed scripts or run the file explicitly with bash script.sh.
Declare a Map with declare -A
Create an empty associative array with declare -A name. The -A attribute marks the variable as a key-value map rather than a numeric indexed array.
declare -A ports=()You can also populate the map in one compound assignment when the initial key-value pairs are already known:
declare -A ports=(
[http]=80
[https]=443
[ssh]=22
)
declare -p portsdeclare -A ports=([ssh]="22" [https]="443" [http]="80" )Use a compound assignment when you already know the initial key-value pairs. Add or overwrite individual entries later with ports[key]=value.
Add -r when the map should be read-only after creation. Use a separate variable so the mutable examples below keep working:
declare -Ar fixed_ports=(
[http]=80
[https]=443
)Later assignments or deletions from fixed_ports fail because the array is read-only.
declare -p prints the variable with its attributes, the same introspection pattern used for scalars in Bash variables. When debugging, declare -p mapname also confirms the -A attribute and lists current key-value pairs. Key order in that output is not guaranteed to match insertion order.
Add and Read Key-Value Pairs
Assign with map[key]=value. Read with "${map[key]}" so word splitting does not break values that contain spaces.
From the example map above, print each service port:
printf 'http=%s https=%s ssh=%s\n' "${ports[http]}" "${ports[https]}" "${ports[ssh]}"http=80 https=443 ssh=22When the key is stored in a variable, expand the array element inside double quotes so the resulting value stays intact:
svc=https
printf '%s=%s\n' "$svc" "${ports[$svc]}"https=443You can also add entries with +=. For example, if you wanted to add DNS to a map, the syntax would be ports+=([dns]=53). Like a normal assignment, an existing key is replaced rather than duplicated.
Use Bash echo vs printf when you need predictable formatting across multiple fields; printf avoids surprises with backslash escapes in values.
Update Existing Keys
Assigning to an existing key overwrites the stored value. No separate update command is required.
Change the HTTP port and read it back:
ports[http]=8080
printf 'http=%s\n' "${ports[http]}"http=8080The key count stays the same because you replaced a value rather than adding a new key.
Loop Over Keys and Values
Iterate keys with "${!map[@]}". The ! prefix lists subscripts (keys) instead of values. Use a Bash for loop to walk the map:
for svc in "${!ports[@]}"; do
printf '%s=%s\n' "$svc" "${ports[$svc]}"
donessh=22
https=443
http=8080The output order above came from one run. Treat iteration order as unspecified; the ordering section below explains why.
To loop values only when you do not need the key, use "${map[@]}" the same way you expand indexed arrays. You lose the key name unless you track it separately.
Count Keys
${#map[@]} returns the number of keys in the associative array.
After the three-service map and the HTTP port update, the count is still three:
printf '%s\n' "${#ports[@]}"3An empty declare -A map=() reports 0. After you delete keys, the count drops accordingly.
Delete a Key
Remove one entry with unset 'map[key]'. Quote the subscript when the key might contain characters the shell would parse.
Drop the SSH entry and inspect what remains:
unset 'ports[ssh]'
declare -p portsdeclare -A ports=([https]="443" [http]="8080" )Confirm the new size:
printf 'count=%s\n' "${#ports[@]}"count=2When the key name is in a variable, use unset "ports[$svc]" with double quotes around the subscript expression.
To remove all entries but keep ports as an associative array, use ports=(). Use unset ports when you want to remove the array variable itself. unset 'ports[key]' removes only that entry.
Check Whether a Key Exists
Bash 4.2+ supports [[ -v map[key] ]] to test whether a subscript is set. These tests live in Bash [[ … ]] conditionals; see Bash [ ] vs [[ ]] when you need the full syntax and portability notes.
After deleting ssh from ports, http still exists and ssh does not:
if [[ -v ports[http] ]]; then echo 'http key exists'; fihttp key existsThe removed ssh entry should no longer register as set:
if [[ ! -v ports[ssh] ]]; then echo 'ssh key absent after unset'; fissh key absent after unsetA key can exist even when its value is empty:
declare -A state=([web]="")
key=web
[[ -v state[$key] ]] && echo 'key exists'key existsThe key exists even though its value is empty. That is why -v is preferable to testing the expanded value.
On Bash releases before 4.2, use [[ -n "${map[$key]+x}" ]] instead. This tests whether the element is set even when its stored value is an empty string. A naive test such as [[ -n ${map[$key]} ]] does not distinguish a missing key from an existing key with an empty value.
Pass an Associative Array to a Function
Pass the associative array's variable name to the function. On Bash 4.3+, local -n creates a nameref to that variable, so a Bash function can read keys and values without copying the map. The argument must be the name of an existing variable, not arbitrary text. Do not pass "${ports[@]}", because expanding only the values loses the link between keys and values.
The helper below receives the map name and prints each key-value pair:
print_map() {
local -n map_ref="$1"
local key
for key in "${!map_ref[@]}"; do
printf '%s=%s\n' "$key" "${map_ref[$key]}"
done
}Call it with the map name and no $ prefix so the nameref binds to the original array:
declare -A ports=(
[http]=80
[https]=443
)
print_map ports | sorthttp=80
https=443local -n requires Bash 4.3 or later. Older scripts must loop in the caller or duplicate keys through other arguments.
Values and Keys Containing Spaces
Keys and values are strings. Spaces are allowed in both when you quote assignments and expansions; see Bash single vs double quotes when a key or value includes metacharacters.
Try a map with a spaced key and a spaced value:
declare -A paths=()
paths["prod env"]="/etc/app/prod.conf"
paths[staging]="/var/lib/app/staging data/settings.ini"
for k in "${!paths[@]}"; do
printf 'key=[%s] value=[%s]\n' "$k" "${paths[$k]}"
donekey=[prod env] value=[/etc/app/prod.conf]
key=[staging] value=[/var/lib/app/staging data/settings.ini]Bracket notation in the sample output shows where each string begins and ends. Without quotes, ${paths[$k]} would word-split the value on output.
Associative Array Ordering
Bash does not guarantee insertion order when you iterate "${!map[@]}". The hash table may return keys in any order, and that order can differ between runs or Bash versions.
This example inserts three keys in one order and then prints the order Bash returns:
declare -A order_demo=()
order_demo[z]=1
order_demo[a]=2
order_demo[m]=3
for k in "${!order_demo[@]}"; do echo "$k"; donez
m
aThe keys are not alphabetical and do not follow z, then a, then m insertion order. When presentation order matters (reports, config dumps, stable diffs), pipe the keys through sort before you loop:
while IFS= read -r k; do
printf '%s=%s\n' "$k" "${order_demo[$k]}"
done < <(printf '%s\n' "${!order_demo[@]}" | sort)a=2
m=3
z=1Bash process substitution with
< <(…) feeds sorted key names into the while read loop without a
temporary file and keeps the loop in the current shell.
Use an Associative Array as a Set
A map with dummy values tracks unique membership. Store 1 (or any constant) under each seen item; the key list is your deduplicated set.
Walk a list that repeats web, db, and cache:
declare -A seen=()
for item in web db cache web api db; do
seen[$item]=1
done
printf 'unique count=%s\n' "${#seen[@]}"unique count=4List the unique members in sorted order:
for k in "${!seen[@]}"; do echo "$k"; done | sortapi
cache
db
webYou only need to know whether a key exists, not store a meaningful value. Test membership with [[ -v seen[$item] ]] before adding a duplicate.
Count Occurrences with a Bash Associative Array
Increment a count per key to tally occurrences: HTTP status codes, error messages, or word frequencies.
The following Bash while loop counts repeated HTTP status codes:
declare -A counts=()
while read -r code; do
counts[$code]=$((${counts[$code]:-0} + 1))
done <<'EOF'
200
404
200
500
404
200
EOFThe ${counts[$code]:-0} form treats a missing key as zero before
adding one. The Bash parameter-expansion guide
explains how :- distinguishes unset or empty values from populated
ones. The explicit :-0 makes the initial value clear. A shorter form
is ((++counts[$code])); Bash treats an unset arithmetic element as
zero before incrementing it. For more on (( )) and $(( )), see
Bash arithmetic.
Print each code and its count, sorted numerically by key:
while IFS= read -r code; do
printf '%s=%s\n' "$code" "${counts[$code]}"
done < <(printf '%s\n' "${!counts[@]}" | sort -n)200=3
404=2
500=1Indexed vs Associative Arrays
Pick the array type from how you address elements:
| Need | Indexed array declare -a |
Associative array declare -A |
|---|---|---|
Access by position (0, 1, 2) |
Yes | No |
Access by string key (http, prod) |
No | Yes |
| Ordered list of items | Natural fit | Do not rely on iteration order |
| Lookup table / dictionary | Awkward (parallel arrays) | Natural fit |
| Set or frequency counter | Practical only when keys are numeric indices | Natural fit for arbitrary string keys |
Bash 3.x / POSIX sh |
Indexed only (Bash) | Requires Bash 4+ |
Use indexed arrays for ordered lists and sequences — see Bash arrays for declare -a, looping, slicing, and sparse indices. Use associative arrays when the lookup key is a name, code, hostname, or other string label.
Common Mistakes
| Symptom | Likely cause | Fix |
|---|---|---|
declare -a instead of -A; numeric indices |
Forgot declare -A before first string key |
Run declare -A map=() before any map[key]= assignment |
| Values split across words | Unquoted ${map[$key]} |
Use "${map[$key]}" when expanding a value |
| Keys appear in random order in output | Expecting insertion order | Sort "${!map[@]}" before looping when order matters |
bad array subscript on assign |
Empty string key map[""]= |
Keys must be non-empty strings |
declare: -A: invalid option or declare: not found |
Bash is too old or the script is running under another shell | Run with Bash 4+ and use a Bash shebang for directly executed scripts |
-v test fails on old Bash |
Bash older than 4.2 | Upgrade Bash or use [[ -n "${map[$key]+x}" ]] |
Reproduce the indexed-array mistake by skipping declare -A:
bash -c 'm=(); m[foo]=bar; declare -p m'declare -a m=([0]="bar")Bash treated foo as a bare word in an indexed context and stored bar at index 0. Always declare -A first.
An empty key fails immediately:
bash -c 'declare -A m=(); m[""]=x' 2>&1 || truebash: line 1: m[""]: bad array subscriptAvoid dynamic key expressions you cannot quote safely. Build the key in a variable, then use "${map[$key]}". Do not use eval to construct subscripts.
Summary
Bash associative arrays give you string-keyed maps when numeric indices are the wrong model. Declare them with declare -A, assign with map[key]=value, read with quoted "${map[key]}", and loop keys through "${!map[@]}".
Treat iteration order as unspecified and sort keys when humans or diffs need a stable sequence. The same structure works as a deduplication set or an occurrence counter when you pick the right update pattern. Use declare -p, [[ -v map[key] ]], and local -n when you pass a map into a function.
These maps require Bash 4 or later and must be interpreted by Bash rather than a POSIX-only shell. For directly executed scripts, use a Bash shebang; alternatively, invoke the script explicitly with bash script.sh. Indexed arrays, string-to-array splitting, and full config-file parsing are separate topics. Use this guide when the lookup key is a name, code, or label rather than a position in a list.
References
- Bash manual — Arrays — indexed and associative arrays
- Bash manual — declare builtin —
-Aand other attributes - Bash manual — Shell Parameters — nameref variables (
local -n) - Bash manual — Conditional Expressions —
-v varnamesubscript tests
Frequently Asked Questions
What Bash version supports associative arrays?
Associative arrays were added in Bash 4.0. Check $BASH_VERSION before using declare -A. Bash 3.x and POSIX sh do not provide them.
Are Bash associative arrays ordered?
No. Iteration order over "${!map[@]}" is unspecified and may change between runs. Sort the keys with sort or sort -n when you need stable output.
How do I loop over keys?
Use for key in "${!map[@]}"; do … "${map[$key]}" … done. The ! prefix on "${!map[@]}" lists keys; without !, "${map[@]}" expands values.
How do I check or use a key-value pair?
Read a value with "${map[key]}". Test whether a key exists with [[ -v map[key] ]] on Bash 4.2+. Assign or overwrite with map[key]=value.
Can keys contain spaces?
Yes. Quote the subscript: map["prod env"]=value and "${map[$key]}" when the key is in a variable. Empty keys are not allowed.

