Bash Scripting 13 ๐ง Associative Arrays โ Key-value dictionary structures in Bash 4+
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
| Operation | Syntax |
|---|---|
| Declare | declare -A arr |
| Compound assign | declare -A arr=([k]=v ...) |
| Assign | arr[key]="value" |
| Read | ${arr[key]} |
| All keys | "${!arr[@]}" |
| All values | "${arr[@]}" |
| Count | ${#arr[@]} |
| Key exists | [[ -v arr[key] ]] (Bash 4.3+) |
| Delete | unset 'arr[key]' |
| Iterate | for k in "${!arr[@]}"; do ...; done |
Indexed vs Associative
| Feature | Indexed | Associative |
|---|---|---|
| Declaration | declare -a | declare -A |
| Subscript | Integer | String |
| Key expansion | ${!arr[@]} (indices) | ${!arr[@]} (keys) |
| Value expansion | ${arr[@]} | ${arr[@]} |
Key Existence Tests
| Test | Bash Version | Checks |
|---|---|---|
[[ -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
| Pitfall | Why It Happens | Fix |
|---|---|---|
declare: -A: invalid option | Bash version older than 4 | Check bash --version |
| Key with spaces splits | Unquoted key expansion | "${!map[@]}" |
unset removes wrong key | Subscript interpreted as glob | unset 'map[key]' |
| Key existence check fails | Bash older than 4.3 | Use ${map[key]+x} |
| Empty key error | Empty string used as a key | Use 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
| Item | Value |
|---|---|
| Requirement | Bash 4.0 or later |
| Declaration | declare -A arr |
| Assign | arr[key]="value" |
| Read | ${arr[key]} |
| All keys | "${!arr[@]}" |
| All values | "${arr[@]}" |
| Count | ${#arr[@]} |
| Key exists | [[ -v arr[key] ]] (4.3+) |
| Delete | unset 'arr[key]' |
| Iterate | for k in "${!arr[@]}"; do ...; done |
| Empty key | Not 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=$1declaration 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.
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!