Bash Associative Arrays: Key-Value Maps

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
Short on time?Beta Get the gist of this 12-minute read in seconds, then ask follow-up questions

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:

bash
printf '%s\n' "$BASH_VERSION"
output
5.2.26(1)-release

Associative 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.

bash
declare -A ports=()

You can also populate the map in one compound assignment when the initial key-value pairs are already known:

bash
declare -A ports=(
    [http]=80
    [https]=443
    [ssh]=22
)
declare -p ports
output
declare -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:

bash
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:

bash
printf 'http=%s https=%s ssh=%s\n' "${ports[http]}" "${ports[https]}" "${ports[ssh]}"
output
http=80 https=443 ssh=22

When the key is stored in a variable, expand the array element inside double quotes so the resulting value stays intact:

bash
svc=https
printf '%s=%s\n' "$svc" "${ports[$svc]}"
output
https=443

You 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:

bash
ports[http]=8080
printf 'http=%s\n' "${ports[http]}"
output
http=8080

The 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:

bash
for svc in "${!ports[@]}"; do
  printf '%s=%s\n' "$svc" "${ports[$svc]}"
done
output
ssh=22
https=443
http=8080

The 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:

bash
printf '%s\n' "${#ports[@]}"
output
3

An 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:

bash
unset 'ports[ssh]'
declare -p ports
output
declare -A ports=([https]="443" [http]="8080" )

Confirm the new size:

bash
printf 'count=%s\n' "${#ports[@]}"
output
count=2

When 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:

bash
if [[ -v ports[http] ]]; then echo 'http key exists'; fi
output
http key exists

The removed ssh entry should no longer register as set:

bash
if [[ ! -v ports[ssh] ]]; then echo 'ssh key absent after unset'; fi
output
ssh key absent after unset

A key can exist even when its value is empty:

bash
declare -A state=([web]="")
key=web
[[ -v state[$key] ]] && echo 'key exists'
output
key exists

The 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:

bash
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:

bash
declare -A ports=(
    [http]=80
    [https]=443
)
print_map ports | sort
output
http=80
https=443

local -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:

bash
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]}"
done
output
key=[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:

bash
declare -A order_demo=()
order_demo[z]=1
order_demo[a]=2
order_demo[m]=3
for k in "${!order_demo[@]}"; do echo "$k"; done
output
z
m
a

The 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:

bash
while IFS= read -r k; do
  printf '%s=%s\n' "$k" "${order_demo[$k]}"
done < <(printf '%s\n' "${!order_demo[@]}" | sort)
output
a=2
m=3
z=1

Bash 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:

bash
declare -A seen=()
for item in web db cache web api db; do
  seen[$item]=1
done
printf 'unique count=%s\n' "${#seen[@]}"
output
unique count=4

List the unique members in sorted order:

bash
for k in "${!seen[@]}"; do echo "$k"; done | sort
output
api
cache
db
web

You 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:

bash
declare -A counts=()
while read -r code; do
  counts[$code]=$((${counts[$code]:-0} + 1))
done <<'EOF'
200
404
200
500
404
200
EOF

The ${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:

bash
while IFS= read -r code; do
  printf '%s=%s\n' "$code" "${counts[$code]}"
done < <(printf '%s\n' "${!counts[@]}" | sort -n)
output
200=3
404=2
500=1

Indexed 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
bash -c 'm=(); m[foo]=bar; declare -p m'
output
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
bash -c 'declare -A m=(); m[""]=x' 2>&1 || true
output
bash: line 1: m[""]: bad array subscript

Avoid 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


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.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)