| |

Bash Scripting 13 ๐Ÿง Associative Arrays โ€” Key-value dictionary structures in Bash 4+

โฑ๏ธ Estimated read: 11 min | |

An associative array is a variable that maps string keys to string values. It is the Bash equivalent of a dictionary or hash table, and it is available natively in Bash version 4 and later . The previous chapter covered indexed arrays, which use numeric subscripts; this chapter covers the associative form, where the subscript is an arbitrary string. The syntax is similar to indexed arrays, but the declaration requires the -A flag, and the key expansion uses the ! prefix to list the keys rather than the indices.

The associative array is the correct structure for any mapping where the keys are meaningful names rather than positions: server names to IP addresses, usernames to roles, configuration keys to values, or any lookup table where a numeric index would be arbitrary. The declaration is a single command, and the operationsโ€”assignment, lookup, iteration, deletionโ€”follow the same parameter expansion patterns as indexed arrays, with the key replacing the index.

Key point: An associative array must be declared with declare -A before it is used. The key expansion ${!arr[@]} returns the keys, and the value expansion ${arr[@]} returns the values. The quoted "${!arr[@]}" form preserves keys that contain spaces, and the "${arr[@]}" form preserves values that contain spaces. The unset 'arr[key]' form removes a single key-value pair, and the [[ -v arr[key] ]] test checks whether a key exists in Bash 4.3 and later .


Why associative arrays require explicit declaration

The declaration problem.

An indexed array can be created implicitly by assigning to a numeric subscript: arr[0]=value creates the array. An associative array cannot be created implicitly, because Bash cannot tell whether arr[foo]=bar is an indexed array with an arithmetic expression or an associative array with a string key. The declare -A arr command tells Bash to treat the variable as an associative array, and the subsequent assignments use string keys . Without the declaration, the assignment is either an error or an arithmetic expansion, depending on the key.

The key-quoting problem.

An associative array key can contain spaces, and the quoted expansion is required to preserve them. for key in "${!arr[@]}" iterates over the keys as separate words, even if a key contains spaces. The unquoted for key in ${!arr[@]} splits the keys on whitespace, and a key like web server becomes two iterations . The same applies to the values: "${arr[@]}" preserves a value with spaces, and ${arr[@]} splits it.

The key-existence problem.

Reading a non-existent key returns an empty string, which is indistinguishable from a key that exists with an empty value. The [[ -v arr[key] ]] test, available in Bash 4.3 and later, checks whether the key exists without reading its value . For older Bash versions, the ${arr[key]+x} expansion returns x if the key is set and nothing if it is not, and the ${arr[key]:+x} form returns x only if the key is set and non-empty .


a. Declaring and populating an associative array

The declare -A command creates an associative array. The compound assignment form arr=([key]=value ...) populates it in a single statement.

#!/usr/bin/env bash
# Declare an empty associative array
declare -A servers

# Assign individual key-value pairs
servers[web]="192.168.1.10"
servers[db]="192.168.1.20"
servers[cache]="192.168.1.30"

# Compound assignment
declare -A roles=(
  [alice]="admin"
  [bob]="developer"
  [carol]="operator"
)

# Read a value by key
echo "${servers[web]}"     # 192.168.1.10
echo "${roles[alice]}"     # admin

The declare -A servers creates the array, and the servers[web]=... assignments populate it. The declare -A roles=(...) form creates and populates the array in one command . The key is the subscript, and the value is the right-hand side. The braces in ${servers[web]} are required; without them, the shell tries to interpret the brackets as a glob pattern.

The key and value lists are available through parameter expansion.

# Number of key-value pairs
echo "${#servers[@]}"      # 3

# All keys
printf '%s\n' "${!servers[@]}"

# All values
printf '%s\n' "${servers[@]}"

# Check whether a key exists (Bash 4.3+)
if [[ -v servers[web] ]]; then
  echo "web key exists"
fi

The ${#servers[@]} expansion is the number of elements, the ${!servers[@]} expansion is the list of keys, and the ${servers[@]} expansion is the list of values . The [[ -v servers[web] ]] test returns true if the key is present, regardless of its value .


b. Iterating over keys and values

The key-value pairs are iterated by looping over the keys and reading the value for each key.

#!/usr/bin/env bash
declare -A servers=(
  [web]="192.168.1.10"
  [db]="192.168.1.20"
  [cache]="192.168.1.30"
)

# Iterate over keys and values
for key in "${!servers[@]}"; do
  echo "$key -> ${servers[$key]}"
done

# Iterate over values only
for value in "${servers[@]}"; do
  echo "Value: $value"
done

The for key in "${!servers[@]}" loop iterates over the keys, and ${servers[$key]} reads the value for each key. The quoted "${!servers[@]}" form preserves keys with spaces, and the quoted "${servers[$key]}" form preserves values with spaces . The order of iteration is not guaranteed; Bash does not maintain insertion order or sort the keys automatically.

If the script needs the values in a sorted order, the keys can be sorted before iteration.

# Iterate in sorted key order
for key in $(printf '%s\n' "${!servers[@]}" | sort); do
  echo "$key -> ${servers[$key]}"
done

The printf and sort pipeline produces the sorted keys, and the loop iterates over them. The quoted expansion in the printf preserves keys with spaces, though the sort command operates on lines and will treat a key with a newline as two keys, which is a limitation of any line-based sorting.


c. Modifying and deleting entries

Adding a new key-value pair is a direct assignment. Overwriting an existing key replaces its value. Deleting a key-value pair uses the unset command with the key.

#!/usr/bin/env bash
declare -A config

# Add entries
config[host]="example.com"
config[port]="8080"

# Overwrite an entry
config[port]="9090"

# Delete an entry
unset 'config[port]'

# Verify
echo "${config[host]}"     # example.com
echo "${config[port]}"     # (empty)
[[ -v config[port] ]] || echo "port is gone"

The config[host]="example.com" assignment adds a new entry. The config[port]="9090" assignment overwrites the existing value. The unset 'config[port]' command removes the entry . The key must be quoted, or the shell may interpret the brackets as a glob pattern. The [[ -v config[port] ]] test confirms that the key is no longer present.

The unset command can remove multiple keys in a loop.

# Delete multiple keys
for key in host port; do
  unset "config[$key]"
done

The quoted "config[$key]" form ensures that a key with spaces or special characters is treated as a single subscript .


Complete Example Session

#!/usr/bin/env bash
# ============================================
# PART 1: DECLARE AND POPULATE
# ============================================
declare -A ip_addresses
ip_addresses[web1]="10.0.0.1"
ip_addresses[web2]="10.0.0.2"
ip_addresses[db1]="10.0.0.3"

# ============================================
# PART 2: READ A VALUE
# ============================================
echo "web1: ${ip_addresses[web1]}"

# ============================================
# PART 3: CHECK KEY EXISTENCE
# ============================================
if [[ -v ip_addresses[web2] ]]; then
  echo "web2 is registered"
fi

# ============================================
# PART 4: ITERATE OVER KEY-VALUE PAIRS
# ============================================
for host in "${!ip_addresses[@]}"; do
  echo "$host -> ${ip_addresses[$host]}"
done

# ============================================
# PART 5: COUNT AND LIST
# ============================================
echo "Total hosts: ${#ip_addresses[@]}"
echo "Hosts: ${!ip_addresses[*]}"

# ============================================
# PART 6: ADD AND OVERWRITE
# ============================================
ip_addresses[cache1]="10.0.0.4"
ip_addresses[web1]="10.0.0.100"
echo "web1 now: ${ip_addresses[web1]}"

# ============================================
# PART 7: DELETE AN ENTRY
# ============================================
unset 'ip_addresses[db1]'
echo "After delete: ${#ip_addresses[@]} hosts"

# ============================================
# PART 8: ITERATE WITH A SORTED ORDER
# ============================================
for host in $(printf '%s\n' "${!ip_addresses[@]}" | sort); do
  echo "Sorted: $host"
done

# ============================================
# PART 9: PASS TO A FUNCTION
# ============================================
lookup() {
  local -n map=$1
  local key=$2
  if [[ -v map[$key] ]]; then
    echo "${map[$key]}"
  else
    echo "not found" >&2
    return 1
  fi
}
lookup ip_addresses web1

# ============================================
# PART 10: COMPOUND ASSIGNMENT
# ============================================
declare -A ports=(
  [http]=80
  [https]=443
  [ssh]=22
)
printf '%s\n' "${ports[@]}"

The ten parts demonstrate the associative array surface: declaration, value lookup, key existence, iteration, counting, modification, deletion, sorted iteration, function passing, and compound assignment. The critical pattern is Part 4: the quoted "${!ip_addresses[@]}" iterates over the keys, and the quoted "${ip_addresses[$host]}" reads the value for each key.


Quick Reference

Associative Array Operations

OperationSyntax
Declaredeclare -A arr
Compound assigndeclare -A arr=([k]=v ...)
Assignarr[key]="value"
Read${arr[key]}
All keys"${!arr[@]}"
All values"${arr[@]}"
Count${#arr[@]}
Key exists[[ -v arr[key] ]] (Bash 4.3+)
Deleteunset 'arr[key]'
Iteratefor k in "${!arr[@]}"; do ...; done

Indexed vs Associative

FeatureIndexedAssociative
Declarationdeclare -adeclare -A
SubscriptIntegerString
Key expansion${!arr[@]} (indices)${!arr[@]} (keys)
Value expansion${arr[@]}${arr[@]}

Key Existence Tests

TestBash VersionChecks
[[ -v arr[key] ]]4.3+Key exists (any value)
${arr[key]+x}4.0+Key is set
${arr[key]:+x}4.0+Key is set and non-empty

Best Practices

โœ… Do This:

# Declare with -A
declare -A map                                                # โœ…

# Quote key expansions
for k in "${!map[@]}"; do echo "${map[$k]}"; done             # โœ…

# Check existence with -v
if [[ -v map[key] ]]; then ...; fi                            # โœ…

# Quote the unset subscript
unset 'map[key]'                                              # โœ…

# Use compound assignment for literals
declare -A ports=([http]=80 [https]=443)                      # โœ…

# Use a nameref for functions
local -n ref=$1                                               # โœ…

โŒ Don’t Do This:

# Forget the declaration
map[key]=value    # Treated as indexed or arithmetic           # โŒ

# Unquoted key expansion
for k in ${!map[@]}; do ...; done                             # โŒ

# Read a missing key without a check
echo "${map[missing]}"    # Empty string, no error             # โŒ

# Use an empty string as a key
map[""]="value"    # Error                                    # โŒ

# Assume insertion order
# Iteration order is unspecified                               # โŒ

Common Pitfalls

PitfallWhy It HappensFix
declare: -A: invalid optionBash version older than 4Check bash --version
Key with spaces splitsUnquoted key expansion"${!map[@]}"
unset removes wrong keySubscript interpreted as globunset 'map[key]'
Key existence check failsBash older than 4.3Use ${map[key]+x}
Empty key errorEmpty string used as a keyUse a non-empty key

Real-World Examples

1. Declare and assign

declare -A map; map[key]="value"

2. Compound assignment

declare -A ports=([http]=80 [https]=443)

3. Read a value

echo "${ports[http]}"

4. All keys

echo "${!ports[@]}"

5. All values

echo "${ports[@]}"

6. Count

echo "${#ports[@]}"

7. Key exists

[[ -v ports[http] ]] && echo "yes"

8. Iterate

for k in "${!ports[@]}"; do echo "$k -> ${ports[$k]}"; done

9. Delete

unset 'ports[http]'

10. Pass by reference

func() { local -n m=$1; echo "${m[key]}"; }; func ports

Visual

Key-Value Mapping

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ASSOCIATIVE ARRAY MAPS STRINGS TO STRINGS                   โ”‚
โ”‚                                                              โ”‚
โ”‚  declare -A servers                                          โ”‚
โ”‚  servers[web]="10.0.0.1"                                     โ”‚
โ”‚  servers[db]="10.0.0.2"                                      โ”‚
โ”‚  servers[cache]="10.0.0.3"                                   โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                                  โ”‚
โ”‚  โ”‚ Key     โ”‚ Value        โ”‚                                  โ”‚
โ”‚  โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค                                  โ”‚
โ”‚  โ”‚ web     โ”‚ 10.0.0.1     โ”‚                                  โ”‚
โ”‚  โ”‚ db      โ”‚ 10.0.0.2     โ”‚                                  โ”‚
โ”‚  โ”‚ cache   โ”‚ 10.0.0.3     โ”‚                                  โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜                                  โ”‚
โ”‚                                                              โ”‚
โ”‚  ${servers[web]} โ†’ "10.0.0.1"                                โ”‚
โ”‚  ${!servers[@]}  โ†’ web db cache                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Iteration

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ITERATE OVER KEYS, READ VALUES                              โ”‚
โ”‚                                                              โ”‚
โ”‚  for key in "${!servers[@]}"; do                             โ”‚
โ”‚      echo "$key -> ${servers[$key]}"                         โ”‚
โ”‚  done                                                        โ”‚
โ”‚                                                              โ”‚
โ”‚  Iteration 1: key="web"   โ†’ "10.0.0.1"                       โ”‚
โ”‚  Iteration 2: key="db"    โ†’ "10.0.0.2"                       โ”‚
โ”‚  Iteration 3: key="cache" โ†’ "10.0.0.3"                       โ”‚
โ”‚                                                              โ”‚
โ”‚  The order is unspecified.                                   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Key Existence

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  THREE WAYS TO CHECK FOR A KEY                               โ”‚
โ”‚                                                              โ”‚
โ”‚  Bash 4.3+:                                                  โ”‚
โ”‚  [[ -v map[key] ]] โ†’ true if the key exists                  โ”‚
โ”‚                                                              โ”‚
โ”‚  Bash 4.0+:                                                  โ”‚
โ”‚  ${map[key]+x} โ†’ "x" if the key is set                       โ”‚
โ”‚  ${map[key]:+x} โ†’ "x" if the key is set and non-empty        โ”‚
โ”‚                                                              โ”‚
โ”‚  Reading ${map[missing]} returns an empty string,            โ”‚
โ”‚  which is indistinguishable from an empty value.             โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
RequirementBash 4.0 or later
Declarationdeclare -A arr
Assignarr[key]="value"
Read${arr[key]}
All keys"${!arr[@]}"
All values"${arr[@]}"
Count${#arr[@]}
Key exists[[ -v arr[key] ]] (4.3+)
Deleteunset 'arr[key]'
Iteratefor k in "${!arr[@]}"; do ...; done
Empty keyNot allowed

Key takeaways:

  • Associative arrays require declare -A. The declaration tells Bash that the subscripts are strings, not arithmetic expressions. Without it, the assignment is an indexed array assignment or an error .
  • The key expansion is "${!arr[@]}". The ! prefix returns the keys, and the quoted form preserves keys with spaces. The value expansion is "${arr[@]}", and the quoted form preserves values with spaces .
  • The [[ -v arr[key] ]] test checks key existence. It is available in Bash 4.3 and later, and it distinguishes a missing key from a key with an empty value. For older versions, the ${arr[key]+x} expansion is the alternative .
  • unset 'arr[key]' removes a key-value pair. The subscript must be quoted, or the shell may interpret the brackets as a glob pattern. The command removes the key and its value, and the array’s length decreases .
  • Iteration order is unspecified. Bash does not maintain insertion order or sort the keys. A script that needs a specific order must sort the keys explicitly, typically with a pipeline through sort.
  • Pass associative arrays to functions with a nameref. The local -n ref=$1 declaration in the function creates a reference to the caller’s array, and the function can read and modify it without copying the entire structure.

Remember: An associative array is a dictionary. The declare -A command creates it, the arr[key]=value syntax populates it, and the "${!arr[@]}" expansion lists the keys. The quoted expansion preserves spaces in keys and values, and the [[ -v arr[key] ]] test checks whether a key exists. The unset 'arr[key]' command removes an entry. The order of iteration is unspecified, so a script that needs sorted output must sort explicitly. The associative array is the correct structure for any mapping where the keys are names, not positions.


Was this helpful?


Stop using slow, ad-bloated tool sites! ๐Ÿคฎ

๐Ÿ”Ž Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
โœ… Finance (Mortgage, Interest, Inflation)
โœ… Tech (Base64, JSON, Dev Suite, IP)
โœ… Health (BMI, BMR, TDEE)
โœ… Productivity (Timer, Workspace, QR)

โšก๏ธ Fast & Private
๐Ÿ”’ No data leaves your device
๐Ÿ’Ž 100% Free

๐Ÿ”— Use it now: https://tools.kandz.me
๐Ÿ”– Bookmark itโ€”youโ€™ll need it later!