Test if element is in array in bash

18

9

Is there a nice way of checking if an array has an element in bash (better than looping through)?

Alternatively, is there another way to check if a number or string equals any of a set of predefined constants?

Tgr

Posted 2010-10-04T10:54:20.940

Reputation: 2 363

Answers

25

In Bash 4, you can use associative arrays:

# set up array of constants
declare -A array
for constant in foo bar baz
do
    array[$constant]=1
done

# test for existence
test1="bar"
test2="xyzzy"

if [[ ${array[$test1]} ]]; then echo "Exists"; fi    # Exists
if [[ ${array[$test2]} ]]; then echo "Exists"; fi    # doesn't

To set up the array initially you could also do direct assignments:

array[foo]=1
array[bar]=1
# etc.

or this way:

array=([foo]=1 [bar]=1 [baz]=1)

Paused until further notice.

Posted 2010-10-04T10:54:20.940

Reputation: 86 075

Actually, the [[]] test doesn't work in the case the value is empty. E.g., "array['test']=''". In this case, the key 'test' exists, and you can see it listed with ${!array[@]}, but "[[ ${array['test']} ]]; echo $?" echoes 1, not 0. – haridsv – 2011-06-06T01:59:00.823

1${array[$test1]} is simple but has a problem: it won't work if you use set -u in your scripts (which is recommended), as you'd get "unbound variable". – tokland – 2012-05-25T21:53:12.167

@tokland: Who recommends it? I certainly don't. – Paused until further notice. – 2012-05-25T22:00:54.017

@DennisWilliamson: Ok, some people recommend it, but I think it would be nice to have a solution that works regardless the value of these flags. – tokland – 2012-05-25T22:03:34.210

10

It's an old question, but I think what is the simplest solution has not appeared yet: test ${array[key]+_}. Example:

declare -A xs=([a]=1 [b]="")
test ${xs[a]+_} && echo "a is set"
test ${xs[b]+_} && echo "b is set"
test ${xs[c]+_} && echo "c is set"

Outputs:

a is set
b is set

To see how this work check this.

tokland

Posted 2010-10-04T10:54:20.940

Reputation: 880

2The info manual recommneds you to use env to avoid ambiguities in aliases, progs and other functions that may have adopted the name "test". As above env test ${xs[a]+_} && echo "a is set". You can also get this functionality using double-brackets, the same trick then checking for null: [[ ! -z "${xs[b]+_}" ]] && echo "b is set" – A.Danischewski – 2016-02-01T13:44:12.527

Also you can use the even simpler [[ ${xs[b]+set} ]] – Arne L. – 2019-08-01T10:18:12.317

5

There is a way to test if an element of an associative array exists (not set), this is different from empty:

isNotSet() {
    if [[ ! ${!1} && ${!1-_} ]]
    then
        return 1
    fi
}

Then use it:

declare -A assoc
KEY="key"
isNotSet assoc[${KEY}]
if [ $? -ne 0 ]
then
  echo "${KEY} is not set."
fi

Diego F. Durán

Posted 2010-10-04T10:54:20.940

Reputation: 171

just a note: declare -A doesn't work on bash 3.2.39 (debian lenny), but it works on bash 4.1.5 (debian squeeze) – Paweł Polewicz – 2011-12-14T16:05:27.823

Associative arrays were introduced in Bash 4. – Diego F. Durán – 2011-12-20T15:23:36.260

1note that if ! some_check then return 1 = some_check. So: isNotSet() { [[ ... ]] }. Check my solution below, you can do it in a simple check. – tokland – 2012-05-25T22:16:09.890

3

You can see if an entry is present by piping the contents of the array to grep.

 printf "%s\n" "${mydata[@]}" | grep "^${val}$"

You can also get the index of an entry with grep -n, which returns the line number of a match (remember to subtract 1 to get zero-based index) This will be reasonably quick except for very large arrays.

# given the following data
mydata=(a b c "hello world")

for val in a c hello "hello world"
do
           # get line # of 1st matching entry
    ix=$( printf "%s\n" "${mydata[@]}" | grep -n -m 1 "^${val}$" | cut -d ":" -f1 )

    if [[ -z $ix ]]
    then
        echo $val missing
    else
         # subtract 1.  Bash arrays are zero-based, but grep -n returns 1 for 1st line, not 0 
        echo $val found at $(( ix-1 ))
    fi
done

a found at 0
c found at 2
hello missing
hello world found at 3

explanation:

  • $( ... ) is the same as using backticks to capture output of a command into a variable
  • printf outputs mydata one element per line
  • (all quotes necessary, along with @ instead of *. this avoids splitting "hello world" into 2 lines)
  • grep searches for exact string: ^ and $ match beginning and end of line
  • grep -n returns line #, in form of 4:hello world
  • grep -m 1 finds first match only
  • cut extracts just the line number
  • subtract 1 from returned line number.

You can of course fold the subtraction into the command. But then test for -1 for missing:

ix=$(( $( printf "%s\n" "${mydata[@]}" | grep -n -m 1 "^${val}$" | cut -d ":" -f1 ) - 1 ))

if [[ $ix == -1 ]]; then echo missing; else ... fi
  • $(( ... )) does integer arithmetic

kane

Posted 2010-10-04T10:54:20.940

Reputation: 31

1

#!/bin/bash
function in_array {
  ARRAY=$2
  for e in ${ARRAY[*]}
  do
    if [[ "$e" == "$1" ]]
    then
      return 0
    fi
  done
  return 1
}

my_array=(Drupal Wordpress Joomla)
if in_array "Drupal" "${my_array[*]}"
  then
    echo "Found"
  else
    echo "Not found"
fi

Cong Nguyen

Posted 2010-10-04T10:54:20.940

Reputation: 111

1Can you elaborate on why you are suggesting this approach? OP asked if there is a way to do it without looping through the array, which is what you are doing in in_array. Cheers – bertieb – 2018-02-27T11:28:16.313

Well, at least that loop is capsuled in a function, which might be good enough for many cases (with small data sets), and doesn't require bash 4+. Probably ${ARRAY[@]} should be used. – Tobias – 2018-09-26T08:49:43.297

1

I don't think you can do it properly without looping unless you have very limited data in the array.

Here is one simple variant, this would correctly say that "Super User" exists in the array. But it would also say that "uper Use" is in the array.

MyArray=('Super User' 'Stack Overflow' 'Server Fault' 'Jeff' );
FINDME="Super User"

FOUND=`echo ${MyArray[*]} | grep "$FINDME"`

if [ "${FOUND}" != "" ]; then
  echo Array contains: $FINDME
else
  echo $FINDME not found
fi

#
# If you where to add anchors < and > to the data it could work
# This would find "Super User" but not "uper Use"
#

MyArray2=('<Super User>' '<Stack Overflow>' '<Server Fault>' '<Jeff>' );

FOUND=`echo ${MyArray2[*]} | grep "<$FINDME>"`

if [ "${FOUND}" != "" ]; then
  echo Array contains: $FINDME
else
  echo $FINDME not found
fi

The problem is that there is no easy way to add the anchors (that I can think of) besides looping through the array. Unless you can add them before you put them in the array...

Nifle

Posted 2010-10-04T10:54:20.940

Reputation: 31 337

It is a nice solution when the constants are alphanumeric, though (with grep "\b$FINDME\b"). Probably could work with non-alphanumeric constants that have no spaces, with "(^| )$FINDME(\$| )" (or something like that... I have never been able to learn what flavor of regexp grep uses.) – Tgr – 2010-11-30T11:10:38.130