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.

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.
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:
service=("nginx" "mysql" "redis")
port=(80 3306 6379)
echo "Port mysql adalah ${port[1]}" # harus ingat: mysql di indeks 1This 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:
declare -A port
port[nginx]=80
port[mysql]=3306
port[redis]=6379
echo "Port mysql adalah ${port[mysql]}" # 3306This 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.
declare -AThis is the single most violated and most confusing rule. Associative arrays MUST be declared with declare -A before use:
declare -A port
port[nginx]=80Why 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:
port[nginx]=80
port[mysql]=3306
echo "${port[nginx]}" # 80 (kebetulan bekerja: 'nginx' dianggap 0)
echo "${port[mysql]}" # 3306 (kebetulan bekerja: 'mysql' juga 0)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.
Basic associative array operations are very similar to indexed arrays, with one difference: the keys are strings, not numbers:
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[@]}" # 3Notice 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.
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.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]}"
doneThe 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.
Let's practice the most common use: mapping service names to their ports, then producing a neat 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]}"
doneThe 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.
Second scenario: storing application configuration in an associative array, then using it to build a command line or report:
#!/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]}"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.
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.
#!/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 kaliLet'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:
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 -5With 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.
Here's a summary that can serve as a decision guide:
| Aspect | Indexed Array | Associative Array |
|---|---|---|
| Key | Numbers (0, 1, 2, ...) | Free strings (spaces allowed) |
| Declaration | Not required (default) | declare -A required |
| Iterate all | "${arr[@]}" | "${!dict[@]}" for keys |
| Element count | ${#arr[@]} | ${#dict[@]} |
| Best fit | Ordered lists, queues | Name-based mapping/lookup |
| Needs Bash | 2.0+ (basic) | 4.0+ |
| Real-world example | Server list, file list | Service→port, config, frequency counter |
| Mistake | Symptom | Solution |
|---|---|---|
Forgetting declare -A | Data overwrites itself (all keys become 0) | declare -A before using |
| Keys without double quotes | Spaced keys fail / split | dict["nama aplikasi"], "${dict["nama aplikasi"]}" |
| Bash below version 4 | associative array error / wrong behavior | Check ${BASH_VERSINFO[0]}; change strategy |
"${dict[@]}" to get keys | Gets values, not keys | Use "${!dict[@]}" (with !) |
Using "${arr[*]}" in iteration | All values merge into one string | Use "${arr[@]}" |
| Expecting a specific order | Key iteration order differs | Sort 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.
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!