Given a bash associative array declared as `declare -A env_of`, how do you loop over its keys, what does `"${env_of[@]}"` give you instead, and can you rely on the order you get?
answer
- bang prefix means subscripts
- no bang means values
- hash order, not insertion order
- quote both the expansion and the key
- sort the keys when output must be stable
basics
~20 sLoop over "${!env_of[@]}" for the keys; "${env_of[@]}" expands to the values, and ${#env_of[@]} is the entry count. The order is bash's internal hash order — neither insertion order nor sorted — so sort explicitly if output must be stable.
solid answer
~40 sThe `!` prefix asks for keys: `for name in "${!env_of[@]}"; do ... done`, and inside the loop you read the value with `"${env_of[$name]}"`. Without the `!`, `"${env_of[@]}"` expands to the values only, with the keys gone, and `${#env_of[@]}` gives the number of entries. Both forms must stay double-quoted so each element survives as one word even when a key or value contains spaces. The important caveat is ordering: iteration follows bash's internal hash layout, which is not insertion order and not sorted, and it can change if you add or remove entries. Any script whose output is compared, committed, or diffed should sort the keys itself, for example `mapfile -t sorted < <(printf '%s\n' "${!env_of[@]}" | sort)`.
code
bash · 11 linesdeclare -A env_of=([web]=prod [db]=staging [cache]=dev)
for name in "${!env_of[@]}"; do
printf '%s -> %s\n' "$name" "${env_of[$name]}"
done
printf 'entries: %d\n' "${#env_of[@]}"
# stable output: impose an order yourself
mapfile -t sorted < <(printf '%s\n' "${!env_of[@]}" | sort)
printf '%s\n' "${sorted[@]}"go deeper
Memorise the pair: "${!map[@]}" gives keys, "${map[@]}" gives values, and always keep the double quotes so keys with spaces stay intact.
Explain that the ! form yields subscripts for both array kinds, that ${#map[@]} counts entries while ${#map} does not, and that ordering is hash order rather than insertion order.
Point out where unordered iteration actually bites — generated config, manifests, test fixtures — and show the sorted-pass idiom you use when a script's output is diffed or asserted on.
Own the convention: any script whose output enters version control or a comparison must produce deterministic ordering, and that rule should be visible in review checklists rather than rediscovered through a flaky test.
## Keys, values and count An associative array in bash (created with `declare -A`, bash 4.0+) is read through three expansions that are easy to mix up: ```bash declare -A env_of=([web]=prod [db]=staging [cache]=dev) "${!env_of[@]}" # the KEYS: web db cache "${env_of[@]}" # the VALUES: prod staging dev "${#env_of[@]}" # the COUNT: 3 ``` The leading `!` inside the braces is the "give me the subscripts" form. It is the same operator that yields indices for an indexed array; for a map it yields the string keys. There is no single expansion that hands you key/value pairs — you iterate keys and look each value up. ## The canonical loop ```bash for name in "${!env_of[@]}"; do printf '%s -> %s\n' "$name" "${env_of[$name]}" done ``` Two details in that loop matter. First, the quotes around `"${!env_of[@]}"`: unquoted, each key would be subject to word splitting, so a key like `us east` would arrive as two loop iterations. Second, `"${env_of[$name]}"` uses `$name` inside the subscript — for an associative array the subscript is a plain string, so the expansion of `$name` becomes the key directly. No arithmetic happens, unlike an indexed array. The `@` versus `*` distinction behaves as it does for indexed arrays: `"${env_of[*]}"` joins all values into a single word separated by the first character of `IFS`, which is almost never what you want in a loop. ## Counting, and the trap of `${#env_of}` `${#env_of[@]}` is the number of entries. `${#env_of}` is a different expansion entirely: it is the string *length* of the element whose key is `0`, which for most maps does not exist, so it prints `0` and looks like an empty map. When a count seems stuck at zero, check whether the `[@]` was dropped. ## Ordering is not defined This is the part candidates get wrong. Bash stores an associative array in a hash table, and iteration yields keys in the order the table happens to hold them. That order is: - **not** insertion order, - **not** sorted, - **not** stable across changes — adding or deleting keys can rearrange what you see, - and not guaranteed to be identical across bash versions. For a lookup table that is only ever queried by key, none of this matters. It matters enormously when the loop *prints* something: a generated config file, a manifest, a summary that a colleague will diff, or a test whose expected output is checked in. Those become intermittently failing tests and noisy diffs. The fix is to impose the order yourself: ```bash mapfile -t sorted < <(printf '%s\n' "${!env_of[@]}" | sort) for name in "${sorted[@]}"; do printf '%s=%s\n' "$name" "${env_of[$name]}" done ``` `mapfile` (also spelled `readarray`) reads lines into an indexed array, and `-t` strips the trailing newlines. Printing one key per line and sorting is safe for ordinary keys; keys containing newlines would need `sort -z` with null-delimited output. ## Keys that need quoting Keys are arbitrary strings, including ones with spaces: ```bash declare -A owner owner["payments api"]=team-a printf '%s\n' "${owner["payments api"]}" ``` Quote the subscript when the key contains spaces or characters the parser treats specially. Note also that `@` and `*` are reserved as subscripts — `${map[@]}` always means "all elements", so a literal key named `@` is not addressable that way. ## What to say in an interview Name the three expansions, show the loop with both sets of quotes, and volunteer the ordering caveat before you are asked — the ordering point is the one that shows you have shipped a script whose output someone else read.
- Why does `${#env_of}` print 0 when the map clearly has entries?Because `${#env_of}` is the string length of the element with key `0`, not the size of the array. Most maps have no key `0`, so it expands to the empty string and reports length 0. The entry count is `${#env_of[@]}` — the `[@]` is what makes it an array-wide expansion.
- What breaks if you write the loop as `for name in ${!env_of[@]}` without quotes?The keys undergo word splitting and pathname expansion after they are produced, so a key containing a space becomes two iterations, and a key containing `*` or `?` can be replaced by matching filenames. Double-quoting the expansion keeps each key as exactly one word.
saying these in an interview costs you the question
- Claims keys come back in insertion order
- Uses ${map[@]} expecting keys, not values
- Drops the quotes around "${!map[@]}"
- Uses ${#map} instead of ${#map[@]} for the count
- Thinks sorting is automatic because it is a hash