80

I was wondering if there is an efficient way to check if an element is present within an array in Bash? I am looking for something similar to what I can do in Python, like:

arr = ['a','b','c','d']

if 'd' in arr:
    do your thing
else:
    do something

I've seen solutions using associative array for bash for Bash 4+, but I am wondering if there is another solution out there.

Please understand that I know the trivial solution is to iterate in the array, but I don't want that.

codeforester
  • 28,846
  • 11
  • 78
  • 104
QuantumLicht
  • 1,873
  • 2
  • 19
  • 31
  • 5
    Don't confuse "concise" with "efficient". But no, there is no concise way in `bash` to do what you want with a simple array. – chepner Jan 16 '13 at 19:43
  • Take a look at http://stackoverflow.com/questions/3685970/bash-check-if-an-array-contains-a-value – Perleone Jan 16 '13 at 20:01

8 Answers8

108

You could do:

if [[ " ${arr[*]} " == *" d "* ]]; then
    echo "arr contains d"
fi

This will give false positives for example if you look for "a b" -- that substring is in the joined string but not as an array element. This dilemma will occur for whatever delimiter you choose.

The safest way is to loop over the array until you find the element:

array_contains () {
    local seeking=$1; shift
    local in=1
    for element; do
        if [[ $element == "$seeking" ]]; then
            in=0
            break
        fi
    done
    return $in
}

arr=(a b c "d e" f g)
array_contains "a b" "${arr[@]}" && echo yes || echo no    # no
array_contains "d e" "${arr[@]}" && echo yes || echo no    # yes

Here's a "cleaner" version where you just pass the array name, not all its elements

array_contains2 () { 
    local array="$1[@]"
    local seeking=$2
    local in=1
    for element in "${!array}"; do
        if [[ $element == "$seeking" ]]; then
            in=0
            break
        fi
    done
    return $in
}

array_contains2 arr "a b"  && echo yes || echo no    # no
array_contains2 arr "d e"  && echo yes || echo no    # yes

For associative arrays, there's a very tidy way to test if the array contains a given key: The -v operator

$ declare -A arr=( [foo]=bar [baz]=qux )
$ [[ -v arr[foo] ]] && echo yes || echo no
yes
$ [[ -v arr[bar] ]] && echo yes || echo no
no

See 6.4 Bash Conditional Expressions in the manual.

glenn jackman
  • 207,528
  • 33
  • 187
  • 305
36

Obvious caveats aside, if your array was actually like the one above, you could do

if [[ ${arr[*]} =~ d ]]
then
  do your thing
else
  do something
fi
Steven Penny
  • 82,115
  • 47
  • 308
  • 348
  • 4
    +1 for teaching something new. But `d` will match `xdy` as well. – Olaf Dietsche Jan 16 '13 at 20:20
  • 3
    @OlafDietsche That could be solved by writing it `if [[ ${arr[*]} =~ $(echo '\') ]]` ([source](https://stackoverflow.com/questions/9792702/does-bash-support-word-boundary-regular-expressions)) – Stefan van den Akker Aug 09 '14 at 12:51
  • Thanks that was helpful. But how can I negate the outcome in case I don't have an else following? That would save some redundant code. [@Steven Penny](http://stackoverflow.com/users/1002260/steven-penny) – Sven M. May 14 '17 at 22:02
  • 1
    @SvenM. if [[ ! ${arr[*]} =~ d ]] ? – rwenz3l Jun 12 '17 at 08:19
23

1) Initialize array arr and add elements

2) set variable to search for SEARCH_STRING

3) check if your array contains element

arr=()
arr+=('a')
arr+=('b')
arr+=('c')

SEARCH_STRING='b'

if [[ " ${arr[*]} " == *"$SEARCH_STRING"* ]];
then
    echo "YES, your arr contains $SEARCH_STRING"
else
    echo "NO, your arr does not contain $SEARCH_STRING"
fi
Marcin Wasiluk
  • 4,285
  • 2
  • 31
  • 42
  • 3
    This was the only reply in the thread that worked for me, thank you! – Sishaar Rao Mar 10 '17 at 03:03
  • This is fantastically concise and I was able to use this with `BASH_ARGV` to test whether a certain word was given as an argument that requires a different one-off setup was present without having to do some kind of `argparse` which is overkill for this case. – dragon788 Jun 01 '18 at 14:13
17

If array elements don't contain spaces, another (perhaps more readable) solution would be:

if echo ${arr[@]} | grep -q -w "d"; then 
    echo "is in array"
else 
    echo "is not in array"
fi
jesjimher
  • 1,069
  • 1
  • 10
  • 16
  • +1. Nice answer. This solution works for me to check whether a number is existed in a number array. While @Steven Penny's solution didn't work 100% correct in my case for number array. if echo ${combined_p2_idx[@]} | grep -q -w $subset_id; then – Good Will Sep 08 '16 at 17:54
  • I agree with @GoodWill. This should be the accepted answer. Other options weren't as robust. – Grr Feb 10 '17 at 15:35
  • It will error if you have negative numbers in the array – jessi Dec 08 '18 at 22:10
  • add `--` to signal the end of parameters and negative number will not treated as a param for grep – drAlberT Aug 27 '19 at 11:33
  • By far, the most elegant and understandable solution. Worked like a charm for me. – Lucas Lima Jan 07 '20 at 21:49
3
array=("word" "two words") # let's look for "two words"

using grep and printf:

(printf '%s\n' "${array[@]}" | grep -x -q "two words") && <run_your_if_found_command_here>

using for:

(for e in "${array[@]}"; do [[ "$e" == "two words" ]] && exit 0; done; exit 1) && <run_your_if_found_command_here>

For not_found results add || <run_your_if_notfound_command_here>

Qwerty
  • 19,992
  • 16
  • 88
  • 107
  • @Querty I don’t know why this isn’t the top answer. `grep -x...` makes it the simplest to use. – ldeck Jul 31 '20 at 19:51
3

As bash does not have a built-in value in array operator and the =~ operator or the [[ "${array[@]" == *"${item}"* ]] notation keep confusing me, I usually combine grep with a here-string:

colors=('black' 'blue' 'light green')
if grep -q 'black' <<< "${colors[@]}"
then
    echo 'match'
fi

Beware however that this suffers from the same false positives issue as many of the other answers that occurs when the item to search for is fully contained, but is not equal to another item:

if grep -q 'green' <<< "${colors[@]}"
then
    echo 'should not match, but does'
fi

If that is an issue for your use case, you probably won't get around looping over the array:

for color in "${colors[@]}"
do
    if [ "${color}" = 'green' ]
    then
        echo "should not match and won't"
        break
    fi
done

for color in "${colors[@]}"
do
    if [ "${color}" = 'light green' ]
    then
        echo 'match'
        break
    fi
done
ssc
  • 8,576
  • 8
  • 51
  • 84
  • @sec all that’s needed as @Querty’s answer above demonstrated to avoid false positives is to add `-x` to grep to match the entire line. – ldeck Jul 31 '20 at 19:53
2

Here's another way that might be faster, in terms of compute time, than iterating. Not sure. The idea is to convert the array to a string, truncate it, and get the size of the new array.

For example, to find the index of 'd':

arr=(a b c d)    
temp=`echo ${arr[@]}`
temp=( ${temp%%d*} )
index=${#temp[@]}

You could turn this into a function like:

get-index() {

    Item=$1
    Array="$2[@]"

    ArgArray=( ${!Array} )
    NewArray=( ${!Array%%${Item}*} )

    Index=${#NewArray[@]}

    [[ ${#ArgArray[@]} == ${#NewArray[@]} ]] && echo -1 || echo $Index

}

You could then call:

get-index d arr

and it would echo back 3, which would be assignable with:

index=`get-index d arr`
Jasonovich
  • 577
  • 4
  • 10
0

FWIW, here's what I used:

expr "${arr[*]}" : ".*\<$item\>"

This works where there are no delimiters in any of the array items or in the search target. I didn't need to solve the general case for my applicaiton.

Edward Falk
  • 9,578
  • 8
  • 66
  • 105