Check if a Bash array contains a value

2019-01-01 01:23发布

问题:

In Bash, what is the simplest way to test if an array contains a certain value?

Edit: With help from the answers and the comments, after some testing, I came up with this:

function contains() {
    local n=$#
    local value=${!n}
    for ((i=1;i < $#;i++)) {
        if [ \"${!i}\" == \"${value}\" ]; then
            echo \"y\"
            return 0
        fi
    }
    echo \"n\"
    return 1
}

A=(\"one\" \"two\" \"three four\")
if [ $(contains \"${A[@]}\" \"one\") == \"y\" ]; then
    echo \"contains one\"
fi
if [ $(contains \"${A[@]}\" \"three\") == \"y\" ]; then
    echo \"contains three\"
fi

I\'m not sure if it\'s the best solution, but it seems to work.

回答1:

There is sample code that shows how to replace a substring from an array. You can make a copy of the array and try to remove the target value from the copy. If the copy and original are then different, then the target value exists in the original string.

The straightforward (but potentially more time-consuming) solution is to simply iterate through the entire array and check each item individually. This is what I typically do because it is easy to implement and you can wrap it in a function (see this info on passing an array to a function).



回答2:

Below is a small function for achieving this. The search string is the first argument and the rest are the array elements:

containsElement () {
  local e match=\"$1\"
  shift
  for e; do [[ \"$e\" == \"$match\" ]] && return 0; done
  return 1
}

A test run of that function could look like:

$ array=(\"something to search for\" \"a string\" \"test2000\")
$ containsElement \"a string\" \"${array[@]}\"
$ echo $?
0
$ containsElement \"blaha\" \"${array[@]}\"
$ echo $?
1


回答3:

This approach has the advantage of not needing to loop over all the elements (at least not explicitly). But since array_to_string_internal() in array.c still loops over array elements and concatenates them into a string, it\'s probably not more efficient than the looping solutions proposed, but it\'s more readable.

if [[ \" ${array[@]} \" =~ \" ${value} \" ]]; then
    # whatever you want to do when arr contains value
fi

if [[ ! \" ${array[@]} \" =~ \" ${value} \" ]]; then
    # whatever you want to do when arr doesn\'t contain value
fi

Note that in cases where the value you are searching for is one of the words in an array element with spaces, it will give false positives. For example

array=(\"Jack Brown\")
value=\"Jack\"

The regex will see Jack as being in the array even though it isn\'t. So you\'ll have to change IFS and the separator characters on your regex if you want still to use this solution, like this

IFS=$\'\\t\'
array=(\"Jack Brown\\tJack Smith\")
unset IFS

value=\"Jack Smith\"

if [[ \"\\t${array[@]}\\t\" =~ \"\\t${value}\\t\" ]]; then
    echo \"yep, it\'s there\"
fi


回答4:

$ myarray=(one two three)
$ case \"${myarray[@]}\" in  *\"two\"*) echo \"found\" ;; esac
found


回答5:

for i in \"${array[@]}\"
do
    if [ \"$i\" -eq \"$yourValue\" ] ; then
        echo \"Found\"
    fi
done

For strings:

for i in \"${array[@]}\"
do
    if [ \"$i\" == \"$yourValue\" ] ; then
        echo \"Found\"
    fi
done


回答6:

If you need performance, you don\'t want to loop over your whole array every time you search.

In this case, you can create an associative array (hash table, or dictionary) that represents an index of that array. I.e. it maps each array element into its index in the array:

make_index () {
  local index_name=$1
  shift
  local -a value_array=(\"$@\")
  local i
  # -A means associative array, -g means create a global variable:
  declare -g -A ${index_name}
  for i in \"${!value_array[@]}\"; do
    eval ${index_name}[\"${value_array[$i]}\"]=$i
  done
}

Then you can use it like this:

myarray=(\'a a\' \'b b\' \'c c\')
make_index myarray_index \"${myarray[@]}\"

And test membership like so:

member=\"b b\"
# the \"|| echo NOT FOUND\" below is needed if you\'re using \"set -e\"
test \"${myarray_index[$member]}\" && echo FOUND || echo NOT FOUND

Or also:

if [ \"${myarray_index[$member]}\" ]; then 
  echo FOUND
fi

Notice that this solution does the right thing even if the there are spaces in the tested value or in the array values.

As a bonus, you also get the index of the value within the array with:

echo \"<< ${myarray_index[$member]} >> is the index of $member\"


回答7:

I typically just use:

inarray=$(echo ${haystack[@]} | grep -o \"needle\" | wc -w)

non zero value indicates a match was found.



回答8:

Here is a small contribution :

array=(word \"two words\" words)  
search_string=\"two\"  
match=$(echo \"${array[@]:0}\" | grep -o $search_string)  
[[ ! -z $match ]] && echo \"found !\"  

Note: this way doesn\'t distinguish the case \"two words\" but this is not required in the question.



回答9:

containsElement () { for e in \"${@:2}\"; do [[ \"$e\" = \"$1\" ]] && return 0; done; return 1; }

Now handles empty arrays correctly.



回答10:

Another one liner without a function:

(for e in \"${array[@]}\"; do [[ \"$e\" == \"searched_item\" ]] && exit 0; done) && echo found || not found

Thanks @Qwerty for the heads up regarding spaces!

corresponding function:

find_in_array() {
  local word=$1
  shift
  for e in \"$@\"; do [[ \"$e\" == \"$word\" ]] && return 0; done
}

example:

some_words=( these are some words )
find_in_array word \"${some_words[@]}\" || echo \"expected missing! since words != word\"


回答11:

One-line solution

printf \'%s\\n\' ${myarray[@]} | grep -P \'^mypattern$\'

Explanation

The printf statement prints each element of the array on a separate line.

The grep statement uses the special characters ^ and $ to find a line that contains exactly the pattern given as mypattern (no more, no less).


Usage

To put this into an if ... then statement:

if printf \'%s\\n\' ${myarray[@]} | grep -q -P \'^mypattern$\'; then
    # ...
fi

I added a -q flag to the grep expression so that it won\'t print matches; it will just treat the existence of a match as \"true.\"



回答12:

If you want to do a quick and dirty test to see if it\'s worth iterating over the whole array to get a precise match, Bash can treat arrays like scalars. Test for a match in the scalar, if none then skipping the loop saves time. Obviously you can get false positives.

array=(word \"two words\" words)
if [[ ${array[@]} =~ words ]]
then
    echo \"Checking\"
    for element in \"${array[@]}\"
    do
        if [[ $element == \"words\" ]]
        then
            echo \"Match\"
        fi
    done
fi

This will output \"Checking\" and \"Match\". With array=(word \"two words\" something) it will only output \"Checking\". With array=(word \"two widgets\" something) there will be no output.



回答13:

a=(b c d)

if printf \'%s\\0\' \"${a[@]}\" | grep -Fqxz c
then
  echo \'array “a” contains value “c”\'
fi

If you prefer you can use equivalent long options:

--fixed-strings --quiet --line-regexp --null-data


回答14:

This is working for me:

# traditional system call return values-- used in an `if`, this will be true when returning 0. Very Odd.
contains () {
    # odd syntax here for passing array parameters: http://stackoverflow.com/questions/8082947/how-to-pass-an-array-to-a-bash-function
    local list=$1[@]
    local elem=$2

    # echo \"list\" ${!list}
    # echo \"elem\" $elem

    for i in \"${!list}\"
    do
        # echo \"Checking to see if\" \"$i\" \"is the same as\" \"${elem}\"
        if [ \"$i\" == \"${elem}\" ] ; then
            # echo \"$i\" \"was the same as\" \"${elem}\"
            return 0
        fi
    done

    # echo \"Could not find element\"
    return 1
}

Example call:

arr=(\"abc\" \"xyz\" \"123\")
if contains arr \"abcx\"; then
    echo \"Yes\"
else
    echo \"No\"
fi


回答15:

given :

array=(\"something to search for\" \"a string\" \"test2000\")
elem=\"a string\"

then a simple check of :

if c=$\'\\x1E\' && p=\"${c}${elem} ${c}\" && [[ ! \"${array[@]/#/${c}} ${c}\" =~ $p ]]; then
  echo \"$elem exists in array\"
fi

where

c is element separator
p is regex pattern

(The reason for assigning p separately, rather than using the expression directly inside [[ ]] is to maintain compatibility for bash 4)



回答16:

I generally write these kind of utilities to operate on the name of the variable, rather than the variable value, primarily because bash can\'t otherwise pass variables by reference.

Here\'s a version that works with the name of the array:

function array_contains # array value
{
    [[ -n \"$1\" && -n \"$2\" ]] || {
        echo \"usage: array_contains <array> <value>\"
        echo \"Returns 0 if array contains value, 1 otherwise\"
        return 2
    }

    eval \'local values=(\"${\'$1\'[@]}\")\'

    local element
    for element in \"${values[@]}\"; do
        [[ \"$element\" == \"$2\" ]] && return 0
    done
    return 1
}

With this, the question example becomes:

array_contains A \"one\" && echo \"contains one\"

etc.



回答17:

Using grep and printf

Format each array member on a new line, then grep the lines.

if printf \'%s\\n\' \"${array[@]}\" | grep -x -q \"search string\"; then echo true; else echo false; fi
example:
$ array=(\"word\", \"two words\")
$ if printf \'%s\\n\' \"${array[@]}\" | grep -x -q \"two words\"; then echo true; else echo false; fi
true

Note that this has no problems with delimeters and spaces.



回答18:

After having answered, I read another answer that I particularly liked, but it was flawed and downvoted. I got inspired and here are two new approaches I see viable.

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>



回答19:

Here\'s my take on this.

I\'d rather not use a bash for loop if I can avoid it, as that takes time to run. If something has to loop, let it be something that was written in a lower level language than a shell script.

function array_contains { # arrayname value
  local -A _arr=()
  local IFS=
  eval _arr=( $(eval printf \'[%q]=\"1\"\\ \' \"\\${$1[@]}\") )
  return $(( 1 - 0${_arr[$2]} ))
}

This works by creating a temporary associative array, _arr, whose indices are derived from the values of the input array. (Note that associative arrays are available in bash 4 and above, so this function won\'t work in earlier versions of bash.) We set $IFS to avoid word splitting on whitespace.

The function contains no explicit loops, though internally bash steps through the input array in order to populate printf. The printf format uses %q to ensure that input data are escaped such that they can safely be used as array keys.

$ a=(\"one two\" three four)
$ array_contains a three && echo BOOYA
BOOYA
$ array_contains a two && echo FAIL
$

Note that everything this function uses is a built-in to bash, so there are no external pipes dragging you down, even in the command expansion.

And if you don\'t like using eval ... well, you\'re free to use another approach. :-)



回答20:

Combining a few of the ideas presented here you can make an elegant if statment without loops that does exact word matches.

$find=\"myword\"
$array=(value1 value2 myword)
if [[ ! -z $(printf \'%s\\n\' \"${array[@]}\" | grep -w $find) ]]; then
  echo \"Array contains myword\";
fi

This will not trigger on word or val, only whole word matches. It will break if each array value contains multiple words.



回答21:

Borrowing from Dennis Williamson\'s answer, the following solution combines arrays, shell-safe quoting, and regular expressions to avoid the need for: iterating over loops; using pipes or other sub-processes; or using non-bash utilities.

declare -a array=(\'hello, stack\' one \'two words\' words last)
printf -v array_str -- \',,%q\' \"${array[@]}\"

if [[ \"${array_str},,\" =~ ,,words,, ]]
then
   echo \'Matches\'
else
   echo \"Doesn\'t match\"
fi

The above code works by using Bash regular expressions to match against a stringified version of the array contents. There are six important steps to ensure that the regular expression match can\'t be fooled by clever combinations of values within the array:

  1. Construct the comparison string by using Bash\'s built-in printf shell-quoting, %q. Shell-quoting will ensure that special characters become \"shell-safe\" by being escaped with backslash \\.
  2. Choose a special character to serve as a value delimiter. The delimiter HAS to be one of the special characters that will become escaped when using %q; that\'s the only way to guarantee that values within the array can\'t be constructed in clever ways to fool the regular expression match. I choose comma , because that character is the safest when eval\'d or misused in an otherwise unexpected way.
  3. Combine all array elements into a single string, using two instances of the special character to serve as delimiter. Using comma as an example, I used ,,%q as the argument to printf. This is important because two instances of the special character can only appear next to each other when they appear as the delimiter; all other instances of the special character will be escaped.
  4. Append two trailing instances of the delimiter to the string, to allow matches against the last element of the array. Thus, instead of comparing against ${array_str}, compare against ${array_str},,.
  5. If the target string you\'re searching for is supplied by a user variable, you must escape all instances of the special character with a backslash. Otherwise, the regular expression match becomes vulnerable to being fooled by cleverly-crafted array elements.
  6. Perform a Bash regular expression match against the string.


回答22:

Here is my take on this problem. Here is the short version:

function arrayContains() {
        local haystack=${!1}
        local needle=\"$2\"
        printf \"%s\\n\" ${haystack[@]} | grep -q \"^$needle$\"
}

And the long version, which I think is much easier on the eyes.

# With added utility function.
function arrayToLines() {
        local array=${!1}
        printf \"%s\\n\" ${array[@]}
}

function arrayContains() {
        local haystack=${!1}
        local needle=\"$2\"
        arrayToLines haystack[@] | grep -q \"^$needle$\"
}

Examples:

test_arr=(\"hello\" \"world\")
arrayContains test_arr[@] hello; # True
arrayContains test_arr[@] world; # True
arrayContains test_arr[@] \"hello world\"; # False
arrayContains test_arr[@] \"hell\"; # False
arrayContains test_arr[@] \"\"; # False


回答23:

I had the case that I had to check if an ID was contained in a list of IDs generated by another script / command. For me worked the following:

# the ID I was looking for
ID=1

# somehow generated list of IDs
LIST=$( <some script that generates lines with IDs> )
# list is curiously concatenated with a single space character
LIST=\" $LIST \"

# grep for exact match, boundaries are marked as space
# would therefore not reliably work for values containing a space
# return the count with \"-c\"
ISIN=$(echo $LIST | grep -F \" $ID \" -c)

# do your check (e. g. 0 for nothing found, everything greater than 0 means found)
if [ ISIN -eq 0 ]; then
    echo \"not found\"
fi
# etc.

You could also shorten / compact it like this:

if [ $(echo \" $( <script call> ) \" | grep -F \" $ID \" -c) -eq 0 ]; then
    echo \"not found\"
fi

In my case, I was running jq to filter some JSON for a list of IDs and had to later check if my ID was in this list and this worked the best for me. It will not work for manually created arrays of the type LIST=(\"1\" \"2\" \"4\") but for with newline separated script output.


PS.: could not comment an answer because I\'m relatively new ...



回答24:

A small addition to @ghostdog74\'s answer about using case logic to check that array contains particular value:

myarray=(one two three)
word=two
case \"${myarray[@]}\" in  (\"$word \"*|*\" $word \"*|*\" $word\") echo \"found\" ;; esac

Or with extglob option turned on, you can do it like this:

myarray=(one two three)
word=two
shopt -s extglob
case \"${myarray[@]}\" in ?(*\" \")\"$word\"?(\" \"*)) echo \"found\" ;; esac

Also we can do it with if statement:

myarray=(one two three)
word=two
if [[ $(printf \"_[%s]_\" \"${myarray[@]}\") =~ .*_\\[$word\\]_.* ]]; then echo \"found\"; fi


回答25:

The following code checks if a given value is in the array and returns its zero-based offset:

A=(\"one\" \"two\" \"three four\")
VALUE=\"two\"

if [[ \"$(declare -p A)\" =~ \'[\'([0-9]+)\']=\"\'$VALUE\'\"\' ]];then
  echo \"Found $VALUE at offset ${BASH_REMATCH[1]}\"
else
  echo \"Couldn\'t find $VALUE\"
fi

The match is done on the complete values, therefore setting VALUE=\"three\" would not match.



回答26:

This could be worth investigating if you don\'t want to iterate:

#!/bin/bash
myarray=(\"one\" \"two\" \"three\");
wanted=\"two\"
if `echo ${myarray[@]/\"$wanted\"/\"WAS_FOUND\"} | grep -q \"WAS_FOUND\" ` ; then
 echo \"Value was found\"
fi
exit

Snippet adapted from: http://www.thegeekstuff.com/2010/06/bash-array-tutorial/ I think it is pretty clever.

EDIT: You could probably just do:

if `echo ${myarray[@]} | grep -q \"$wanted\"` ; then
echo \"Value was found\"
fi

But the latter only works if the array contains unique values. Looking for 1 in \"143\" will give false positive, methinks.



回答27:

Although there were several great and helpful answers here, I didn\'t find one that seemed to be the right combination of performant, cross-platform, and robust; so I wanted to share the solution I wrote for my code:

#!/bin/bash

# array_contains \"$needle\" \"${haystack[@]}\"
#
# Returns 0 if an item ($1) is contained in an array ($@).
#
# Developer note:
#    The use of a delimiter here leaves something to be desired. The ideal
#    method seems to be to use `grep` with --line-regexp and --null-data, but
#    Mac/BSD grep doesn\'t support --line-regexp.
function array_contains()
{
    # Extract and remove the needle from $@.
    local needle=\"$1\"
    shift

    # Separates strings in the array for matching. Must be extremely-unlikely
    # to appear in the input array or the needle.
    local delimiter=\'#!-\\8/-!#\'

    # Create a string with containing every (delimited) element in the array,
    # and search it for the needle with grep in fixed-string mode.
    if printf \"${delimiter}%s${delimiter}\" \"$@\" | \\
        grep --fixed-strings --quiet \"${delimiter}${needle}${delimiter}\"; then
        return 0
    fi

    return 1
}


回答28:

My version of the regular expressions technique that\'s been suggested already:

values=(foo bar)
requestedValue=bar

requestedValue=${requestedValue##[[:space:]]}
requestedValue=${requestedValue%%[[:space:]]}
[[ \"${values[@]/#/X-}\" =~ \"X-${requestedValue}\" ]] || echo \"Unsupported value\"

What\'s happening here is that you\'re expanding the entire array of supported values into words and prepending a specific string, \"X-\" in this case, to each of them, and doing the same to the requested value. If this one is indeed contained in the array, then the resulting string will at most match one of the resulting tokens, or none at all in the contrary. In the latter case the || operator triggers and you know you\'re dealing with an unsupported value. Prior to all of that the requested value is stripped of all leading and trailing whitespace through standard shell string manipulation.

It\'s clean and elegant, I believe, though I\'m not too sure of how performant it may be if your array of supported values is particularly large.



回答29:

Expanding on the above answer from Sean DiSanti, I think the following is a simple and elegant solution that avoids having to loop over the array and won\'t give false positives due to partial matches

function is_in_array {
    local ELEMENT=\"${1}\"
    local DELIM=\",\"
    printf \"${DELIM}%s${DELIM}\" \"${@:2}\" | grep -q \"${DELIM}${ELEMENT}${DELIM}\"
}

Which can be called like so:

$ haystack=(\"needle1\" \"needle2\" \"aneedle\" \"spaced needle\")
$ is_in_array \"needle\" \"${haystack[@]}\"
$ echo $?
1
$ is_in_array \"needle1\" \"${haystack[@]}\"
$ echo $?
0


回答30:

A combination of answers by Beorn Harris and loentar gives one more interesting one-liner test:

delim=$\'\\x1F\' # define a control code to be used as more or less reliable delimiter
if [[ \"${delim}${array[@]}${delim}\" =~ \"${delim}a string to test${delim}\" ]]; then
    echo \"contains \'a string to test\'\"
fi

This one does not use extra functions, does not make replacements for testing and adds extra protection against occasional false matches using a control code as a delimiter.



标签: arrays bash