24

Is there a way of checking if a string exists in an array of strings - without iterating through the array?

For example, given the script below, how I can correctly implement it to test if the value stored in variable $test exists in $array?

array=('hello' 'world' 'my' 'name' 'is' 'perseus')

#pseudo code
$test='henry'
if [$array[$test]]
   then
      do something
   else
      something else
fi

Note

I am using bash 4.1.5

15
  • I'm 100% positive an identical question already exists here. Haven't found it yet, though. Commented Jul 9, 2012 at 14:14
  • stackoverflow.com/questions/3685970/… Commented Jul 9, 2012 at 14:14
  • @CharlesDuffy: this may be the one you are referring to: stackoverflow.com/questions/3685970/… However, I don't like the solution for two reasons: 1. It involves iterating over the array, 2. A custom function must be written. I would prefer to use 'inbuilt' bash function(s) Commented Jul 9, 2012 at 14:16
  • @HomunculusReticulli Oh. If you want only builtins, the answer is "no, you can't do that" -- and you should have specified it in your question. Commented Jul 9, 2012 at 14:16
  • ...well, let's be clearer -- you can't come up with a non-iterative solution without using associative arrays. Commented Jul 9, 2012 at 14:18

11 Answers 11

18

With bash 4, the closest thing you can do is use associative arrays.

declare -A map
for name in hello world my name is perseus; do
  map["$name"]=1
done

...which does the exact same thing as:

declare -A map=( [hello]=1 [my]=1 [name]=1 [is]=1 [perseus]=1 )

...followed by:

tgt=henry
if [[ ${map["$tgt"]} ]] ; then
  : found
fi
Sign up to request clarification or add additional context in comments.

4 Comments

But strictly speaking (despite of the name) an "associative array" is not an array, but a hash table. For a pure array there is no alternative to expecting half of the items on average to find out whether a specific item is present in the array. The other alternative would be sorting the array (a real array this time) and doing a binary search. Then you would have to inspect at most log2(number_of_items).
@U.Windl, bash doesn't have things that are pure arrays. Numerically-indexed "arrays" are also maps in bash; they're just maps where the only allowed keys are integers. (That's why they're allowed to be sparse, and why arr=( [1500]="hi" ) is just as memory-efficient as arr=( [0]="hi" ))
Didn't know that works with declare -a arrays; I thought it requires declare -A arrays, thus switching to associative arrays (hash table).
@U.Windl, regular declare -a arrays are absolutely sparse (and implemented as hash tables under the hood). Try it yourself.
9

There will always technically be iteration, but it can be relegated to the shell's underlying array code. Shell expansions offer an abstraction that hide the implementation details, and avoid the necessity for an explicit loop within the shell script.

Handling word boundaries for this use case is easier with fgrep, which has a built-in facility for handling whole-word fixed strings. The regular expression match is harder to get right, but the example below works with the provided corpus.

External Grep Process

array=('hello' 'world' 'my' 'name' 'is' 'perseus')
word="world"
if echo "${array[@]}" | fgrep --word-regexp "$word"; then
    : # do something
fi

Bash Regular Expression Test

array=('hello' 'world' 'my' 'name' 'is' 'perseus')
word="world"
if [[ "${array[*]}" =~ (^|[^[:alpha:]])$word([^[:alpha:]]|$) ]]; then
    : # do something
fi

2 Comments

"Always" is a bit strong. Associative array lookups are O(1), not O(n).
@CharlesDuffy O(1) at best depending on the underlying implementation.
4

You can use an associative array since you're using Bash 4.

declare -A array=([hello]= [world]= [my]= [name]= [is]= [perseus]=)

test='henry'
if [[ ${array[$test]-X} == ${array[$test]} ]]
then
    do something
else
    something else
fi

The parameter expansion substitutes an "X" if the array element is unset (but doesn't if it's null). By doing that and checking to see if the result is different from the original value, we can tell if the key exists regardless of its value.

11 Comments

Think I beat you by... 25 seconds? :)
@DennisWilliamson: This is the kind of approach I was hoping for. Will this work for any bash array. See my previous question (stackoverflow.com/questions/11395776/bash-string-interpolation) to see how I am building my array. If your solution works for all bash array types (can't see why not), then this is my preferred solution.
@HomunculusReticulli: It only works for associative arrays (or regular arrays if you're testing for the presence of a numeric index).
This is completely pedantic, but I want to point out that associative arrays still perform iteration at the implementation level. The shell programmer just doesn't have to manually implement the indexing operation. :)
@Jens: That's a different test. In mine, the -X sets a default if the element is unset.
|
3
array=('hello' 'world' 'my' 'name' 'is' 'perseus')
regex="^($(IFS=\|; echo "${array[*]}"))$"

test='henry'
[[ $test =~ $regex ]] && echo "found" || echo "not found"

3 Comments

Build the regex from the array, and I think you'd have a winner here.
@Charles-Duffy: Updated with the regex
Not particularly generalized as-is -- need to escape any array contents during expansion, lest the array contain anything that doesn't match itself directly as a regex. One way to do this, though involving some performance hit: requote() { sed 's/[^^]/[&]/g; s/\^/\\^/g' <<< "$1"; }
2

Reading your post I take it that you don't just want to know if a string exists in an array (as the title would suggest) but to know if that string actually correspond to an element of that array. If this is the case please read on.

I found a way that seems to work fine .

Useful if you're stack with bash 3.2 like I am (but also tested and working in bash 4.2):

array=('hello' 'world' 'my' 'name' 'is' 'perseus')
IFS=:     # We set IFS to a character we are confident our 
          # elements won't contain (colon in this case)

test=:henry:        # We wrap the pattern in the same character

# Then we test it:
# Note the array in the test is double quoted, * is used (@ is not good here) AND 
# it's wrapped in the boundary character I set IFS to earlier:
[[ ":${array[*]}:" =~ $test ]] && echo "found! :)" || echo "not found :("
not found :(               # Great! this is the expected result

test=:perseus:      # We do the same for an element that exists
[[ ":${array[*]}:" =~ $test ]] && echo "found! :)" || echo "not found :("
found! :)               # Great! this is the expected result

array[5]="perseus smith"    # For another test we change the element to an 
                            # element with spaces, containing the original pattern.

test=:perseus:
[[ ":${array[*]}:" =~ $test ]] && echo "found!" || echo "not found :("
not found :(               # Great! this is the expected result

unset IFS        # Remember to unset IFS to revert it to its default value  

Let me explain this:

This workaround is based on the principle that "${array[*]}" (note the double quotes and the asterisk) expands to the list of elements of array separated by the first character of IFS.

  1. Therefore we have to set IFS to whatever we want to use as boundary (a colon in my case):

    IFS=:
    
  2. Then we wrap the element we are looking for in the same character:

    test=:henry:
    
  3. And finally we look for it in the array. Take note of the rules I followed to do the test (they are all mandatory): the array is double quoted, * is used (@ is not good) AND it's wrapped in the boundary character I set IFS to earlier:

    [[ ":${array[*]}:" =~ $test ]] && echo found || echo "not found :("
    not found :(
    
  4. If we look for an element that exists:

    test=:perseus:
    [[ ":${array[*]}:" =~ $test ]] && echo "found! :)" || echo "not found :("
    found! :)
    
  5. For another test we can change the last element 'perseus' for 'perseus smith' (element with spaces), just to check if it's a match (which shouldn't be):

    array[5]="perseus smith"
    test=:perseus:
    [[ ":${array[*]}:" =~ $test ]] && echo "found!" || echo "not found :("
    not found :(
    

    Great!, this is the expected result since "perseus" by itself is not an element anymore.

  6. Important!: Remember to unset IFS to revert it to its default value (unset) once you're done with the tests:

    unset IFS
    

So so far this method seems to work, you just have to be careful and choose a character for IFS that you are sure your elements won't contain.

Hope it helps anyone!

Regards, Fred

1 Comment

I know I'm late to the party, but I'd like to add that the "ascii unit separator" is a good candidate for the IFS to use :-) That is exactly what this non-printable character was invented for. To type it in vim press Ctrl+V followed by 031. I like to assign it to a readonly variable and use that when I need it.
2

In most cases, the following would work. Certainly it has restrictions and limitations, but easy to read and understand.

if [ "$(echo " ${array[@]} " | grep " $test ")" == "" ]; then
    echo notFound
else
    echo found
fi

Comments

1

Instead of iterating over the array elements it is possible to use parameter expansion to delete the specified string as an array item (for further information and examples see Messing with arrays in bash and Modify every element of a Bash array without looping).

(
set -f
export IFS=""

test='henry'
test='perseus'

array1=('hello' 'world' 'my' 'name' 'is' 'perseus')
#array1=('hello' 'world' 'my' 'name' 'is' 'perseusXXX' 'XXXperseus')

# removes empty string as array item due to IFS=""
array2=( ${array1[@]/#${test}/} )

n1=${#array1[@]}
n2=${#array2[@]}

echo "number of array1 items: ${n1}"
echo "number of array2 items: ${n2}"
echo "indices of array1: ${!array1[*]}"
echo "indices of array2: ${!array2[*]}"

echo 'array2:'
for ((i=0; i < ${#array2[@]}; i++)); do 
   echo "${i}: '${array2[${i}]}'"
done

if [[ $n1 -ne $n2 ]]; then
   echo "${test} is in array at least once! "
else
   echo "${test} is NOT in array! "
fi
)

Comments

0
q=( 1 2 3 )
[ "${q[*]/1/}" = "${q[*]}" ] && echo not in array || echo in array 
#in array
[ "${q[*]/7/}" = "${q[*]}" ] && echo not in array || echo in array 
#not in array

3 Comments

This answer is both iterative (how do you think ${foo[@]/bar/} works?) and inaccurate (not distinguishing between (1 "2 3" 4) and (1 2 3 4))
The replacement happens per array entry then concats them instead of concats them and does a replacement, I checked ( thats not to say this is not a terrible way to do this ) .
Correct -- replaces per entry, then concats. So, if you're trying to test whether 2 is an entry, you wouldn't want 2 3 to be modified, which in this case it would be.
0
#!/bin/bash

test="name"

array=('hello' 'world' 'my' 'yourname' 'name' 'is' 'perseus')
nelem=${#array[@]}
[[ "${array[0]} " =~ "$test " ]] || 
[[ "${array[@]:1:$((nelem-1))}" =~ " $test " ]] || 
[[ " ${array[$((nelem-1))]}" =~ " $test" ]] && 
echo "found $test" || echo "$test not found"

Just treat the expanded array as a string and check for a substring, but to isolate the first and last element to ensure they are not matched as part of a lesser-included substring, they must be tested separately.

9 Comments

You can super easily have false positives with this and if your array had word boundaries in an entry you could`t even craft a regex you could be sure worked
That ought to tighten it up a bit.
Could you walk through [[ "${array[@]}" =~ "${i:0:$((${#test}))}" ]] , where is the i coming from ?
Should be no i in the present answer. I tested with a loop and without. Copied the wrong line :p
@CalvinDuyCanhTran - remove the spaces from [[ "${array[@]}" =~ " $test " ]] to make [[ "${array[@]}" =~ "$test" ]] to match the string without whitespace.
|
0

for simple use cases I use something like this

array=( 'hello' 'world' 'I' 'am' 'Joe' )
word=$1

[[ " ${array[*]} " =~ " $word " ]] && echo "$word is in array!"

Note the spaces around ". This works as long as there are no spaces in the array values and the input doesn't match more values at once, like word='hello world'. If there are, you'd have to play with $IFS on top of that.

Comments

0
if ! grep -q "$item" <<< "$itemlist" ;  then .....

Should work fine.

2 Comments

Welcome to SO, Could you add more explanation to your answer please.
But grep will always inspect all items, even if you don't see it!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.