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 impl
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
)
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.
array=('hello' 'world' 'my' 'name' 'is' 'perseus')
word="world"
if echo "${array[@]}" | fgrep --word-regexp "$word"; then
: # do something
fi
array=('hello' 'world' 'my' 'name' 'is' 'perseus')
word="world"
if [[ "${array[*]}" =~ (^|[^[:alpha:]])$word([^[:alpha:]]|$) ]]; then
: # do something
fi
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
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.
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
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
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.
Therefore we have to set IFS to whatever we want to use as boundary (a colon in my case):
IFS=:
Then we wrap the element we are looking for in the same character:
test=:henry:
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 :(
If we look for an element that exists:
test=:perseus:
[[ ":${array[*]}:" =~ $test ]] && echo "found! :)" || echo "not found :("
found! :)
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.
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