122

Using:

set -o nounset
  1. Having an indexed array like:

    myArray=( "red" "black" "blue" )
    

    What is the shortest way to check if element 1 is set?
    I sometimes use the following:

    test "${#myArray[@]}" -gt "1" && echo "1 exists" || echo "1 doesn't exist"
    

    I would like to know if there's a preferred one.

  2. How to deal with non-consecutive indexes?

    myArray=()
    myArray[12]="red"
    myArray[51]="black"
    myArray[129]="blue"
    

    How to quick check that 51 is already set for example?

  3. How to deal with associative arrays?

    declare -A myArray
    myArray["key1"]="red"
    myArray["key2"]="black"
    myArray["key3"]="blue"
    

    How to quick check that key2 is already used for example?

wjandrea
  • 28,235
  • 9
  • 60
  • 81
Luca Borrione
  • 16,324
  • 8
  • 52
  • 66

11 Answers11

166

To check if the element is set (applies to both indexed and associative array)

[ "${array[key]+abc}" ] && echo "exists"

Basically what ${array[key]+abc} does is

  • if array[key] is set, return abc
  • if array[key] is not set, return nothing

References:
  1. See Parameter Expansion in Bash manual and the little note

if the colon is omitted, the operator tests only for existence [of parameter]

  1. This answer is actually adapted from the answers for this SO question: How to tell if a string is not defined in a bash shell script?

A wrapper function:

exists(){
  if [ "$2" != in ]; then
    echo "Incorrect usage."
    echo "Correct usage: exists {key} in {array}"
    return
  fi   
  eval '[ ${'$3'[$1]+muahaha} ]'  
}

For example

if ! exists key in array; then echo "No such array element"; fi 
chepner
  • 497,756
  • 71
  • 530
  • 681
doubleDown
  • 8,048
  • 1
  • 32
  • 48
  • I solved in this way: if test "${myArray['key_or_index']+isset}"; then echo "yes"; else echo "no"; fi; It seems to me the simplest way and it applies to indexed and associative arrays. Thank you – Luca Borrione Nov 04 '12 at 19:13
  • 1
    @doubleDown How do you use [ ${array[key]+abc} ] in an if clause to only do something if [ ${array[key]+abc} ] doesn't exist? – olala Feb 19 '14 at 17:43
  • 1
    Also doesn't work when you accidentally query enumerated array as associative one. – Tomáš Zato Oct 06 '15 at 11:58
  • 2
    @duanev: Without `+abc`, `[ ${array[key]} ]` will evaluate to false if the element is indeed set but to an empty value, so it's actually testing the value non-emptiness rather than the key existence. – musiphil Feb 18 '20 at 17:58
  • @duanev Without `+abc` also failed when `array[key]` is not set and `set -u` is effective. – Ding-Yi Chen Mar 27 '20 at 07:33
  • @Ding-YiChen, musiphil oooh, good catches y'all - misinformation removed. – duanev Mar 28 '20 at 17:53
  • I am confused. I have this (using your function): `ERRORS['DB']='gg' if exists 'DDD' in ERRORS; then echo "1"; else echo "2"; fi if exists 'DB' in ERRORS; then echo "3"; else echo "4"; fi` But it returns 1 and 3. I expected it to return 2 and 3. Is my expectation correct or am I missing something ? – Itération 122442 Apr 07 '20 at 15:14
  • `[[ ${array[key]+Y} ]] && echo Y || echo N` – Thamme Gowda May 21 '20 at 02:18
  • @doubleDown I think I have improved on your answer, and added flexibility to deal with arrays versus maps. I add a -n as an explicit check for a non-zero length string, which technically isn't necessary, but I think makes the code clearer, showing that a true result needs to evaluate a string of substance. I agree that a key that does not exist should return "nothing". – Anthony Rutledge Aug 08 '21 at 14:55
  • 3
    **But `eval` is evil!!** Try this: `exists foo in 'O};cat /etc/passwd;echo -e \\e[5m'` for sample!! – F. Hauri - Give Up GitHub Sep 02 '21 at 08:27
  • If you are like me, and have the luxury of namerefs in bash, you can use those instead of evil eval. `local -n arr=$3; [ ${arr[$1]+abc} ]` – Esa Lindqvist Dec 21 '21 at 05:59
  • In shells with `declare -p` or `typeset -p` support you can scan for the key in the output, e.g. with glob matching `[ "$(declare -p arr)" = *[[:space:]]\[key\]=* ] && echo "exists"` – Joshua Skrzypek Nov 01 '22 at 15:10
70

From man bash, conditional expressions:

-v varname
              True if the shell variable varname is set (has been assigned a value).

example:

declare -A foo
foo[bar]="this is bar"
foo[baz]=""
if [[ -v "foo[bar]" ]] ; then
  echo "foo[bar] is set"
fi
if [[ -v "foo[baz]" ]] ; then
  echo "foo[baz] is set"
fi
if [[ -v "foo[quux]" ]] ; then
  echo "foo[quux] is set"
fi

This will show that both foo[bar] and foo[baz] are set (even though the latter is set to an empty value) and foo[quux] is not.

David Tonhofer
  • 14,559
  • 5
  • 55
  • 51
Vineet
  • 2,103
  • 14
  • 9
  • 2
    I missed it at a quick glance; notice that the typical array expansion syntax is not used. – Nathan Chappell Feb 23 '19 at 20:41
  • With `set -u`, why does `[[ -v "${foo[bar]}" ]]` produce an unbound variable error if `bar` doesn't exist in the dictionary? Works fine without the `${}`; I'm just used to using it for everything by default. – bgfvdu3w Feb 23 '20 at 04:44
  • 3
    `"${foo[bar]}"` evaluates the array variable first, so the `[[ -v` command tests for a variable with the name of that value – andysh Apr 27 '20 at 15:01
  • The presence, or absence, of a value for key is not the question here. Determining if the key exists is sufficient. This is actually a wrong answer because `-v` only returns "true **if** the variable name has been set (has been assigned a value". That goes beyond the requirements here. – Anthony Rutledge Aug 12 '21 at 07:37
23

New answer

From version 4.2 of (and newer), there is a new -v option to built-in test command.

From version 4.3, this test could address element of arrays.

array=([12]="red" [51]="black" [129]="blue")

for i in 10 12 30 {50..52} {128..131};do
    if [ -v 'array[i]' ];then
        echo "Variable 'array[$i]' is defined"
    else
        echo "Variable 'array[$i]' not exist"
    fi
done
Variable 'array[10]' not exist
Variable 'array[12]' is defined
Variable 'array[30]' not exist
Variable 'array[50]' not exist
Variable 'array[51]' is defined
Variable 'array[52]' not exist
Variable 'array[128]' not exist
Variable 'array[129]' is defined
Variable 'array[130]' not exist
Variable 'array[131]' not exist

Note: regarding ssc's comment, I've single quoted 'array[i]' in -v test, in order to satisfy shellcheck's error SC2208. This seem not really required here, because there is no glob character in array[i], anyway...

This work with associative arrays in same way:

declare -A aArray=([foo]="bar" [bar]="baz" [baz]=$'Hello world\041')

for i in alpha bar baz dummy foo test;do
    if [ -v 'aArray[$i]' ];then
        echo "Variable 'aArray[$i]' is defined"
    else
        echo "Variable 'aArray[$i]' not exist"
    fi
done
Variable 'aArray[alpha]' not exist
Variable 'aArray[bar]' is defined
Variable 'aArray[baz]' is defined
Variable 'aArray[dummy]' not exist
Variable 'aArray[foo]' is defined
Variable 'aArray[test]' not exist

With a little difference:
In regular arrays, variable between brackets ([i]) is integer, so dollar symbol ($) is not required, but for associative array, as key is a word, $ is required ([$i])!

Old answer for prior to V4.2

Unfortunately, bash give no way to make difference betwen empty and undefined variable.

But there is some ways:

$ array=()
$ array[12]="red"
$ array[51]="black"
$ array[129]="blue"

$ echo ${array[@]}
red black blue

$ echo ${!array[@]}
12 51 129

$ echo "${#array[@]}"
3

$ printf "%s\n" ${!array[@]}|grep -q ^51$ && echo 51 exist
51 exist

$ printf "%s\n" ${!array[@]}|grep -q ^52$ && echo 52 exist

(give no answer)

And for associative array, you could use the same:

$ unset array
$ declare -A array
$ array["key1"]="red"
$ array["key2"]="black"
$ array["key3"]="blue"
$ echo ${array[@]}
blue black red

$ echo ${!array[@]}
key3 key2 key1

$ echo ${#array[@]}
3

$ set | grep ^array=
array=([key3]="blue" [key2]="black" [key1]="red" )

$ printf "%s\n" ${!array[@]}|grep -q ^key2$ && echo key2 exist || echo key2 not exist
key2 exist

$ printf "%s\n" ${!array[@]}|grep -q ^key5$ && echo key5 exist || echo key5 not exist
key5 not exist

You could do the job without the need of externals tools (no printf|grep as pure bash), and why not, build checkIfExist() as a new bash function:

$ checkIfExist() {
    eval 'local keys=${!'$1'[@]}';
    eval "case '$2' in
        ${keys// /|}) return 0 ;;
        * ) return 1 ;;
      esac";
}

$ checkIfExist array key2 && echo exist || echo don\'t
exist

$ checkIfExist array key5 && echo exist || echo don\'t
don't

or even create a new getIfExist bash function that return the desired value and exit with false result-code if desired value not exist:

$ getIfExist() {
    eval 'local keys=${!'$1'[@]}';
    eval "case '$2' in
        ${keys// /|}) echo \${$1[$2]};return 0 ;;
        * ) return 1 ;;
      esac";
}

$ getIfExist array key1
red
$ echo $?
0

$ # now with an empty defined value
$ array["key4"]=""
$ getIfExist array key4

$ echo $?
0
$ getIfExist array key5
$ echo $?
1
F. Hauri - Give Up GitHub
  • 64,122
  • 17
  • 116
  • 137
  • Ok for downvotes: This answer was posted before V4.2 of [tag:bash]! Answer edited! – F. Hauri - Give Up GitHub Feb 04 '20 at 14:59
  • Doesn't work on `bash 4.2.46`. Does work on `bash 4.4.12`. – Irfy Apr 11 '20 at 16:45
  • @Irfy What does'nt work? `-v` option of `test` or `getIfExist` function? – F. Hauri - Give Up GitHub Apr 12 '20 at 06:50
  • The `-v` doesn't work on arrays on my CentOS 7.7.1908 with bash 4.2.46. The code from your first code block prints `not exist` in all cases under that bash. (Also tried `[$i]` instead of `[i]`, no difference) – Irfy Apr 27 '20 at 13:42
  • 2
    `-v` was added to bash-4.2 *BUT* support for checking array indexes was not added until bash-4.3. – mr.spuratic Nov 18 '20 at 12:33
  • 1
    @mr.spuratic Thanks, for comment! – F. Hauri - Give Up GitHub Nov 18 '20 at 19:10
  • 5
    This whole page is a testament to the monumental failure that is bash. For the most basic thing, it is filled with 20 counter-intuitive methods and all with comments like "(doesn't) work for me/this or that version." – Emanuel P May 04 '21 at 08:44
  • 2
    Thanks, works great for me on macOS / brew bash 5.1.8 :-) `shellcheck` reports [SC2208](https://github.com/koalaman/shellcheck/wiki/SC2208) for both _New answer_ code samples: Apparently, the `if` should use `[[ ... ]]` instead of `[ ... ]` or the expression after `-v` should be quoted, e.g. `if [[ -v aArray[$i] ]]` or `if [ -v 'aArray[$i]' ]`. Beats me, I usually just do what `shellcheck` tells me... – ssc Sep 01 '21 at 20:17
  • @JohanBoulé Which part of the code, under which version of bash?? – F. Hauri - Give Up GitHub Mar 07 '23 at 22:00
  • @F.Hauri-GiveUpGitHub Section "New answer" with bash 4.2.46 – Johan Boulé Mar 07 '23 at 23:57
  • 1
    @JohanBoulé Tested with v4.2.46: You're right this seem not work. But the second part with `checkIfExist` function seem work fine. If else, you could try to replace. in 1st sample. `if [ -v 'array[i]' ];then ...` by `if ${aArray[$i]+:} false;then ...` (without bracket!) I've just tested this trick now, under 4.2.46(1)-release. – F. Hauri - Give Up GitHub Mar 08 '23 at 06:50
12

What about a -n test and the :- operator?

For example, this script:

#!/usr/bin/env bash

set -e
set -u

declare -A sample

sample["ABC"]=2
sample["DEF"]=3

if [[ -n "${sample['ABC']:-}" ]]; then
  echo "ABC is set"
fi

if [[ -n "${sample['DEF']:-}" ]]; then
  echo "DEF is set"
fi

if [[ -n "${sample['GHI']:-}" ]]; then
  echo "GHI is set"
fi

Prints:

ABC is set
DEF is set
GuyPaddock
  • 2,233
  • 2
  • 23
  • 27
  • Great compact solution which responds as expected for an empty string – Ryan Dugan Jan 13 '20 at 01:01
  • Huge upvote for this solution that works with set -u in bash 4.2. Right now, I am working with Oracle Database on Red Hat 7, and bash 4.2 is installed there. – Brian Fitzgerald Feb 28 '21 at 17:23
  • This should be the accepted answer! Worked for me (bash 4.2.46) while the accepted -v answer did not. – freddieknets Apr 12 '21 at 10:07
  • @GuyPaddock Are you certain that including the ":" with your array variables is giving you the response that you want? Null, or empty string, values are valid values for an array element, and we don't want to test them. We only want to test for the existence of the index, or key. "if the colon is included, the operator tests for both parameter’s existence **and that its value** is not null" https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Shell-Parameter-Expansion – Anthony Rutledge Aug 08 '21 at 12:04
  • @AnthonyRutledge In my case, I did not want null values either. – GuyPaddock Aug 11 '21 at 20:20
  • 1
    `-n` simply works. https://github.com/koalaman/shellcheck/wiki/SC2236 – Chirag Arora Jan 06 '22 at 19:30
  • 1
    You're correct, @ChiragArora. I was not aware of that option when I wrote this answer originally. – GuyPaddock Feb 02 '22 at 19:04
5

tested in bash 4.3.39(1)-release

declare -A fmap
fmap['foo']="boo"

key='foo'
# should echo foo is set to 'boo'
if [[ -z "${fmap[${key}]}" ]]; then echo "$key is unset in fmap"; else echo "${key} is set to '${fmap[${key}]}'"; fi
key='blah'
# should echo blah is unset in fmap
if [[ -z "${fmap[${key}]}" ]]; then echo "$key is unset in fmap"; else echo "${key} is set to '${fmap[${key}]}'"; fi
gdoubleod
  • 1,268
  • 3
  • 19
  • 33
  • That fails when the value of the key is an empty string. As a workaround you can use the `+` parameter expansion to replace an empty value with some placeholder like an underscore. For example `declare -A a[x]=;[[ ${a[x]} ]];echo $?` prints `1`, but `declare -A a[x]=;[[ ${a[x]+_} ]];echo $?` prints `0`. – nisetama Dec 06 '15 at 21:33
2

Reiterating this from Thamme:

[[ ${array[key]+Y} ]] && echo Y || echo N

This tests if the variable/array element exists, including if it is set to a null value. This works with a wider range of bash versions than -v and doesn't appear sensitive to things like set -u. If you see a "bad array subscript" using this method please post an example.

dippas
  • 58,591
  • 15
  • 114
  • 126
2

Both in the case of arrays and hash maps I find the easiest and more straightforward solution is to use the matching operator =~.

For arrays:

myArray=("red" "black" "blue")
if [[ " ${myArray[@]} " =~ " blue " ]]; then
    echo "blue exists in myArray"
else
    echo "blue does not exist in myArray"
fi

NOTE: The spaces around the array guarantee the first and last element can match. The spaces around the value guarantee an exact match.

For hash maps, it's actually the same solution since printing a hash map as a string gives you a list of its values.

declare -A myMap
myMap=(
    ["key1"]="red"
    ["key2"]="black"
    ["key3"]="blue"
)
if [[ " ${myMap[@]} " =~ " blue " ]]; then
   echo "blue exists in myMap"
else
   echo "blue does not exist in myMap"
fi

But what if you would like to check whether a key exists in a hash map? In the case you can use the ! operator which gives you the list of keys in a hash map.

if [[ " ${!myMap[@]} " =~ " key3 " ]]; then
   echo "key3 exists in myMap"
else
   echo "key3 does not exist in myMap"
fi
Diego Pino
  • 11,278
  • 1
  • 55
  • 57
1

This is the easiest way I found for scripts.

<search> is the string you want to find, ASSOC_ARRAY the name of the variable holding your associative array.

Dependign on what you want to achieve:

key exists:

if grep -qe "<search>" <(echo "${!ASSOC_ARRAY[@]}"); then echo key is present; fi

key exists not:

if ! grep -qe "<search>" <(echo "${!ASSOC_ARRAY[@]}"); then echo key not present; fi

value exists:

if grep -qe "<search>" <(echo "${ASSOC_ARRAY[@]}"); then echo value is present; fi

value exists not:

if ! grep -qe "<search>" <(echo "${ASSOC_ARRAY[@]}"); then echo value not present; fi
sjas
  • 18,644
  • 14
  • 87
  • 92
1

I wrote a function to check if a key exists in an array in Bash:

# Check if array key exists
# Usage: array_key_exists $array_name $key
# Returns: 0 = key exists, 1 = key does NOT exist
function array_key_exists() {
    local _array_name="$1"
    local _key="$2"
    local _cmd='echo ${!'$_array_name'[@]}'
    local _array_keys=($(eval $_cmd))
    local _key_exists=$(echo " ${_array_keys[@]} " | grep " $_key " &>/dev/null; echo $?)
    [[ "$_key_exists" = "0" ]] && return 0 || return 1
}

Example

declare -A my_array
my_array['foo']="bar"

if [[ "$(array_key_exists 'my_array' 'foo'; echo $?)" = "0" ]]; then
    echo "OK"
else
    echo "ERROR"
fi

Tested with GNU bash, version 4.1.5(1)-release (i486-pc-linux-gnu)

Lucas Stad
  • 11
  • 1
  • 4
1

For all time people, once and for all.

There's a "clean code" long way, and there is a shorter, more concise, bash centered way.

$1 = The index or key you are looking for.

$2 = The array / map passed in by reference.

function hasKey ()
{
    local -r needle="${1:?}"
    local -nr haystack=${2:?}

    for key in "${!haystack[@]}"; do
        if [[ $key == $needle ]] ;
            return 0
        fi
    done

    return 1
}

A linear search can be replaced by a binary search, which would perform better with larger data sets. Simply count and sort the keys first, then do a classic binary halving of of the haystack as you get closer and closer to the answer.

Now, for the purist out there that is like "No, I want the more performant version because I may have to deal with large arrays in bash," lets look at a more bash centered solution, but one that maintains clean code and the flexibility to deal with arrays or maps.

function hasKey ()
{
    local -r needle="${1:?}"
    local -nr haystack=${2:?}

    [ -n ${haystack["$needle"]+found} ]
}

The line [ -n ${haystack["$needle"]+found} ]uses the ${parameter+word} form of bash variable expansion, not the ${parameter:+word} form, which attempts to test the value of a key, too, which is not the matter at hand.

Usage

local -A person=(firstname Anthony lastname Rutledge)

if hasMapKey "firstname" person; then
     # Do something
fi

When not performing substring expansion, using the form described below (e.g., ‘:-’), Bash tests for a parameter that is unset or null. Omitting the colon results in a test only for a parameter that is unset. Put another way, if the colon is included, the operator tests for both parameter’s existence and that its value is not null; if the colon is omitted, the operator tests only for existence.

${parameter:-word}

If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.

${parameter:=word}

If parameter is unset or null, the expansion of word is assigned to parameter. The value of parameter is then substituted. Positional

parameters and special parameters may not be assigned to in this way. ${parameter:?word}

If parameter is null or unset, the expansion of word (or a message to that effect if word is not present) is written to the standard

error and the shell, if it is not interactive, exits. Otherwise, the value of parameter is substituted. ${parameter:+word}

If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted.

https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Shell-Parameter-Expansion

If $needle does not exist expand to nothing, otherwise expand to the non-zero length string, "found". This will make the -n test succeed if the $needle in fact does exist (as I say "found"), and fail otherwise.

Anthony Rutledge
  • 6,980
  • 2
  • 39
  • 44
0

I get bad array subscript error when the key I'm checking is not set. So, I wrote a function that loops over the keys:

#!/usr/bin/env bash
declare -A helpList 

function get_help(){
    target="$1"

    for key in "${!helpList[@]}";do
        if [[ "$key" == "$target" ]];then
            echo "${helpList["$target"]}"
            return;
        fi
    done
}

targetValue="$(get_help command_name)"
if [[ -z "$targetvalue" ]];then
    echo "command_name is not set"
fi

It echos the value when it is found & echos nothing when not found. All the other solutions I tried gave me that error.

Reed
  • 14,703
  • 8
  • 66
  • 110