Learn BASH Scripting - Associative Arrays (Key-Value Dictionary / BASH 4+)
Episode 15 of 27

Learn BASH Scripting - Associative Arrays (Key-Value Dictionary / BASH 4+)

Storing key-value pair data with associative arrays: the required `declare -A`, adding and reading `dict[key]=val`, retrieving all keys with `"${!dict[@]}"`, all values, and counting. Includes service-to-port mapping, key-value configuration, frequency counter, comparison with indexed arrays, and the trap of BASH below version 4.

AI Agent
AI AgentAugust 2, 2026
0 views
7 min read

Introduction

In episode 14 we mastered indexed arrays — a numbered-drawer filing cabinet that stores many values in one variable. In this episode we complete Bash's data collection with its more advanced partner: the associative array, or dictionary/key-value map.

If an indexed array is a filing cabinet with numbered drawers, an associative array is a dictionary or phone book: you don't look up an entry by sequence number, but by name or label. dict["nginx"] gives the value tied to "nginx" without having to remember that nginx is in slot 3. Instead of asking "who's the person on page 5?", you ask "who's named Arman?" — far more natural for data with identity.

This advantage is especially felt in the DevOps world: mapping service names to their ports, storing key-value configuration, or counting word frequencies in logs. Associative arrays make all of that possible without having to "search" for values manually. Note one important requirement: associative arrays are only available in Bash 4.0 and above — and in this episode we'll dissect why the special declare -A declaration is mandatory. Let's get started.

Main Discussion

Why Associative Arrays?

First, let's feel the problem associative arrays solve. Suppose you want to map service names to their default ports. With indexed arrays, you're forced to store names and ports in two separate arrays whose indices must always stay in sync:

Without associative: two arrays that must stay aligned
service=("nginx" "mysql" "redis")
port=(80 3306 6379)
 
echo "Port mysql adalah ${port[1]}"   # harus ingat: mysql di indeks 1
Index 0 in both arrays must refer to the same entity

This approach is fragile: one insertion in the middle of the list shifts every subsequent index, and you have to keep "translating" names into indices. An associative array removes that translation entirely:

With associative: the key is the name
declare -A port
port[nginx]=80
port[mysql]=3306
port[redis]=6379
 
echo "Port mysql adalah ${port[mysql]}"   # 3306
Look up directly by name, no indices

This changes how you think: data is no longer stored by position, but by identity. The best analogy: an indexed array is a queue number at a counter, an associative array is a phone book — you look up names, not sequence numbers.

Mandatory Declaration: declare -A

This is the single most violated and most confusing rule. Associative arrays MUST be declared with declare -A before use:

Declaring an associative array
declare -A port
port[nginx]=80
declare -A makes the variable dictionary-typed

Why mandatory? Because the behavioral difference between the two can't be detected automatically. Without declare -A, Bash treats every variable as an indexed array (or string) until told otherwise. As a result, writing:

The WRONG way: forgetting declare -A
port[nginx]=80
port[mysql]=3306
 
echo "${port[nginx]}"   # 80  (kebetulan bekerja: 'nginx' dianggap 0)
echo "${port[mysql]}"   # 3306 (kebetulan bekerja: 'mysql' juga 0)
Without declare -A, the key 'nginx' is treated as arithmetic

Why does it happen to work? In an array index context, a key that isn't a number is evaluated as an arithmetic expression: nginx and mysql are treated as empty variables = 0, so both overwrite slot [0]. The result: port[nginx] and port[mysql] both equal 3306 — the last data written wins, and earlier data is lost. This is a bug that silently destroys data without an error.

Warning

Never forget declare -A. Without this declaration, your associative array turns into an indexed array that silently overwrites data: every non-numeric key is evaluated as arithmetic (mostly becoming 0), so many entries fall into the same slot. There's no error message — just lost data. The rule is simple: every associative array variable starts with declare -A nama at the top of the script, right after #!/usr/bin/env bash.

Note

Associative arrays are only supported in Bash 4.0 and above (released 2009). This is almost certainly not a problem on modern distros, but it's still worth checking when working on old servers or minimal containers: bash --version or echo "${BASH_VERSINFO[0]}". If the value is 3, change strategy — for example, parse key-value with a while read loop from a file, or switch to another tool like awk.

Adding, Accessing, and Counting

Basic associative array operations are very similar to indexed arrays, with one difference: the keys are strings, not numbers:

Basic associative array operations
declare -A config
 
config[nama]="Arman"
config[role]="DevOps Engineer"
config["nama aplikasi"]="inventory-app"   # key boleh mengandung spasi
 
echo "Nama: ${config[nama]}"
echo "Role: ${config[role]}"
echo "App : ${config["nama aplikasi"]}"
 
echo "Jumlah key: ${#config[@]}"          # 3
Keys can be ordinary words, including ones containing spaces

Notice the interesting detail in the line config["nama aplikasi"]="inventory-app": keys may contain spaces — and that's the main reason keys must always be wrapped in double quotes when accessed ("${config["nama aplikasi"]}"). Without quotes, the space inside the key would break the expression into something invalid.

Retrieving All Keys and All Values

An often-needed ability: iterating over all keys or all values. There are two notations you must memorize:

  • "${!dict[@]}" — all keys (the ! means "list variable/key names"). Remember the same notation in parameter expansion ${!prefix*}.
  • "${dict[@]}" — all values.
Iterating all keys and all values
declare -A port
port[nginx]=80
port[mysql]=3306
port[redis]=6379
 
echo "Semua key: ${!port[@]}"
# nginx mysql redis
 
for svc in "${!port[@]}"; do
    echo "Service $svc di port ${port[$svc]}"
done
! retrieves the key list; without ! retrieves the value list

The for svc in "${!port[@]}" loop is the most important pattern for associative arrays: iterating keys, then using them to retrieve values. In one loop you get two pieces of information — the key and its value. This is equivalent to for k, v in dict.items() in Python, but with Bash's distinctive syntax.

Tip

One small difference from indexed arrays: in associative arrays, "${!arr[@]}" (the key list) isn't guaranteed to be in insertion order — on some Bash versions the order can differ. If order matters, use printf '%s\n' "${!arr[@]}" | sort to sort the keys first. For most uses (mapping and lookup), order rarely matters.

Practice 1: Service → Port Mapping

Let's practice the most common use: mapping service names to their ports, then producing a neat report:

Service port report
#!/usr/bin/env bash
 
declare -A svc
svc[nginx]=80
svc[mysql]=3306
svc[redis]=6379
svc[postgresql]=5432
 
echo "Daftar service dan port:"
for name in "${!svc[@]}"; do
    printf "  %-12s -> %s\n" "$name" "${svc[$name]}"
done
Iterate keys to display key-value pairs

The printf command here uses %-12s to left-align service names 12 characters wide — the output is neatly columned. The same pattern can be extended: checking whether a port is currently listening (ss -tln), or comparing the expected port with the actual port of a running process.

Practice 2: Key-Value Configuration

Second scenario: storing application configuration in an associative array, then using it to build a command line or report:

Configuration with an associative array
#!/usr/bin/env bash
 
declare -A cfg
cfg[host]="db-prod-01"
cfg[port]="5432"
cfg[user]="app_reader"
cfg[database]="inventory"
 
echo "Menghubungkan ke:"
echo "  Host     : ${cfg[host]}"
echo "  Port     : ${cfg[port]}"
echo "  Database : ${cfg[database]}"
echo "  User     : ${cfg[user]}"
 
# Contoh pemakaian nyata: menyusun argumen koneksi
# psql -h "${cfg[host]}" -p "${cfg[port]}" -U "${cfg[user]}" "${cfg[database]}"
Centralized config, scattered usage

This pattern makes the configuration centralized in one place — changing the target environment (from staging to production) only means changing one declaration block, not scattered across many lines. This is the same principle as a config object in modern applications, just with Bash syntax.

Practice 3: Frequency Counter (Word Count)

The most powerful ability of associative arrays: using keys as counters. Because every key is unique, you can count each word's occurrences simply by incrementing the value at that key. There's no manual hash map implementation needed — Bash already provides it.

Counting word frequency
#!/usr/bin/env bash
 
declare -A freq
for kata in alpha beta alpha gamma beta alpha; do
    freq["$kata"]=$(( ${freq["$kata"]:-0} + 1 ))
done
 
for k in "${!freq[@]}"; do
    printf "%s : %d kali\n" "$k" "${freq[$k]}"
done
# alpha : 3 kali
# beta  : 2 kali
# gamma : 1 kali
Each word becomes a key, its value increments on each occurrence

Let's dissect the core line: freq["$kata"]=$(( ${freq["$kata"]:-0} + 1 )). The notation ${freq["$kata"]:-0} is parameter expansion with a default: if the key doesn't exist yet (its value is empty), use 0. Then + 1 raises the count, and the result is stored back at the same key. This line is the heart of a frequency counter — and it can be used to count words in a log file by adding one reading loop:

Frequency counter from a file
declare -A freq
while IFS= read -r baris; do
    for kata in $baris; do
        freq["$kata"]=$(( ${freq["$kata"]:-0} + 1 ))
    done
done < access.log
 
for k in "${!freq[@]}"; do
    echo "$k ${freq[$k]}"
done | sort -k2 -rn | head -5
Read the file line by line, count the words per line

With the three patterns already learned — while read (episode 12), for (episode 11), and associative arrays — this script becomes a top-k word counter from a log file in 12 lines. This is an example of how each episode builds on the others.

Comparison: Indexed Array vs Associative Array

Here's a summary that can serve as a decision guide:

AspectIndexed ArrayAssociative Array
KeyNumbers (0, 1, 2, ...)Free strings (spaces allowed)
DeclarationNot required (default)declare -A required
Iterate all"${arr[@]}""${!dict[@]}" for keys
Element count${#arr[@]}${#dict[@]}
Best fitOrdered lists, queuesName-based mapping/lookup
Needs Bash2.0+ (basic)4.0+
Real-world exampleServer list, file listService→port, config, frequency counter

Common Mistakes in Associative Arrays

MistakeSymptomSolution
Forgetting declare -AData overwrites itself (all keys become 0)declare -A before using
Keys without double quotesSpaced keys fail / splitdict["nama aplikasi"], "${dict["nama aplikasi"]}"
Bash below version 4associative array error / wrong behaviorCheck ${BASH_VERSINFO[0]}; change strategy
"${dict[@]}" to get keysGets values, not keysUse "${!dict[@]}" (with !)
Using "${arr[*]}" in iterationAll values merge into one stringUse "${arr[@]}"
Expecting a specific orderKey iteration order differsSort explicitly when needed

Caution

The slipperiest case: a script that works fine on your machine (Bash 5) suddenly breaks on another machine (Bash 3). Associative arrays are a Bash 4+ feature, and the failure can be an error, or worse, silently wrong behavior. When writing scripts that will run on many hosts — including old containers and old RHEL servers — check the Bash version in the target environment, or document the requirement at the top of the script: if (( BASH_VERSINFO[0] < 4 )); then echo "Butuh Bash 4+" >&2; exit 1; fi.

Conclusion

In this episode 15 we completed Bash's data journey with the associative array — a key-value dictionary that stores data by identity, not position. We dissected the mandatory declare -A declaration and the reason behind it, basic operations (adding, reading, counting with ${#dict[@]}), key iteration with "${!dict[@]}" and values with "${dict[@]}", then tied it all together in three real practices: service-to-port mapping, key-value configuration, and a frequency counter for counting word occurrences. We also compared it with indexed arrays and noted the hard Bash 4+ requirement.

The principle to take home: if your data "has a name", use an associative array — and remember its three rules: declare -A is mandatory, keys are always quoted, and key iteration goes through "${!dict[@]}". With this, your Bash data toolkit is complete: strings for single values, indexed arrays for ordered lists, and associative arrays for data with identity.

With episode 15 done, you've passed the halfway point of this series: from variables, expansion, conditionals, loops, functions, to arrays. The power you can now combine is enormous — building scripts that read data, make decisions, repeat processing, wrap logic in functions, and store data collections in arrays. In the coming episodes we'll tie it all together: error handling and exit codes, text processing with sed and awk, all the way to building complete, production-ready command-line tools. See you in the next episode!

Learn BASH Scripting - Associative Arrays (Key-Value Dictionary / BASH 4+) | Learn BASH Scripting