523

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

Charles Duffy
  • 235,655
  • 34
  • 305
  • 356
Paolo Tedesco
  • 49,782
  • 29
  • 130
  • 181

39 Answers39

575

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 array contains value
fi

if [[ ! " ${array[@]} " =~ " ${value} " ]]; then
    # whatever you want to do when array 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"

if [[ "\t${array[@]}\t" =~ "\t${value}\t" ]]; then
    echo "true"
else
    echo "false"
fi

This will print "false".

Obviously this can also be used as a test statement, allowing it to be expressed as a one-liner

[[ " ${array[@]} " =~ " ${value} " ]] && echo "true" || echo "false"
Keegan
  • 8,180
  • 1
  • 21
  • 33
  • 2
    I added a space at the start of the first regex value match, so that it would only match the word, not something ending in the word. Works great. Howver, I don't understand why you use the second condition, wouldn't the first work fine alone? – JStrahl Mar 22 '13 at 08:08
  • 1
    @AwQiruiGuo I'm not sure I'm following. Are you talking about arrays with dollar literals? If so, just make sure to escape the dollars in the value you're matching against with backslashes. – Keegan Sep 12 '15 at 14:38
  • 1
    If someone wants to check if the element is not in the list without using the else. This will do: if [[ ! " ${arr[@]} " =~ " ${value} " ]]; then ...fi – Good Will Oct 20 '16 at 11:32
  • 11
    Oneliner: `[[ " ${branches[@]} " =~ " ${value} " ]] && echo "YES" || echo "NO";` – ericson.cepeda Feb 23 '17 at 22:27
  • 1
    Is there a reason you use `${array[@]}` instead of `${array[*]}`? It seems `=~` ends up coalescing the array into a single string anyways, so `*` would probably be more clear. – dimo414 Jun 29 '18 at 17:21
  • 1
    I used `@` instead of `*` because it keeps quoted words together, which is particularly useful if you are using a different IFS character. See the examples at https://www.linuxjournal.com/content/bash-arrays. – Keegan Jun 30 '18 at 04:25
  • 5
    Shellcheck complains about this solution, SC2199 and SC2076. I couldn't fix the warnings without breaking the functionality. Any thoughts about that other than disabling shellcheck for that line? – Ali Essam May 16 '19 at 12:11
  • 4
    [SC2076](https://github.com/koalaman/shellcheck/wiki/SC2076) is easy to fix, just remove the double quotes in the `if`. I don't think there's a way to avoid [SC2199](https://github.com/koalaman/shellcheck/wiki/SC2199) with this approach. You'd have to explicitly loop though the array, as shown in some of the other solutions, or [ignore](https://github.com/koalaman/shellcheck/wiki/Ignore) the warning. – Keegan May 16 '19 at 13:14
  • If the elements in the array might contain spaces, you can do something a little different to protect against that. Here, I use `:` as a safety character instead: `[[ $(printf ':%s:' "${array[@]}") =~ ":value:") ]]` – Chris Cogdon Jun 19 '19 at 17:24
  • 1
    Why do you need space or tab wrapping around the matching word? – jiashenC May 05 '20 at 15:01
  • To protect against regex matches where your search term is a substring of one of the elements. If you took out the IFS spaces in the if, had something like "Jack Blackstone" in the array, and searched for "Jack Black", you'd still find a match. – Keegan May 06 '20 at 20:00
  • **GOTCHA** This unexpectedly matches filenames which are substrings of other filenames. For example, if your haystack contains `".header"` and your needle is `"ad"` then ad will get matched even though there are no spaces in the filenames. – Josh Habdas May 12 '20 at 08:33
  • 2
    @JoshHabdas If that's happening you probably forgot the spaces around the values in quotes (or maybe you're using some other shell that has different behavior than Bash). I tested your scenario with Bash 4.4.12 and it doesn't get matched. – Keegan May 14 '20 at 18:12
  • The literal spaces I did not use though in hindsight it makes sense why they might be required. Tested in GNU bash, version 5.0.16(1)-release. – Josh Habdas May 15 '20 at 01:14
  • there is an issue with this command, if the you are comparing array of strings, with a string as value, and if the value is substring as well as complete string then output will true for both cases. So be aware. – majid bhatti Oct 21 '20 at 16:35
  • The last example I have where the IFS character is changed should prevent this. If it's not (because the IFS character is in your haystack), then change IFS to a different character that's not. – Keegan Oct 22 '20 at 18:27
423

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
dimo414
  • 42,340
  • 17
  • 131
  • 218
patrik
  • 4,247
  • 2
  • 11
  • 4
  • 5
    Works nicely! I just have to remember to pass the array as with quotes: `"${array[@]}"`. Otherwise elements containing spaces will break functionality. – Juve Nov 09 '12 at 08:53
  • 27
    Nice. I'd call it elementIn() because it checks if the first argument is contained in the second. containsElements() sounds like the array would go first. For newbies like me, an example of how to use a function that does not write to stdout in an "if" statement would help: `if elementIn "$table" "${skip_tables[@]}" ; then echo skipping table: ${table}; fi;` Thanks for your help! – GlenPeterson Jul 01 '13 at 14:20
  • Ok I need a bit of explanation here. I don't understand the use of "&&" there. Take the expression --- for e in "${@:2}"; do [[ "$e" == "$1" ]] && return 0; done --- Now if you read the test bit and parse it yourself, you start taking the string "a string" and compare it with the first element of the array "something to search for" so that's your first do [[ "$e" == "$1" ]] and obviously the output of that condition here is false. Now why telling bash "and return 0" (&& return 0) right after given that the previous "if" control returned false? – Bluz Oct 31 '13 at 15:50
  • Hope my question makes sense.I understand what that script does but I don't really understand why it actually works. – Bluz Oct 31 '13 at 15:50
  • 5
    @Bluz the && construct is a boolean AND operator. The use of boolean operators creates a boolean statement.Boolean logic says the whole statement can only be true if both the statements before and after the && evaluate to true. This is used as a shortcut insted of and if block.The test is evaluated and if false, there is no need to evaluate the return as it's irrelevant to the whole statement once the test has failed and is therefore not run. If the test is successfull then the sucess of the boolean statement DOES require the outcome of the return to be determined so the code is run. – peteches Nov 05 '13 at 16:37
  • after a lot of efforts, I wasn't able to get it work. It stopped my script whatever I did. – herve Jan 29 '16 at 18:26
  • Thanks, sorted my problem out...Although, IMO you have the return "0" and "1" the wrong way around. Return 1 if true/found in array in loop, else was not found in array within the loop so return 0 ;) (Or call the function "notContainsElement" – James Sep 24 '16 at 11:54
  • 4
    @James by convention the success code in bash is "0" and error is everything >= 1. This is why it returns 0 on success. :) – tftd Nov 02 '16 at 23:49
  • The below post was helpful to me when attempting to use the above function. Changing it to a index style loop would likely make it bullet proof. Link: http://stackoverflow.com/questions/9084257/bash-array-with-spaces-in-elements#9084479 – FredCooke Mar 15 '17 at 07:52
  • A more robust system would be to pass the array _name_, and then use indirection inside the function: for e in "${!2[@]}" ; do ... – Chris Cogdon Jul 13 '17 at 15:56
  • Ignore my comment above: ${!name[@]} does something wildly different in bash. – Chris Cogdon Jul 13 '17 at 16:09
  • 3
    The 0/1 convention is there so you can do this: `if containsElement "a string" "${array[@]}"; then echo "found it" ; else echo "didn't find it" ; fi` – LinuxDisciple Aug 18 '17 at 19:18
  • @ChrisCogdon good idea - I got redirection to work by using another var in the function - `arr="$2[@]"` appends the array expansion to the passed-in array name, and then `for e in ${!arr}` iterates through the actual expanded array – johnny Feb 14 '18 at 20:09
  • But note that indirection like that confuses [shellcheck](http://www.shellcheck.net) _a little_ ([SC2034](https://github.com/koalaman/shellcheck/wiki/Sc2034#exceptions)), so I ended up using that only where I use the array somewhere else as well - otherwise for one-time checks I use `elementIn` (renamed as suggested by @GlenPeterson above) – johnny Feb 14 '18 at 20:12
  • It also seems to sort-of work if you just provide the name with the expansion appended already - `containsElement ARRAY_NAME[@] match_val`, but this doesn't seem to work with things like `ARRAY_NAME[@]:3` (limiting the scope of the search in the array), so maybe it's best not to even allow that option... – johnny Feb 15 '18 at 20:12
  • Sorry for refloating this, but I'm using this code and works really well in a script to delete git branches, but now I want to refactor a bit and I'm not quite understanding the role of e, can someone please expand on this? Thanks! – Mauricio Machado Oct 23 '18 at 15:43
  • @MauricioMachado `$e` is quite simply the element in the array against which you try to match the variable `$match` at the moment. (I keep coming back to this answer like twice a year. Excellent!) – pfnuesel Oct 31 '18 at 02:36
  • 5
    I ve been trying to get my head around the `local e match=$1 shift` and `for e` but still can't understand how it works. – Stelios Dec 05 '18 at 12:57
  • 12
    @Stelios `shift` shifts the argument list by 1 to the left (dropping the first argument) and `for` without an `in` implicitly iterates over the argument list. – Christian Jan 02 '19 at 10:56
  • 3
    The code is working like a charm and it is simple yet elegant. However, I don't understand one thing. I'm executing "echo $e" before the for loop and it appears to be empty as expected. How does for loop know it should traverse $1 which is the array after shift operation? Is it just supposed to read $1 argument out of nowhere? What am I missing here? – Hmerac Feb 26 '19 at 07:36
  • I prefer this to the [[ " ${array[@]} " =~ " ${value} " ]]; @Keegan solution below as does not appear sensitive to spaces (as thoughtfully pointed out by Keegan in his approach) – Pancho Jul 12 '19 at 11:06
  • 1
    despite the comment by @Christian, i still don't understand how `e` is supposed to contain positional parameter indexes >1. i know what `shift` does, but nothing is actually looping over `$@` to add each parameter to `$e`. also, the function doesn't seem to work on bash v5.1 – Erin Schoonover Jan 03 '21 at 15:50
  • 1
    @ErinSchoonover `for` is implicitly looping over `"$@"` if no `in` part is specified. Yes, I'd call that a questionable syntactic shortcut but it is what it is. – Christian Jan 29 '21 at 12:45
  • 1
    @Christian Thank you! Running this through a shell after setting debugging mode (`set -x`), I can see that `for` is doing just what you say: `- for e in "$@"` – Erin Schoonover Mar 01 '21 at 16:05
66

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."

JellicleCat
  • 23,907
  • 21
  • 96
  • 144
  • 3
    Nice solution! On GNU grep, there is also "--line-regexp" which could replace "-P" and the ^ and $ in the pattern: printf '%s\n' ${myarray[@]} | grep -q --line-regexp 'mypattern' – presto8 Aug 29 '19 at 14:04
  • As great as this is, it doesn't work if the array values have spaces. – Fmstrat Nov 20 '20 at 02:20
  • 2
    @Fmstrat I think you'll find that it does work in bash even if array values have spaces. 1. Did you specify `"${myarray[@]}"` with quotes? 2. Have you used `echo "${#myarray[@]}"` to verify that your array itself has the number of items you expect (and doesn't think that the spaces are separating items)? – JellicleCat Nov 20 '20 at 20:49
  • wrong! **your answer fasely returns `true`** for `myarray=($'not\nmypattern')` . Following fixes that but needs a recent version of `grep` (which groks `-z`) and also correctly works for any `$mypattern` you might think of: `printf '%s\0' "${myarray[@]}" | grep -Fqxz -- "$mypattern"` – Tino Mar 28 '21 at 09:40
  • 1
    @Tino do you think you could express that correction in a polite way that improves the community? – JellicleCat Mar 29 '21 at 05:59
  • A full example would be better. For example input and output etc. – oiyio Apr 29 '21 at 11:22
59
$ myarray=(one two three)
$ case "${myarray[@]}" in  *"two"*) echo "found" ;; esac
found
ghostdog74
  • 286,686
  • 52
  • 238
  • 332
  • 72
    Note that this doesn't iterate over each element in the array separately... instead it simply concatenates the array and matches "two" as a substring. This could cause undesirable behavior if one is testing whether the exact word "two" is an element in the array. – MartyMacGyver Aug 19 '13 at 23:21
  • I thought this was going to work for me in comparing file types but found that as the counters increased it was counting up too many values... boo! – Mike Q Jan 24 '14 at 22:02
  • 19
    wrong! Reason: ```case "${myarray[@]}" in *"t"*) echo "found" ;; esac``` outputs: `found` – Sergej Jevsejev Aug 02 '16 at 09:22
  • @MartyMacGyver, could you please have look on my addition to this answer https://stackoverflow.com/a/52414872/1619950 – Aleksandr Podkutin Sep 19 '18 at 22:20
56
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
Scott
  • 1,915
  • 2
  • 17
  • 24
  • That said, you can use an indexed for loop and avoid getting killed when an array element contains IFS: for (( i = 0 ; i < ${#array[@]} ; i++ )) – Matt K Sep 10 '10 at 15:45
  • @Matt: You have to be careful using `${#}` since Bash supports sparse arrays. – Dennis Williamson Sep 10 '10 at 15:58
  • @Paolo, if your array contains a space then just compare it as a string. a space is a string as well. – Scott Sep 10 '10 at 16:05
  • @Paolo: You can make that a function, but arrays can't be passed as arguments so you'll have to treat it as a global. – Dennis Williamson Sep 10 '10 at 16:08
  • Dennis is right. From the bash reference manual: "If the word is double-quoted, ... ${name[@]} expands each element of name to a separate word" – Matt K Sep 10 '10 at 16:09
  • I was pretty much sticking with the question, which said 'simplest'. This might not be the most efficient or best – Scott Sep 10 '10 at 17:10
22

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"
LeoRochael
  • 10,305
  • 5
  • 23
  • 32
  • +1 for the idea that you should be using an associative array. I think the code for `make_index` is a bit more contrived due to the indirection; you could have used a fixed array name with a much simpler code. – musiphil Jul 23 '15 at 23:33
18

I typically just use:

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

non zero value indicates a match was found.

... actually, to solve the problem mentioned with it not working with needle1 and needle2, if you only want an exact match, nothing more, nothing less, just add a w after the -o for a whole word match

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

EricB
  • 3
  • 2
Sean DiSanti
  • 329
  • 2
  • 6
  • True, this is definitely the easiest solution - should be marked answer in my opinion. At least have my upvote! [: – ToVine Apr 24 '15 at 21:35
  • 3
    That won't work for similar needles. For example, `haystack=(needle1 needle2); echo ${haystack[@]} | grep -o "needle" | wc -w` – Keegan May 29 '15 at 15:40
  • 1
    Very true. joining with a delimiter not present in any element and adding it to the needle would help with that. Maybe something like... (untested) `inarray=$(printf ",%s" "${haystack[@]}") | grep -o ",needle" | wc -w)` – Sean DiSanti May 30 '15 at 21:44
  • 2
    Using grep -x would avoid false positives: `inarray=$(printf ",%s" "${haystack[@]}") | grep -x "needle" | wc -l` – jesjimher Apr 07 '16 at 10:02
  • Perhaps simply `inarray=$(echo " ${haystack[@]}" | grep -o " needle" | wc -w)` as -x causes grep to try and match the entire input string – hallo Aug 12 '18 at 22:40
  • awesome variable names, loved it :) – Kabachok Sep 06 '18 at 10:04
17

Another one liner without a function:

(for e in "${array[@]}"; do [[ "$e" == "searched_item" ]] && exit 0; done) && echo "found" || echo "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
  return 1
}

example:

some_words=( these are some words )
find_in_array word "${some_words[@]}" || echo "expected missing! since words != word"
Augusto Hack
  • 1,804
  • 16
  • 31
estani
  • 17,829
  • 2
  • 74
  • 52
  • 1
    Why do we need a subshell here? – codeforester Mar 17 '18 at 23:42
  • 1
    @codeforester this is old... but as it was written you need it in order to break from it, that's what the `exit 0` does (stops asap if found). – estani Mar 19 '18 at 14:37
  • The end of the one liner should be `|| echo not found` instead of `|| not found` or the shell will try to execute a command by the name of *not* with argument *found* if the requested value is not in the array. – zoke Aug 05 '18 at 09:25
12
containsElement () { for e in "${@:2}"; do [[ "$e" = "$1" ]] && return 0; done; return 1; }

Now handles empty arrays correctly.

Niko
  • 25,682
  • 7
  • 85
  • 108
Yann
  • 322
  • 3
  • 13
  • How is this different than @patrik's answer? The only difference I see is `"$e" = "$1"` (instead of `"$e" == "$1"`) which looks like a bug. – CivFan Jul 25 '15 at 01:19
  • 1
    It is not. @patrik's merged my comment in his original answer back then (patch #4). Note: `"e" == "$1"` is syntactically clearer. – Yann Jul 28 '15 at 14:30
  • @CivFan In its current form this is shorter than the one in patrik's answer, because of the elegant ${@:2} and the self documenting $1. I would add that quoting is not necessary within [[ ]]. – Hontvári Levente Dec 19 '19 at 13:32
10

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.

hornetbzz
  • 8,226
  • 5
  • 33
  • 51
8

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.

Dennis Williamson
  • 303,596
  • 86
  • 357
  • 418
  • Why not just replace `words` with a regex `^words$` that matches only the entire string, which completely eliminates the need for checking each item individually? – Dejay Clayton Sep 12 '17 at 13:53
  • @DejayClayton: Because `pattern='^words$'; if [[ ${array[@]} =~ $pattern ]]` will never match since it's checking the _whole_ array at once as if it were a scalar. The individual checks in my answer are to be done only if there's a reason to proceed based on the rough match. – Dennis Williamson Sep 12 '17 at 17:20
  • Ah, I see what you're trying to do. I've proposed a variant answer that's more performant and secure. – Dejay Clayton Sep 13 '17 at 20:49
8

How to check if a Bash Array contains a value


False positive match

array=(a1 b1 c1 d1 ee)

[[ ${array[*]} =~ 'a' ]] && echo 'yes' || echo 'no'
# output:
yes

[[ ${array[*]} =~ 'a1' ]] && echo 'yes' || echo 'no'
# output:
yes

[[ ${array[*]} =~ 'e' ]] && echo 'yes' || echo 'no'
# output:
yes

[[ ${array[*]} =~ 'ee' ]] && echo 'yes' || echo 'no'
# output:
yes

Exact match

In order to look for an exact match, your regex pattern needs to add extra space before and after the value like (^|[[:space:]])"VALUE"($|[[:space:]])

# Exact match

array=(aa1 bc1 ac1 ed1 aee)

if [[ ${array[*]} =~ (^|[[:space:]])"a"($|[[:space:]]) ]]; then
    echo "Yes";
else
    echo "No";
fi
# output:
No

if [[ ${array[*]} =~ (^|[[:space:]])"ac1"($|[[:space:]]) ]]; then
    echo "Yes";
else
    echo "No";
fi
# output:
Yes

find="ac1"
if [[ ${array[*]} =~ (^|[[:space:]])"$find"($|[[:space:]]) ]]; then
    echo "Yes";
else
    echo "No";
fi
# output:
Yes

For more usage examples the source of examples are here

Rosta Kosta
  • 107
  • 1
  • 2
  • -1 as complete fail for `array=('not act1')`. Such wrong implementations even created major PITA in the past: A security adapter gets a list of all files in a directory and returns if access to the directory is allowed. Directories with `dir_protect` are protected. As uploads in directories always append an extension, you are safe, right? A hacker uploads `dir_protect hack.txt` (with a space in it, as spaces are common in Windows) and thus creates a DoS (the security adapter has no direct access to the directory, only to the list of files transferred). – Tino Mar 28 '21 at 09:56
  • There are many answers but there is only a few with input and output. Thanks for sharing your sample with input output. – oiyio Apr 29 '21 at 11:40
6
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
Steven Penny
  • 82,115
  • 47
  • 308
  • 348
  • 1
    This doesn't work with BSD-grep on Mac, as there is no --null-data. :( – Will Jul 26 '15 at 22:15
  • Correct, but you should use `grep -Fqxz -- c` in case `c` is replaced with something starting with `-`. – Tino Mar 28 '21 at 10:02
6

Here's a compilation of several possible implementations, complete with integrated verification and simple benchmarking (requires Bash >= 4.0):

#!/usr/bin/env bash

# Check if array contains item [$1: item, $2: array name]
function in_array_1() {
    local needle="$1" item
    local -n arrref="$2"
    for item in "${arrref[@]}"; do
        [[ "${item}" == "${needle}" ]] && return 0
    done
    return 1
}

# Check if array contains item [$1: item, $2: array name]
function in_array_2() {
    local needle="$1" arrref="$2[@]" item
    for item in "${!arrref}"; do
        [[ "${item}" == "${needle}" ]] && return 0
    done
    return 1
}

# Check if array contains item [$1: item, $2: array name]
function in_array_3() {
    local needle="$1" i
    local -n arrref="$2"
    for ((i=0; i < ${#arrref[@]}; i++)); do
        [[ "${arrref[i]}" == "${needle}" ]] && return 0
    done
    return 1
}

# Check if array contains item [$1: item, $2..$n: array items]
function in_array_4() {
    local needle="$1" item
    shift
    for item; do
        [[ "${item}" == "${needle}" ]] && return 0
    done
    return 1
}

# Check if array contains item [$1: item, $2..$n: array items]
function in_array_5() {
    local needle="$1" item
    for item in "${@:2}"; do
        [[ "${item}" == "${needle}" ]] && return 0
    done
    return 1
}

# Check if array contains item [$1: item, $2: array name]
function in_array_6() {
    local needle="$1" arrref="$2[@]" array i
    array=("${!arrref}")
    for ((i=0; i < ${#array[@]}; i++)); do
        [[ "${array[i]}" == "${needle}" ]] && return 0
    done
    return 1
}

# Check if array contains item [$1: item, $2..$n: array items]
function in_array_7() {
    local needle="$1" array=("${@:2}") item
    for item in "${array[@]}"; do
        [[ "${item}" == "${needle}" ]] && return 0
    done
    return 1
}

# Check if array contains item [$1: item, $2..$n: array items]
function in_array_8() {
    local needle="$1"
    shift
    while (( $# > 0 )); do
        [[ "$1" == "${needle}" ]] && return 0
        shift
    done
    return 1
}


#------------------------------------------------------------------------------


# Generate map for array [$1: name of source array, $2: name of target array]
# NOTE: target array must be pre-declared by caller using 'declare -A <name>'
function generate_array_map() {
    local -n srcarr="$1" dstmap="$2"
    local i key
    dstmap=()
    for i in "${!srcarr[@]}"; do
        key="${srcarr[i]}"
        [[ -z ${dstmap["${key}"]+set} ]] && dstmap["${key}"]=${i} || dstmap["${key}"]+=,${i}
    done
}

# Check if array contains item [$1: item, $2: name of array map]
function in_array_9() {
    local needle="$1"
    local -n mapref="$2"
    [[ -n "${mapref["${needle}"]+set}" ]] && return 0 || return 1
}


#------------------------------------------------------------------------------


# Test in_array function [$1: function name, $2: function description, $3: test array size]
function test() {
    local tname="$1" tdesc="$2" tn=$3 ti=0 tj=0 ta=() tct=0 tepapre="" tepapost="" tepadiff=()
    local -A tam=()

    echo -e "\e[1m${tname} (${tdesc}):\e[0m"

    # Generate list of currently defined variables
    tepapre="$(compgen -v)"

    # Fill array with random items
    for ((ti=0; ti < ${tn}; ti++)); do
        ta+=("${RANDOM} ${RANDOM} ${RANDOM} ${RANDOM}")
    done

    # Determine function call type (pass array items, pass array name, pass array map)
    case "${tname}" in
        "in_array_1"|"in_array_2"|"in_array_3"|"in_array_6") tct=0; ;;
        "in_array_4"|"in_array_5"|"in_array_7"|"in_array_8") tct=1; ;;
        "in_array_9") generate_array_map ta tam; tct=2; ;;
        *) echo "Unknown in_array function '${tname}', aborting"; return 1; ;;
    esac

    # Verify in_array function is working as expected by picking a few random
    # items and checking
    echo -e "\e[1mVerification...\e[0m"
    for ((ti=0; ti < 10; ti++)); do
        tj=$(( ${RANDOM} % ${#ta[@]} ))
        echo -n "Item ${tj} '${ta[tj]}': "
        if (( ${tct} == 0 )); then
            "${tname}" "${ta[tj]}" ta && echo -en "\e[1;32mok\e[0m" || echo -en "\e[1;31mnok\e[0m"
            echo -n " "
            "${tname}" "${ta[tj]}.x" ta && echo -en "\e[1;31mnok\e[0m" || echo -en "\e[1;32mok\e[0m"
        elif (( ${tct} == 1 )); then
            "${tname}" "${ta[tj]}" "${ta[@]}" && echo -en "\e[1;32mok\e[0m" || echo -en "\e[1;31mnok\e[0m"
            echo -n " "
            "${tname}" "${ta[tj]}.x" "${ta[@]}" && echo -en "\e[1;31mnok\e[0m" || echo -en "\e[1;32mok\e[0m"
        elif (( ${tct} == 2 )); then
            "${tname}" "${ta[tj]}" tam && echo -en "\e[1;32mok\e[0m" || echo -en "\e[1;31mnok\e[0m"
            echo -n " "
            "${tname}" "${ta[tj]}.x" tam && echo -en "\e[1;31mnok\e[0m" || echo -en "\e[1;32mok\e[0m"
        fi
        echo
    done

    # Benchmark in_array function
    echo -en "\e[1mBenchmark...\e[0m"
    time for ((ti=0; ti < ${#ta[@]}; ti++)); do
        if (( ${tct} == 0 )); then
            "${tname}" "${ta[ti]}" ta
        elif (( ${tct} == 1 )); then
            "${tname}" "${ta[ti]}" "${ta[@]}"
        elif (( ${tct} == 2 )); then
            "${tname}" "${ta[ti]}" tam
        fi
    done

    # Generate list of currently defined variables, compare to previously
    # generated list to determine possible environment pollution
    echo -e "\e[1mEPA test...\e[0m"
    tepapost="$(compgen -v)"
    readarray -t tepadiff < <(echo -e "${tepapre}\n${tepapost}" | sort | uniq -u)
    if (( ${#tepadiff[@]} == 0 )); then
        echo -e "\e[1;32mclean\e[0m"
    else
        echo -e "\e[1;31mpolluted:\e[0m ${tepadiff[@]}"
    fi

    echo
}


#------------------------------------------------------------------------------


# Test in_array functions
n=5000
echo
( test in_array_1 "pass array name, nameref reference, for-each-loop over array items" ${n} )
( test in_array_2 "pass array name, indirect reference, for-each-loop over array items" ${n} )
( test in_array_3 "pass array name, nameref reference, c-style for-loop over array items by index" ${n} )
( test in_array_4 "pass array items, for-each-loop over arguments" ${n} )
( test in_array_5 "pass array items, for-each-loop over arguments as array" ${n} )
( test in_array_6 "pass array name, indirect reference + array copy, c-style for-loop over array items by index" ${n} )
( test in_array_7 "pass array items, copy array from arguments as array, for-each-loop over array items" ${n} )
( test in_array_8 "pass array items, while-loop, shift over arguments" ${n} )
( test in_array_9 "pre-generated array map, pass array map name, direct test without loop" ${n} )

Results:

in_array_1 (pass array name, nameref reference, for-each-loop over array items):
Verification...
Item 862 '19528 10140 12669 17820': ok ok
Item 2250 '27262 30442 9295 24867': ok ok
Item 4794 '3857 17404 31925 27993': ok ok
Item 2532 '14553 12282 26511 32657': ok ok
Item 1911 '21715 8066 15277 27126': ok ok
Item 4289 '3081 10265 16686 19121': ok ok
Item 4837 '32220 1758 304 7871': ok ok
Item 901 '20652 23880 20634 14286': ok ok
Item 2488 '14578 8625 30251 9343': ok ok
Item 4165 '4514 25064 29301 7400': ok ok
Benchmark...
real    1m11,796s
user    1m11,262s
sys     0m0,473s
EPA test...
clean

in_array_2 (pass array name, indirect reference, for-each-loop over array items):
Verification...
Item 2933 '17482 25789 27710 2096': ok ok
Item 3584 '876 14586 20885 8567': ok ok
Item 872 '176 19749 27265 18038': ok ok
Item 595 '6597 31710 13266 8813': ok ok
Item 748 '569 9200 28914 11297': ok ok
Item 3791 '26477 13218 30172 31532': ok ok
Item 2900 '3059 8457 4879 16634': ok ok
Item 676 '23511 686 589 7265': ok ok
Item 2248 '31351 7961 17946 24782': ok ok
Item 511 '8484 23162 11050 426': ok ok
Benchmark...
real    1m11,524s
user    1m11,086s
sys     0m0,437s
EPA test...
clean

in_array_3 (pass array name, nameref reference, c-style for-loop over array items by index):
Verification...
Item 1589 '747 10250 20133 29230': ok ok
Item 488 '12827 18892 31996 1977': ok ok
Item 801 '19439 25243 24485 24435': ok ok
Item 2588 '17193 18893 21610 9302': ok ok
Item 4436 '7100 655 8847 3068': ok ok
Item 2620 '19444 6457 28835 24717': ok ok
Item 4398 '4420 16336 612 4255': ok ok
Item 2430 '32397 2402 12631 29774': ok ok
Item 3419 '906 5361 32752 7698': ok ok
Item 356 '9776 16485 20838 13330': ok ok
Benchmark...
real    1m17,037s
user    1m17,019s
sys     0m0,005s
EPA test...
clean

in_array_4 (pass array items, for-each-loop over arguments):
Verification...
Item 1388 '7932 15114 4025 15625': ok ok
Item 3900 '23863 25328 5632 2752': ok ok
Item 2678 '31296 4216 17485 8874': ok ok
Item 1893 '16952 29047 29104 23384': ok ok
Item 1616 '19543 5999 4485 22929': ok ok
Item 93 '14456 2806 12829 19552': ok ok
Item 265 '30961 19733 11863 3101': ok ok
Item 4615 '10431 9566 25767 13518': ok ok
Item 576 '11726 15104 11116 74': ok ok
Item 3829 '19371 25026 6252 29478': ok ok
Benchmark...
real    1m30,912s
user    1m30,740s
sys     0m0,011s
EPA test...
clean

in_array_5 (pass array items, for-each-loop over arguments as array):
Verification...
Item 1012 '29213 31971 21483 30225': ok ok
Item 2802 '4079 5423 29240 29619': ok ok
Item 473 '6968 798 23936 6852': ok ok
Item 2183 '20734 4521 30800 2126': ok ok
Item 3059 '14952 9918 15695 19309': ok ok
Item 1424 '25784 28380 14555 21893': ok ok
Item 1087 '16345 19823 26210 20083': ok ok
Item 257 '28890 5198 7251 3866': ok ok
Item 3986 '29035 19288 12107 3857': ok ok
Item 2509 '9219 32484 12842 27472': ok ok
Benchmark...
real    1m53,485s
user    1m53,404s
sys     0m0,077s
EPA test...
clean

in_array_6 (pass array name, indirect reference + array copy, c-style for-loop over array items by index):
Verification...
Item 4691 '25498 10521 20673 14948': ok ok
Item 263 '25265 29824 3876 14088': ok ok
Item 2550 '2416 14274 12594 29740': ok ok
Item 2269 '2769 11436 3622 28273': ok ok
Item 3246 '23730 25956 3514 17626': ok ok
Item 1059 '10776 12514 27222 15640': ok ok
Item 53 '23813 13365 16022 4092': ok ok
Item 1503 '6593 23540 10256 17818': ok ok
Item 2452 '12600 27404 30960 26759': ok ok
Item 2526 '21190 32512 23651 7865': ok ok
Benchmark...
real    1m54,793s
user    1m54,326s
sys     0m0,457s
EPA test...
clean

in_array_7 (pass array items, copy array from arguments as array, for-each-loop over array items):
Verification...
Item 2212 '12127 12828 27570 7051': ok ok
Item 1393 '19552 26263 1067 23332': ok ok
Item 506 '18818 8253 14924 30710': ok ok
Item 789 '9803 1886 17584 32686': ok ok
Item 1795 '19788 27842 28044 3436': ok ok
Item 376 '4372 16953 17280 4031': ok ok
Item 4846 '19130 6261 21959 6869': ok ok
Item 2064 '2357 32221 22682 5814': ok ok
Item 4866 '10928 10632 19175 14984': ok ok
Item 1294 '8499 11885 5900 6765': ok ok
Benchmark...
real    2m35,012s
user    2m33,578s
sys     0m1,433s
EPA test...
clean

in_array_8 (pass array items, while-loop, shift over arguments):
Verification...
Item 134 '1418 24798 20169 9501': ok ok
Item 3986 '12160 12021 29794 29236': ok ok
Item 1607 '26633 14260 18227 898': ok ok
Item 2688 '18387 6285 2385 18432': ok ok
Item 603 '1421 306 6102 28735': ok ok
Item 625 '4530 19718 30900 1938': ok ok
Item 4033 '9968 24093 25080 8179': ok ok
Item 310 '6867 9884 31231 29173': ok ok
Item 661 '3794 4745 26066 22691': ok ok
Item 4129 '3039 31766 6714 4921': ok ok
Benchmark...
real    5m51,097s
user    5m50,566s
sys     0m0,495s
EPA test...
clean

in_array_9 (pre-generated array map, pass array map name, direct test without loop):
Verification...
Item 3696 '661 6048 13881 26901': ok ok
Item 815 '29729 13733 3935 20697': ok ok
Item 1076 '9220 3405 18448 7240': ok ok
Item 595 '8912 2886 13678 24066': ok ok
Item 2803 '13534 23891 5344 652': ok ok
Item 1810 '12528 32150 7050 1254': ok ok
Item 4055 '21840 7436 1350 15443': ok ok
Item 2416 '19550 28434 17110 31203': ok ok
Item 1630 '21054 2819 7527 953': ok ok
Item 1044 '30152 22211 22226 6950': ok ok
Benchmark...
real    0m0,128s
user    0m0,128s
sys     0m0,000s
EPA test...
clean
Maxxim
  • 828
  • 8
  • 11
  • Worth to note, that pure putting of array to /dev/null with echo takes about half of the in_array_1 function. Also the solution which uses printf and grep (similar to https://stackoverflow.com/a/47541882/31086, but adopted to be compatible with this benchmark) is only tiny bit faster than in_array_1 – Slimak Jul 15 '20 at 18:30
  • Timing of `generate_array_map`? – Tino Mar 28 '21 at 14:14
  • It gives error in this line readarray -t tepadiff < – oiyio Apr 29 '21 at 11:36
  • @oiyio: what does the error message say? Are you using a very old Bash version (< 4.0)? – Maxxim Apr 29 '21 at 21:06
  • @Tino: negligible (<1s). If you are curious, just put 'time' before the call in function 'test'. – Maxxim Apr 29 '21 at 21:26
  • Yes, my bash version is 3.2.57 – oiyio Apr 30 '21 at 07:47
  • @oiyio: that version doesn't support readarray, namerefs etc. Script will only work with Bash >= 4.0. – Maxxim Apr 30 '21 at 09:36
  • ok. Thanks for explanation. – oiyio Apr 30 '21 at 10:02
5

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
Chris Prince
  • 6,390
  • 1
  • 38
  • 56
5

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.
Dejay Clayton
  • 3,210
  • 1
  • 24
  • 18
  • Very clever. I can see that most potential issues are prevented, but I'd want to test to see if there are any corner cases. Also, I'd like to see an example of handling point 5. Something like `printf -v pattern ',,%q,,' "$user_input"; if [[ "${array_str},," =~ $pattern ]]` perhaps. – Dennis Williamson Sep 13 '17 at 22:22
  • `case "$(printf ,,%q "${haystack[@]}"),," in (*"$(printf ,,%q,, "$needle")"*) true;; (*) false;; esac` – Tino Mar 06 '18 at 17:30
4

One-line check without 'grep' and loops

if ( dlm=$'\x1F' ; IFS="$dlm" ; [[ "$dlm${array[*]}$dlm" == *"$dlm${item}$dlm"* ]] ) ; then
  echo "array contains '$item'"
else
  echo "array does not contain '$item'"
fi

This approach uses neither external utilities like grep nor loops.

What happens here, is:

  • we use a wildcard substring matcher to find our item in the array that is concatenated into a string;
  • we cut off possible false positives by enclosing our search item between a pair of delimiters;
  • we use a non-printable character as delimiter, to be on the safe side;
  • we achieve our delimiter being used for array concatenation too by temporary replacement of the IFS variable value;
  • we make this IFS value replacement temporary by evaluating our conditional expression in a sub-shell (inside a pair of parentheses)
Sergey Ushakov
  • 2,149
  • 1
  • 22
  • 15
  • Eliminate dlm. Use IFS directly. – Robin A. Meade Apr 23 '20 at 04:32
  • This is best answer. I liked it so much, [I wrote a function using this technique](https://stackoverflow.com/a/61379914). – Robin A. Meade Apr 23 '20 at 05:29
  • Thank you @RobinA.Meade, I get your point, and your suggestion may help to save a number of chars in the script for ones who care :) Still I personally prefer to stay with current syntax, keeping in mind separation of concerns between custom delimitation and array concatenation... – Sergey Ushakov Aug 17 '20 at 12:39
  • The problem with this still is that array entries might contain the `$dlm` by accident! You can protect against this by using something like `dlm="${array[*]}"; dlm=",=${dlm//?/=}=,"` which makes the delimiter longer than the array, so the array cannot contain the delimiter by chance, but this looks a bit .. crank and probably makes this solution `O(n^2)` – Tino Mar 28 '21 at 10:21
4

The answer with most votes is very concise and clean, but it can have false positives when a space is part of one of the array elements. This can be overcome when changing IFS and using "${array[*]}" instead of "${array[@]}". The method is identical, but it looks less clean. By using "${array[*]}", we print all elements of $array, separated by the first character in IFS. So by choosing a correct IFS, you can overcome this particular issue. In this particular case, we decide to set IFS to an uncommon character $'\001' which stands for Start of Heading (SOH)

$ array=("foo bar" "baz" "qux")
$ IFS=$'\001'
$ [[ "$IFS${array[*]}$IFS" =~ "${IFS}foo${IFS}" ]] && echo yes || echo no
no
$ [[ "$IFS${array[*]}$IFS" =~ "${IFS}foo bar${IFS}" ]] && echo yes || echo no
yes
$ unset IFS

This resolves most issues false positives, but requires a good choice of IFS.

note: If IFS was set before, it is best to save it and reset it instead of using unset IFS


related:

kvantour
  • 20,742
  • 4
  • 38
  • 51
3

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.

Ecker00
  • 314
  • 1
  • 11
3

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
Aleksandr Podkutin
  • 2,333
  • 1
  • 15
  • 28
2

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)

Beorn Harris
  • 95
  • 1
  • 3
2

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.

Qwerty
  • 19,992
  • 16
  • 88
  • 107
1

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.

Barry Kelly
  • 39,856
  • 4
  • 99
  • 180
  • Can someone post an example of this used within an if, particularly how you pass in the array. I'm trying to check to see if an argument to the script was passed by treating the params as an array, but it doesn't want to work. params=("$@") check=array_contains ${params} 'SKIPDIRCHECK' if [[ ${check} == 1 ]]; then .... But when running the script with 'asas' as an argument, it keeps saying asas: command not found. :/ – Steve Childs Mar 31 '16 at 10:48
1

Using parameter expansion:

${parameter:+word} If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted.

declare -A myarray
myarray[hello]="world"

for i in hello goodbye 123
do
  if [ ${myarray[$i]:+_} ]
  then
    echo ${!myarray[$i]} ${myarray[$i]} 
  else
    printf "there is no %s\n" $i
  fi
done
  • `${myarray[hello]:+_}` works great for associaive arrays, but not for usual indexed arrays. The question is about finding a value in an aray, not checking if the key of an associative array exists. – Eric May 13 '20 at 11:33
1

keep it simple :

Array1=( "item1" "item2" "item3" "item-4" )
var="item3"

count=$(echo ${Array1[@]} | tr ' ' '\n' | awk '$1 == "'"$var"'"{print $0}' | wc -l)
[ $count -eq 0 ] && echo "Not found" || echo "found"
Mahmoud Odeh
  • 887
  • 1
  • 4
  • 16
0

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>

Qwerty
  • 19,992
  • 16
  • 88
  • 107
0

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. :-)

ghoti
  • 41,419
  • 7
  • 55
  • 93
  • What if the array contains square brackets? – gniourf_gniourf Feb 09 '17 at 07:02
  • @gniourf_gniourf - seems to be fine if square brackets are balanced, but I can see it being a problem if your array includes values with unbalanced square brackets. In that case I'd invoke the `eval` instruction at the end of the answer. :) – ghoti Feb 09 '17 at 11:10
  • It's not that I don't like `eval` (I have nothing against it, unlike most people who cry _`eval` is evil,_ mostly without understanding what's evil about it). Just that your command is broken. Maybe `%q` instead of `%s` would be better. – gniourf_gniourf Feb 09 '17 at 12:46
  • 1
    @gniourf_gniourf: I only meant the "another approach" bit (and I'm totally with you re `eval`, obviously), but you're absolutely right, `%q` appears to help, without breaking anything else that I can see. (I didn't realize that %q would escape square brackets too.) Another issue I saw and fixed was regarding whitespace. With `a=(one "two " three)`, similar to Keegan's issue: not only did `array_contains a "two "` get a false negative, but `array_contains a two` got a false positive. Easy enough to fix by setting `IFS`. – ghoti Feb 10 '17 at 13:06
  • Regarding whitespaces, isn't it because there are quotes missing? it also breaks with glob characters. I think you want this instead: `eval _arr=( $(eval printf '[%q]="1"\ ' "\"\${$1[@]}\"") )`, and you can ditch the `local IFS=`. There's still a problem with empty fields in the array, as Bash will refuse to create an empty key in an associative array. A quick hacky way to fix it is to prepend a dummy character, say `x`: `eval _arr=( $(eval printf '[x%q]="1"\ ' "\"\${$1[@]}\"") )` and `return $(( 1 - 0${_arr[x$2]} ))`. – gniourf_gniourf Feb 10 '17 at 18:13
0

The OP added the following answer themselves, with the commentary:

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
Charles Duffy
  • 235,655
  • 34
  • 305
  • 356
  • Note that `==` should generally be avoided in favor of `=`, which is guaranteed by the POSIX standard for `test` to work as a string-comparison operator. `==` is an extension, and not all shells honor it. Also, `function funcname() {` is a combination of the POSIX-standard syntax `funcname() {` and the legacy ksh syntax `function funcname {`, and yet is compatible with _neither_ POSIX sh, nor with legacy ksh. Just use the POSIX syntax instead for new code. – Charles Duffy Nov 14 '20 at 16:34
  • ...see https://wiki.bash-hackers.org/scripting/obsolete re: the above assertions regarding function declaration syntax. – Charles Duffy Nov 14 '20 at 16:35
0
: NeedleInArgs "$needle" "${haystack[@]}"
: NeedleInArgs "$needle" arg1 arg2 .. argN
NeedleInArgs()
{
local a b;
printf -va '\n%q\n' "$1";
printf -vb '%q\n' "${@:2}";
case $'\n'"$b" in (*"$a"*) return 0;; esac;
return 1;
}

Use like:

NeedleInArgs "$needle" "${haystack[@]}" && echo "$needle" found || echo "$needle" not found;
  • For bash v3.1 and above (printf -v support)
  • No forks nor external programs
  • No loops (except internal expansions within bash)
  • Works for all possible values and arrays, no exceptions, nothing to worry about

Can also be used directly like in:

if      NeedleInArgs "$input" value1 value2 value3 value4;
then
        : input from the list;
else
        : input not from list;
fi;

For bash from v2.05b to v3.0 printf lacks -v, hence this needs 2 additional forks (but no execs, as printf is a bash builtin):

NeedleInArgs()
{
case $'\n'"`printf '%q\n' "${@:2}"`" in
(*"`printf '\n%q\n' "$1"`"*) return 0;;
esac;
return 1;
}

Note that I tested the timing:

check call0:  n: t4.43 u4.41 s0.00 f: t3.65 u3.64 s0.00 l: t4.91 u4.90 s0.00 N: t5.28 u5.27 s0.00 F: t2.38 u2.38 s0.00 L: t5.20 u5.20 s0.00
check call1:  n: t3.41 u3.40 s0.00 f: t2.86 u2.84 s0.01 l: t3.72 u3.69 s0.02 N: t4.01 u4.00 s0.00 F: t1.15 u1.15 s0.00 L: t4.05 u4.05 s0.00
check call2:  n: t3.52 u3.50 s0.01 f: t3.74 u3.73 s0.00 l: t3.82 u3.80 s0.01 N: t2.67 u2.67 s0.00 F: t2.64 u2.64 s0.00 L: t2.68 u2.68 s0.00
  • call0 and call1 are different variants of calls to another fast pure-bash-variant
  • call2 is this here.
  • N=notfound F=firstmatch L=lastmatch
  • lowercase letter is short array, uppercase is long array

As you can see, this variant here has a very stable runtime, so it does not depend that much on the match position. The runtime is dominated mostly by array length. The runtime of the searching variant is highly depending on the match position. So in edge cases this variant here can be (much) faster.

But very important, the searching variant is much mor RAM efficient, as this variant here always transforms the whole array into a big string.

So if your RAM is tight and you expect mostly early matches, then do not use this here. However if you want a predictable runtime, have long arrays to match expect late or no match at all, and also double RAM use is not much of a concern, then this here has some advantage.

Script used for timing test:

in_array()
{
    local needle="$1" arrref="$2[@]" item
    for item in "${!arrref}"; do
        [[ "${item}" == "${needle}" ]] && return 0
    done
    return 1
}

NeedleInArgs()
{
local a b;
printf -va '\n%q\n' "$1";
printf -vb '%q\n' "${@:2}";
case $'\n'"$b" in (*"$a"*) return 0;; esac;
return 1;
}

loop1() { for a in {1..100000}; do "$@"; done }
loop2() { for a in {1..1000}; do "$@"; done }

run()
{
  needle="$5"
  arr=("${@:6}")

  out="$( ( time -p "loop$2" "$3" ) 2>&1 )"

  ret="$?"
  got="${out}"
  syst="${got##*sys }"
  got="${got%"sys $syst"}"
  got="${got%$'\n'}"
  user="${got##*user }"
  got="${got%"user $user"}"
  got="${got%$'\n'}"
  real="${got##*real }"
  got="${got%"real $real"}"
  got="${got%$'\n'}"
  printf ' %s: t%q u%q s%q' "$1" "$real" "$user" "$syst"
  [ -z "$rest" ] && [ "$ret" = "$4" ] && return
  printf 'FAIL! expected %q got %q\n' "$4" "$ret"
  printf 'call:   %q\n' "$3"
  printf 'out:    %q\n' "$out"
  printf 'rest:   %q\n' "$rest"
  printf 'needle: %q\n' "$5"
  printf 'arr:   '; printf ' %q' "${@:6}"; printf '\n'
  exit 1
}

check()
{
  printf 'check %q: ' "$1"
  run n 1 "$1" 1 needle a b c d
  run f 1 "$1" 0 needle needle a b c d
  run l 1 "$1" 0 needle a b c d needle
  run N 2 "$1" 1 needle "${rnd[@]}"
  run F 2 "$1" 0 needle needle "${rnd[@]}"
  run L 2 "$1" 0 needle "${rnd[@]}" needle
  printf '\n'
}

call0() { chk=("${arr[@]}"); in_array "$needle" chk; }
call1() { in_array "$needle" arr; }
call2() { NeedleInArgs "$needle" "${arr[@]}"; }

rnd=()
for a in {1..1000}; do rnd+=("$a"); done

check call0
check call1
check call2
Tino
  • 7,380
  • 3
  • 48
  • 52
-1

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.

jmpp
  • 109
  • 7
-1

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
robert
  • 1,163
  • 10
  • 19
  • I haven't been using bash for such a long time now that I have a hard time understanding the answers, or even what I wrote myself :) I cannot believe that this question is still getting activity after all this time :) – Paolo Tedesco Mar 07 '16 at 09:16
  • What about `test_arr=("hello" "world" "two words")`? – Qwerty Nov 01 '16 at 12:09
-1

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 ...

E. Körner
  • 120
  • 3
  • 8
-2

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.

Sven Rieke
  • 147
  • 6
-2

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.

Sigg3.net
  • 35
  • 4
-2

A little late, but you could use this:

#!/bin/bash
# isPicture.sh

FILE=$1
FNAME=$(basename "$FILE") # Filename, without directory
EXT="${FNAME##*.}" # Extension

FORMATS=(jpeg JPEG jpg JPG png PNG gif GIF svg SVG tiff TIFF)

NOEXT=( ${FORMATS[@]/$EXT} ) # Formats without the extension of the input file

# If it is a valid extension, then it should be removed from ${NOEXT},
#+making the lengths inequal.
if ! [ ${#NOEXT[@]} != ${#FORMATS[@]} ]; then
    echo "The extension '"$EXT"' is not a valid image extension."
    exit
fi
Coder-256
  • 4,041
  • 1
  • 18
  • 44
-2

I came up with this one, which turns out to work only in zsh, but I think the general approach is nice.

arr=( "hello world" "find me" "what?" )
if [[ "${arr[@]/#%find me/}" != "${arr[@]}" ]]; then
    echo "found!"
else
    echo "not found!"
fi

You take out your pattern from each element only if it starts ${arr[@]/#pattern/} or ends ${arr[@]/%pattern/} with it. These two substitutions work in bash, but both at the same time ${arr[@]/#%pattern/} only works in zsh.

If the modified array is equal to the original, then it doesn't contain the element.

Edit:

This one works in bash:

 function contains () {
        local arr=(${@:2})
        local el=$1
        local marr=(${arr[@]/#$el/})
        [[ "${#arr[@]}" != "${#marr[@]}" ]]
    }

After the substitution it compares the length of both arrays. Obly if the array contains the element the substitution will completely delete it, and the count will differ.

spelufo
  • 597
  • 2
  • 18
-2

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
}
Will
  • 21,498
  • 11
  • 84
  • 98
  • 1
    Why did you downvote and deletevote this? It's a valid way to solve the problem, and I documented it well. – Will May 25 '16 at 10:07
-2

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
Dylan
  • 406
  • 3
  • 7
-3

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.


UPD: Thanks to @ChrisCogdon note, this incorrect code was re-written and published as https://stackoverflow.com/a/58527681/972463 .

Sergey Ushakov
  • 2,149
  • 1
  • 22
  • 15
  • Just curious: will anyone of the downvoters care to explain? What's the use of just voting down at the cost of one's points silently? Is there anything really wrong about this answer? – Sergey Ushakov Aug 27 '16 at 15:23
  • Also strictly not a one-liner – m0skit0 Sep 24 '18 at 14:49
  • 2
    It doesn't work because the array expansion doesn't work the way you think. If `array=(one two three)` then `":${array[@]}:"` generates three words: `:one`, `two`, `three:` – Chris Cogdon Jun 19 '19 at 17:18
  • @ChrisCogdon Thank you for pointing out the flaw in array concatenation. The corrected version is now published as https://stackoverflow.com/a/58527681/972463 . – Sergey Ushakov Oct 23 '19 at 16:58