15

I have an array of animals:

declare -A animals=()
animals+=([horse])

and I want to check if an animal exists or not:

if [ -z "$animals[horse]"]; then
    echo "horse exists";
fi

but this does not work.

2

5 Answers 5

28

In bash 4.3, the -v operator can be applied to arrays.

declare -A animals
animals[horse]=neigh
# Fish are silent
animals[fish]=
[[ -v animals[horse] ]] && echo "horse exists"
[[ -v animals[fish] ]] && echo "fish exists"
[[ -v animals[unicorn] ]] || echo "unicorn does not exist" 

In prior versions, you would need to be more careful distinguishing between the key not existing and the key referring to any empty string.

animal_exists () {
    # If the given key maps to a non-empty string (-n), the
    # key obviously exists. Otherwise, we need to check if
    # the special expansion produces an empty string or an
    # arbitrary non-empty string.
    [[ -n ${animals[$1]} || -z ${animals[$1]-foo} ]]
}

animal_exists horse && echo "horse exists"
animal_exists fish && echo "fish exists"
animal_exists unicorn || echo "unicorn does not exist"
Sign up to request clarification or add additional context in comments.

3 Comments

Typo in first section of answer above. The last line should read: [[ -v animals[unicorn] ]] || echo "unicorn does not exist" and not [[ -v animals[unicorn] ]] && echo "unicorn does not exist" Also, the test commands do not need to be double-brackets. Single brackets also work.
I prefer to use the double brackets, as -v is not part of the POSIX standard for [, and if you have -v, you have [[ as well.
Note that [[ -v foo[bar] ]] works when your script has set -u. Otherwise you get an error if you try to expand a nonexistent array member, so things like test -n "${foo[bar]}" will not work.
10

There are several typos in your script

When I run it as it is, I get the following error messages from BASH:

1. animals: [horse]: must use subscript when assigning associative array
2. [: missing `]'

The first one says that if you want to use horse as an index to an associative array, you have to assign a value to it. An empty value (null) is ok.

-animals+=([horse])
+animals+=([horse]=)

The second message says that you need to separate the value you want to test and the bracket, as square bracket is considered a part of the value if not separated by spaces

-if [ -z "$animals[horse]"]; then
+if [ -z "$animals[horse]" ]; then

Finally, an element in an associative array exists when there is a value assigned to it (even if this value is null). As the question of testing if an array value is set has already been answered on this site, we can borrow the solution

-if [ -z "$animals[horse]"]; then
+if [ -n "${animals[horse]+1}" ]; then

For your convinience here is the complete script:

declare -A animals=()
animals+=([horse]=)

if [ -n "${animals[horse] + 1}" ]; then
    echo "horse exists";
fi

2 Comments

It's far simpler to write animals[horse]= than to use +=
@chepner fair enough, unless the OP wants to define several elements at once.
4

In BASH you can do:

declare -A animals=()
animals+=([horse]=)

[[ "${animals[horse]+foobar}" ]] && echo "horse exists"

"${animals[horse]+foobar}" returns foobar if horse is a valid index in array otherwise it returns nothing.

2 Comments

Sorry, ... what's the meaning of = at the end of line "animals+=([horse]=)"
That is assignment of an empty value to index horse. animals+=([horse]) will give syntax error otherwise.
1

Here's another way using pattern matching

Use ! in your array reference to get the keys. Use * to get all the keys as a space-separated string. My testing shows you could use @ instead of * even though they work slightly differently in some cases. The curly braces around test_key are not required. I use them out of habit. The parenthesies around ${test_key} may be replaced with double-quotes. Single quotes expand differently and will cause the test to fail.

declare -A animals=(["horse"]="equine" ["cat"]="feline" ["cow"]="tasty") # ...etc

test_key="fish"

if [[ ${!animals[*]} =~ (${test_key}) ]]; then
#       ^        ^
    # do stuff if key does exist
else
    # do stuff if key doesn't exist
fi

This also works in-line:

[[ ${!animals[*]} =~ (${test_key}) ]] && do stuff if true || do stuff if false

The only place I can think this might have trouble is if you search for a partial key and there are multiple keys with the same partial pattern or the key is also a partial match for another key since this will match the pattern anywhere in a string. You could call this a fuzzy search. For example, if You're looking for a key named "Jim" and you have a key named "Jimmy" this will return true.

Make the test less fuzzy

You can limit this by tightening up the pattern matching. This can easily be accomplished by adding a space, which is the separator when treating an array like a string. This can be tricky too. If you add a space before the test key, it will fail if it's the first key. The same is true for the last key if you add the space after. You could test for both separately.

[[ ${!animals[*]} =~ (${test_key} ) || ${!animals[*]} =~ ( ${test_key}) ]] && ... || ...
#                                ^                        ^

The other caveat is if there is only 1 key. If there is only 1 key in the array, then there will be no spaces. You could add some simple logic to test for how many keys and then only test with spaces for key quantities greater than 1.

if [[ ${#animals[@]} -gt 1 ]] ; then
    # test with spaces
else
    # test without spaces
fi
For bonus points you could also use grep ;)
grep "$test_key " <<< ${!animals[*]} && echo "${test_key} exists" || ( grep " $test_key" <<< ${!animals[*]} && echo "${test_key} exists" || echo "${test_key} doesn't exist" )

You would have to include spaces in your test here as well. I wouldn't use grep, but you could.

Comments

0

A bit old, but came up when I was searching for an answer, my system doesnt have bash 4.3 and I was specifically concerned about animals[horse-2] example

Another way you can do it is by dumping the contents out of the the array and then checking if the substring matches -

declare -A animals
animals[horse-2]=neigh-2
animals[dog]=woof
animals_as_string=$(declare -p animals)
other_animals="horse-2 moose cow"

for animal in $other_animals
do
  if [[ "$animal" == *$animals_as_string" ]]; then
    echo "found $animal"
  fi
done

Granted you'd need something a bit more to make sure you didn't find animals[horse-2] when searching for animals[horse] (if you had that in your array)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.