skip to content

A bash associative array `declare -A opts` may legitimately hold empty-string values. How do you test whether a given key is present, as opposed to present-but-empty?

level: middleimportance: nice to knowfreq 30%

answer

  1. ask about the key, not the value
  2. empty string is not the same as absent
  3. -v takes a bare name, no dollar sign
  4. plus-form expansion tests set, not non-empty
  5. quote the argument to unset

basics

~20 s

Use [[ -v opts[key] ]], which is true when the key exists whatever its value (bash 4.3+ accepts array subscripts here). The pre-4.3 equivalent is [[ -n ${opts[key]+x} ]]. Testing [[ -n ${opts[key]} ]] conflates absent with empty.

solid answer

~50 s

The trap is that the obvious test asks about the value, not the key. `[[ -n ${opts[key]} ]]` is false both when the key is missing and when it holds an empty string, and `${opts[key]:-default}` substitutes the default in both cases too. To ask about presence, use `[[ -v opts[key] ]]` — bash 4.2 added `-v` for variables and bash 4.3 taught it array subscripts — which is true whenever the key exists, empty value or not. If you must support bash 4.0 or 4.1, `${opts[key]+x}` is the classic stand-in: the form without a colon substitutes only when the key is *set*, so `[[ -n ${opts[key]+x} ]]` tests presence. The distinction matters whenever an empty string is meaningful data, such as a config map where a key set to empty means "explicitly blank" rather than "not configured".

code

bash · 9 lines
bash
declare -A opts
opts[verbose]=''

[[ -v opts[verbose] ]] && echo 'key present'          # bash 4.3+
[[ -n ${opts[verbose]} ]] || echo 'value is empty'
[[ -n ${opts[missing]+x} ]] || echo 'key missing'      # pre-4.3 form

unset 'opts[verbose]'                                  # quote it
[[ -v opts[verbose] ]] || echo 'key deleted'

go deeper

for a junior

Know that an empty value and a missing key look identical to [[ -n ... ]], and that [[ -v map[key] ]] is the test that asks about the key itself.

for a middle

Explain that -v takes a bare name rather than an expansion, that array subscripts in -v need bash 4.3, and that ${map[k]+x} is the older presence test because the colonless form triggers on set rather than non-empty.

for a senior

Show why this matters in production data — a config map where blank is a deliberate value — and mention the deletion hazards: quote the unset argument against globbing, and prefer map=() over unset map so the associative attribute survives.

for a principal

Decide early whether empty is a legal value in your script's data model; if it is, presence tests and clearing semantics become an interface contract for every helper that reads the map, not an incidental detail.

## Two different questions "Is this key in the map?" and "does this key have a non-empty value?" are different questions, and bash's most reachable test answers the second one. In a map used as a lookup table this rarely matters, because every key you store has a real value. It matters as soon as the empty string is meaningful — a parsed config where `LOG_FILE=` means "deliberately blank", a set of seen items where the value is a placeholder, a flags map where presence is the whole signal. ```bash declare -A opts opts[verbose]='' # key exists, value empty [[ -n ${opts[verbose]} ]] # FALSE - asks about the value [[ -v opts[verbose] ]] # TRUE - asks about the key [[ -v opts[missing] ]] # FALSE - key really is absent ``` ## The `-v` operator `-v` is a unary test that reports whether a *name* is set. Bash 4.2 introduced it for ordinary variables; bash 4.3 extended it to understand array subscripts, which is the form you need here. Note that the operand is written bare — `[[ -v opts[verbose] ]]`, not `[[ -v ${opts[verbose]} ]]`. You are naming the thing to test, not expanding it. Passing the expansion instead would hand `-v` the *value* as a name, which is a different (and usually meaningless) test. Because `[[ ]]` performs no word splitting or pathname expansion on its operands, the subscript needs no quoting even when it holds a variable: `[[ -v opts[$key] ]]` is safe. ## The pre-4.3 alternative If you are targeting older bash 4.x, use the set-versus-unset form of parameter expansion. `${var+word}` — the version *without* a colon — substitutes `word` only when the variable is set, regardless of whether it is empty; the colon form `${var:+word}` additionally requires a non-empty value. Applied to a subscript: ```bash if [[ -n ${opts[verbose]+x} ]]; then echo 'key exists (possibly empty)' fi ``` The `x` is arbitrary — any non-empty placeholder works. This idiom predates `-v` and still appears widely in scripts that must run on a range of bash versions. ## Removing a key, and the quoting trap Deletion is `unset`, and it comes with a real hazard: ```bash unset 'opts[verbose]' # correct - quoted unset opts[verbose] # risky - the word is a glob ``` Outside `[[ ]]`, the word `opts[verbose]` is an ordinary command argument, and `[...]` is a pathname-expansion bracket expression. If the current directory happens to contain a file matching that pattern — `optsv`, `optse`, `optsr` and so on — the shell replaces the argument with that filename and `unset` operates on the wrong name, silently leaving your key in place. Quoting the argument stops the expansion. ShellCheck warns about unquoted `unset` arguments for exactly this reason. The same reasoning applies to `unset 'opts[$key]'`, where the key's own contents could contain glob characters. Also note the difference between deleting a key and emptying it: `unset 'opts[k]'` makes `[[ -v opts[k] ]]` false, while `opts[k]=''` leaves it true. ## Wiping the whole map `unset opts` removes the variable entirely, including its `-A` attribute — so a later `opts[k]=v` would recreate it as an *indexed* array with the arithmetic-subscript behaviour that entails. If you want an empty map that is still a map, re-declare it: `unset opts; declare -A opts`, or assign `opts=()`, which empties the array while keeping the associative attribute. ## What good looks like A script that treats presence as data should say so in the test it writes. Reaching for `-v` (or the `+x` form) instead of `-n` on the value is a small tell that the author has thought about what an empty value means in their data model, which is the point the question is really probing.

  • Why is `unset 'opts[key]'` written with quotes?
    Because outside `[[ ]]` the word `opts[key]` is a glob: the brackets form a character class, so if a file named `optsk`, `optse` or `optsy` exists in the current directory, pathname expansion rewrites the argument and `unset` targets the wrong name while your key survives. Quoting suppresses the expansion; ShellCheck flags the unquoted form.
  • What is the difference between `unset opts` and `opts=()`?
    `opts=()` empties the array but keeps its associative attribute, so later assignments still use string keys. `unset opts` destroys the variable and its `-A` attribute, so the next subscripted assignment silently recreates it as an indexed array with arithmetic subscripts. Prefer `opts=()` when you only want to clear it.

saying these in an interview costs you the question

  • Uses [[ -n ${map[k]} ]] to test key presence
  • Writes [[ -v ${map[k]} ]] with a dollar sign
  • Thinks ${map[k]:-x} distinguishes absent from empty
  • Leaves unset map[k] unquoted
  • Assumes unset map keeps the associative attribute

context