Skip to content
shellhacks.dev
Bash

How to Check If a Key Exists in a Bash Associative Array

Test whether a key exists in a Bash associative array with -v, the +x expansion, or a default value, and tell a missing key from a key with an empty value.

6 min read
Summarize in AI
Illustration for "How to Check If a Key Exists in a Bash Associative Array": a shell script in an editor, a loop arrow, a table of open ports.

Reading a missing key from a Bash dictionary returns an empty string, which is the same thing a key set to "" returns. A script that needs the difference, such as a cache that stores empty results, must test for the key itself. The forms below assume Bash 5.2 and an array created as in associative arrays in Bash.

Test with -v

Shell
declare -A seen=([a]=1 [empty]="")

key=empty
if [[ -v seen[$key] ]]; then
  echo "key exists"
fi

key=missing
[[ -v seen[$key] ]] || echo "key missing"
Text
key exists
key missing

-v checks whether a variable or array element is set, and an element set to the empty string counts as set. Use it unquoted inside [[ ]] as shown, since the subscript is parsed as part of the name. Bash 4.3 added subscripts to -v; on Bash 4.2 it reports an error.

The expansion that works everywhere

Shell
if [[ -n ${seen[$key]+x} ]]; then
  echo "present"
fi

${var+word} expands to word only if var is set, including to the empty string, so the test is true for present keys and false for absent ones. This is the form to use in a script that has to run on Bash 4.0 and 4.1 as well. The same family of expansions is described in default values for variables.

Why testing the value fails

Shell
[[ -n ${seen[empty]} ]] || echo "treated as missing"

The value is empty, so -n is false even though the key is there. If your dictionary only ever holds non-empty values this is fine and shorter, but the day a value comes back empty the lookup starts reporting the key as absent.

Read safely under set -u

Shell
set -u
port=${ports[$name]-8080}

With set -u (see Bash strict mode) a missing key aborts the script with "unbound variable". ${ports[$name]-8080} supplies a default instead, and ${ports[$name]:-8080} also replaces an empty value. The Bash manual on parameter expansion lists every variant.

Three states of an array element

An element of a Bash array can be in one of three states: it does not exist, it exists with an empty value, or it exists with a non-empty value. Most scripts conflate the first two because simple expansion returns an empty string for both. The conflation causes bugs in cases like a configuration table where an empty value means "explicitly disabled" and a missing key means "use the default". The tests in the post separate the states: -v and +x tell you whether the key exists, and a second test on the value tells you whether it is empty. Combining them yields a small decision table, and writing it as a case statement keeps the three outcomes visible.

Shell
if [[ -z ${cfg[$k]+x} ]]; then
  echo "$k: not set, use default"
elif [[ -z ${cfg[$k]} ]]; then
  echo "$k: set to empty, disabled"
else
  echo "$k: ${cfg[$k]}"
fi

The first test uses the expansion that is empty only when the key is absent. The second runs only when the key exists, and tests the value. Putting them in a function called cfg_state that prints one word, missing, empty or value, lets the rest of the script use a plain case.

Differences between Bash versions

The -v test for array elements appeared in Bash 4.2 for plain variables and was extended to array subscripts in 4.3. The macOS system Bash is 3.2, which has neither associative arrays nor the test, so a script that runs there needs a different data structure. The expansion form with + works in every version that has associative arrays, which makes it the safest choice for a script that must support Bash 4.0 and 4.1, still found on very old enterprise systems. State the minimum version at the top of the script and check it: (( BASH_VERSINFO[0] >= 4 )) || exit gives a clear message instead of a syntax error halfway through.

Subscript pitfalls

Inside [[ -v arr[$key] ]], the subscript is subject to arithmetic evaluation rules for indexed arrays and to quote removal for associative ones, and keys with unusual characters may be parsed differently from what you expect. A key containing a closing bracket, a space, or the characters @ and * can match the special all-elements subscripts or terminate the subscript early. The safe approach is to store the key in a variable and quote it in each use, and to validate keys read from outside against an allowed pattern before use. If the keys can be arbitrary text, store them as values in a different structure, or hash them into safe identifiers.

Using the test in real code

The check-then-set pattern is the basis of memoisation: [[ -v cache[$k] ]] || cache[$k]=$(expensive "$k"). Because the test looks at the key and not the value, a result that is the empty string is cached too, which avoids calling the expensive function again for the same input. The same pattern implements a "visited" set in graph walks, and a "seen" set in the deduplication shown elsewhere. Wherever a key can legitimately hold nothing, test existence rather than content, and your script will treat a blank answer as an answer.

Passing and testing keys in functions

A helper that checks for a key should take the array name and the key, using a nameref for the array: has_key() { local -n _a=$1; [[ -n ${_a[$2]+x} ]]; }. Callers then write has_key cfg "$k", and the quoting details are in one place. Add a second helper that returns the value or a default, get_or() { local -n _a=$1; printf '%s' "${_a[$2]-$3}"; }, using the single-dash form so an empty value is preserved and only a missing key falls back. Small functions like these remove the repeated expansions that cause mistakes, and they are simple to test with a table of present, empty and missing keys.

When the script becomes a place where these helpers are used a dozen times, that is a signal to check whether another language would serve better. Python dictionaries and jq both give the three-state distinction naturally, with in and has, and without the quoting rules.

Where this does not apply

Keys that contain ], * or @ are awkward inside a subscript; assign the key to a variable first and always quote it in the other forms, as in "${seen["$key"]}". On the macOS Bash 3.2 none of this exists because there are no associative arrays, so a different approach such as grep -Fx against a key file is needed, covered in indexed versus associative arrays. For indexed arrays, -v arr[3] tests whether index 3 is set, which is not the same as having at least four elements.

A last case worth knowing is the loop that builds a cache: check the key with -v, compute the value only on a miss, and store it, even when the answer is empty. Without the existence test, an empty result looks like a miss every time and the expensive step runs again on each call. The Bash conditional expressions reference documents -v alongside the other tests.

Share

Keep reading

Get new posts by email

One email when a new post is published. No spam.

Unsubscribe in one click. No spam, ever.