Reputation: 57192
In Bash, what is the simplest way to test if an array contains a certain value?
Upvotes: 826
Views: 969917
Reputation: 12197
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[*]} " =~ [[:space:]]${value}[[:space:]] ]]; then
# whatever you want to do when array contains value
fi
if [[ ! " ${array[*]} " =~ [[:space:]]${value}[[:space:]] ]]; 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="|"
array=("Jack Brown${IFS}Jack Smith")
value="Jack"
if [[ "${IFS}${array[*]}${IFS}" =~ "${IFS}${value}${IFS}" ]]; then
echo "true"
else
echo "false"
fi
unset IFS # or set back to original IFS if previously set
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"
Since this solution relies on regular expressions to check the array, you must escape any regex characters in the needle. For example, if you were searching with this.
array=("valueToBeFound")
value=".*"
You would have to set value
to \.\*
, otherwise all array elements will be seen to match.
Upvotes: 901
Reputation: 520
The only thing that worked for me (Almquist shell [no declare, shift, i++ ... ] - Android 11) was reconstructing the array. Create a file called array_contains.sh
#!/bin/sh
build_array_from_arguments() {
array=()
va=""
i=0
arr=( "$@" )
for ix in "${arr[@]}"; do
if [ "$i" -eq 0 ]; then
va="$ix"
else
array+=("$ix")
fi
i=$((i + 1))
done
for elem in "${array[@]}"; do
if [[ "$elem" == "$va" ]]; then
return 0
fi
done
return 1
}
build_array_from_arguments "$@"
call it like this
array_of_numbers=( "1" "2" "3" )
sh /sdcard/array_contains.sh "1" "${array_of_numbers[@]}" # or chmod if you have the rights
Upvotes: -1
Reputation: 7727
Great answers here, but I feel there is still a lack of a truly reusable function that allows easy calls in an if
for example and proper testing.
Borrowing a bit here and there from some of the answers above, the following function is what always works, on every case, no matter if the array or the search string contains line breaks, null bytes, weird encoding, etc:
in_array() {
local needle="$1"
shift 1
local haystack=("$@")
local value
for value in "${haystack[@]}"; do
[ "$value" = "$needle" ] && return 0
done
return 1
}
The function above can be used as follows. Notice how the arguments are quoted when calling the function, specially the one referring to the array contents. This is extremely important to preserve the correct separation between the elements of the array:
my_array=(foo bar 'fizz buzz')
if in_array foo "${my_array[@]}"; then
# "foo" is in the array, do stuff...
fi
if ! in_array fizz "${my_array[@]}"; then
# "fizz" is not in the array, do stuff...
fi
And here is the test coverage using bats
. Feel free to suggest more test cases!
#!/usr/bin/env bats
setup() {
load /usr/lib/bats/bats-support/load
load /usr/lib/bats/bats-assert/load
source "$PROJECT_ROOT/src/in_array.sh"
# Literal line break
export lb=$'\n'
# Literal null byte
export nb=$'\0'
}
@test "sanity check: exported var \$lb works as expected" {
str_with_linebreak="$(echo -e 'foo\nbar')"
assert [ "$str_with_linebreak" = "foo${lb}bar" ]
}
@test "sanity check: exported var \$nb works as expected" {
str_with_null_byte="$(echo -e 'foo\0bar')"
assert [ "$str_with_null_byte" = "foo${nb}bar" ]
}
@test "in_array: never match in an empty array" {
data=()
refute in_array foo "${data[@]}"
refute in_array '' "${data[@]}"
refute in_array ' ' "${data[@]}"
refute in_array "$lb" "${data[@]}"
refute in_array "$nb" "${data[@]}"
}
@test "in_array: matches single words correctly" {
data=(foo bar 123)
assert in_array foo "${data[@]}"
assert in_array bar "${data[@]}"
assert in_array 123 "${data[@]}"
refute in_array baz "${data[@]}"
refute in_array fo "${data[@]}"
refute in_array ar "${data[@]}"
refute in_array 12 "${data[@]}"
refute in_array 'foo bar' "${data[@]}"
refute in_array 'ar 12' "${data[@]}"
refute in_array '' "${data[@]}"
refute in_array ' ' "${data[@]}"
refute in_array "$lb" "${data[@]}"
refute in_array "$nb" "${data[@]}"
}
@test "in_array: matches terms containing spaces" {
data=(foo bar 'fizz buzz')
assert in_array foo "${data[@]}"
assert in_array bar "${data[@]}"
assert in_array 'fizz buzz' "${data[@]}"
refute in_array fizz "${data[@]}"
refute in_array buzz "${data[@]}"
refute in_array 'foo bar' "${data[@]}"
refute in_array 'bar fizz' "${data[@]}"
refute in_array '' "${data[@]}"
refute in_array ' ' "${data[@]}"
refute in_array "$lb" "${data[@]}"
refute in_array "$nb" "${data[@]}"
}
@test "in_array: matches terms containing line breaks" {
needle="fizz${lb}buzz"
data=(foo bar "$needle")
assert in_array foo "${data[@]}"
assert in_array bar "${data[@]}"
assert in_array "$needle" "${data[@]}"
refute in_array fizz "${data[@]}"
refute in_array buzz "${data[@]}"
refute in_array "foo${lb}bar" "${data[@]}"
refute in_array "bar${lb}fizz" "${data[@]}"
refute in_array '' "${data[@]}"
refute in_array ' ' "${data[@]}"
refute in_array "$lb" "${data[@]}"
refute in_array "$nb" "${data[@]}"
}
@test "in_array: matches terms containing null bytes" {
needle="fizz${nb}buzz"
data=(foo bar "$needle")
assert in_array foo "${data[@]}"
assert in_array bar "${data[@]}"
assert in_array "$needle" "${data[@]}"
refute in_array fizz "${data[@]}"
refute in_array buzz "${data[@]}"
refute in_array "foo${nb}bar" "${data[@]}"
refute in_array "bar${nb}fizz" "${data[@]}"
refute in_array '' "${data[@]}"
refute in_array ' ' "${data[@]}"
refute in_array "$lb" "${data[@]}"
refute in_array "$nb" "${data[@]}"
}
Output:
$ docker container run \
--rm --volume "$PWD:/code" --workdir /code \
bats/bats tests.bats
1..7
ok 1 sanity check: exported var $lb works as expected
ok 2 sanity check: exported var $nb works as expected
ok 3 in_array: never match in an empty array
ok 4 in_array: matches single words correctly
ok 5 in_array: matches terms containing spaces
ok 6 in_array: matches terms containing line breaks
ok 7 in_array: matches terms containing null bytes
Upvotes: 5
Reputation: 33
There are a lot of decent answers here, but due to some constraints, (self imposed), that I was facing in a script of my own (bash prompt), I bent over backwards to both avoid subshells, external programs, limitations on search terms or array values, bash loops and and bash regex.
The result of that is a reasonably simple, one-liner(ish) function that correctly identifies values within an array while handling spaces, quotes and other special characters correctly. It does this without the use of regex, without bash loops, without limitations on search terms or values and without setting any environment variables (apart from '_', which happens for every bash simple command).
It is also completely compatible with the restrictions of parameter expansion with Bash 4.2 + and most likely works with bash versions far lower than that so long as they support the parameter expansion features used.
Ok then, whats my fantastic solution to add to the pile of 30+?
Welp, it turns out that this is big O'l Big O notation downside attached to it.
Im pretty sure its O(n^{2})
.... yea ...
So is this useful ever? If you need precisely those things i mentioned for no apparent reason, and want to skip wasting way too much time figuring out that it was technically possible, then absolutely! If you are literally anyone else who wants a semblance of a reliable, linear solution, just go with any of the other list based option. Now that I've done it, I can confidently say this is a bad solution and does not work.
The big enemy is in the "${w//?/?(?)}", which does that for each character of each element of the entered array. Some arguments for a command? No problem. You want to use remotely sizable elements or arrays? LAAAAAAG.
Anyhow, I'll leave this just in case someone really really wants to know if its possible like I did, but I leave this more as a warning than a recommendation. I'd like to avoid someone else go designing a square wheel just to find out they're in for a bumpy ride.
# Function (shortest) (assumes extglob is enabled)
# Usage: item_in_array "search_term" "${target_array[@]}"
item_in_array() { [[ -n $1 ]] && local s="$1" && shift && local w="${s::-1}" && w="${w//?/?(?)}" && IFS='' w="${*##@(@(${s//?/?}+(?))|@($w)|!("$s"))}" && [[ -n $w ]]; }
# Function (shortest-er) (assumes extglob is enabled)
# Usage: item_in_array "search_term" "${target_array[@]}"
#
# Note: This function is the same as item_in_array, but uses
# bad substitution to check for a non-empty search term
# instead of the more readable [[ -n $1 ]]. It saves just
# three characters, but is less readable.
item_in_array2() { local s="$1" && shift && local w="${s::-1}" &>/dev/null && w="${w//?/?(?)}" && IFS='' w="${*##@(@(${s//?/?}+(?))|@($w)|!("$s"))}" && [[ -n $w ]]; }
# Function (short)
# Usage: item_in_array "search_term" "${target_array[@]}"
item_in_array_short() {
! shopt extglob &>/dev/null && shopt -s extglob
[[ -n $1 ]] && local s="$1" && shift && local w="${s::-1}" && w="${w//?/?(?)}" && IFS='' w="${*##@(@(${s//?/?}+(?))|@($w)|!("$s"))}" && [[ -n $w ]]
}
# Function (commented and somewhat readable)
# Usage: item_in_array "search_term" "${target_array[@]}"
item_in_array_long() {
# Ensure extglob is enabled
! shopt extglob &>/dev/null && shopt -s extglob
# Ensure that the search term is not empty, returning otherwise
[[ -n $1 ]] &&
# Take the first argument off of the argument list as the search term
local s="$1" && shift &&
# Create a pattern we will use later to set any elements shorter than
# the search term to an empty string
local w="${s::-1}" &>/dev/null && w="${w//?/?(?)}" &&
# 1.Set IFS to an empty string to prevent empty spaces in final result
# 2.Set the result to the target array with elements seperated with an
# empty string with the longest match of the following patterns
# removed from the beginning of the string:
# 1. Any element that is longer than the search term
# @(${s//?/?}+(?))
# 2. Any element that is the same as the search term
# @($w)
# Note: This is a pattern and is purposefully unquoted
# 3. Any element that is not the search term
# !("$s")
# 3. Return 0 if the result is not empty, 1 otherwise. This is as in
# step 2.2 we truncate the result with the search term to its last
# character, which is fine, as we truncate all elements of the same
# length as the search term to an empty string in step 2.3.
IFS='' w="${*##@(@(${s//?/?}+(?))|@($w)|!("$s"))}" && [[ -n $w ]]
}
# RESULT: expected ?? received for search_term
# PASSED: false == false for ' '
# PASSED: false == false for ''-b {{}{($")}} -b''
# PASSED: false == false for 'third'
# PASSED: true == true for 'third third'
# PASSED: false == false for '-a -v'
# PASSED: true == true for '--help'
# PASSED: false == false for 'third item'
# PASSED: true == true for '-b {{}{($")}} -b'
# PASSED: false == false for '{{}{($")}}'
# PASSED: true == true for '-v -a -v'
# PASSED: true == true for 'item four'
# PASSED: true == true for ' '
# PASSED: true == true for '-v'
# PASSED: false == false for 'item third'
# PASSED: false == false for '-b'
# PASSED: false == false for 'item'
#! /bin/bash
# Description
#
# This is a function that returns whether or not an item is an exact match for an element of the passed in array.
#
# This function has the following attributes works with all the constraints of parameter expansion in bash 4.2.
# It avoids for loops, while loops, sub-shells and bash regex (to avoid changing BASH_REMATCH).
# It uses just shell parameter expansions and string comparisons
# It is also the shortest function for this purpose vs all the other functions of a similar goal that I have seen,
# hence this gist.
#
# Note: This function requires extglob to be available.
# Function (commented and somewhat readable)
# Usage: item_in_array "search_term" "${target_array[@]}"
item_in_array_long() {
# Ensure extglob is enabled
! shopt extglob &>/dev/null && shopt -s extglob
# Ensure that the search term is not empty, returning otherwise
[[ -n $1 ]] &&
# Take the first argument off of the argument list as the search term
local s="$1" && shift &&
# Create a pattern we will use later to set any elements shorter than
# the search term to an empty string
local w="${s::-1}" &>/dev/null && w="${w//?/?(?)}" &&
# 1.Set IFS to an empty string to prevent empty spaces in final result
# 2.Set the result to the target array with elements seperated with an
# empty string with the longest match of the following patterns
# removed from the beginning of the string:
# 1. Any element that is longer than the search term
# @(${s//?/?}+(?))
# 2. Any element that is the same as the search term
# @($w)
# Note: This is a pattern and is purposefully unquoted
# 3. Any element that is not the search term
# !("$s")
# 3. Return 0 if the result is not empty, 1 otherwise. This is as in
# step 2.2 we truncate the result with the search term to its last
# character, which is fine, as we truncate all elements of the same
# length as the search term to an empty string in step 2.3.
IFS='' w="${*##@(@(${s//?/?}+(?))|@($w)|!("$s"))}" && [[ -n $w ]]
}
# Function (short)
# Usage: item_in_array "search_term" "${target_array[@]}"
item_in_array_short() {
! shopt extglob &>/dev/null && shopt -s extglob
[[ -n $1 ]] && local s="$1" && shift && local w="${s::-1}" && w="${w//?/?(?)}" && IFS='' w="${*##@(@(${s//?/?}+(?))|@($w)|!("$s"))}" && [[ -n $w ]]
}
# Function (shortest) (assumes extglob is enabled)
# Usage: item_in_array "search_term" "${target_array[@]}"
item_in_array() { [[ -n $1 ]] && local s="$1" && shift && local w="${s::-1}" && w="${w//?/?(?)}" && IFS='' w="${*##@(@(${s//?/?}+(?))|@($w)|!("$s"))}" && [[ -n $w ]]; }
# Function (shortest-er) (assumes extglob is enabled)
# Usage: item_in_array "search_term" "${target_array[@]}"
#
# Note: This function is the same as item_in_array, but uses
# bad substitution to check for a non-empty search term
# instead of the more readable [[ -n $1 ]]. It saves just
# three characters, but is less readable.
item_in_array2() { local s="$1" && shift && local w="${s::-1}" &>/dev/null && w="${w//?/?(?)}" && IFS='' w="${*##@(@(${s//?/?}+(?))|@($w)|!("$s"))}" && [[ -n $w ]]; }
# Tests
test_array=(
'first item'
' '
'second item'
'third third'
'-v -a -v'
'-b -b -b'
'item four'
'-v'
'--help'
'-b {{}{($")}} -b'
)
declare -A test_searches=(
['item']=false
['third item']=false
['third']=false
['-a -v']=false
['item third']=false
['third third']=true
['item four']=true
['-v -a -v']=true
['-a -v']=false
['-v']=true
['-b']=false
[' ']=false
[' ']=true
['--help']=true
['{{}{($")}}']=false
["'-b {{}{($\")}} -b'"]=false
['-b {{}{($")}} -b']=true
)
test_item_in_array_function() {
local function_name="$1"
shift
printf '\n\e[48;2;50;50;25mTESTING: %-35s\e[0m\n' "$function_name"
printf '\e[48;2;40;40;40mRESULT: expected ?? received for search_term\e[0m\n'
for search_term in "${!test_searches[@]}"; do
expected="${test_searches[$search_term]}"
if "$function_name" "$search_term" "${test_array[@]}"; then
received=true
else
received=false
fi
if [[ $received == "$expected" ]]; then
printf "\e[48;2;0;20;0mPASSED: %8s == %-8s for '%s'\e[0m\n" "$expected" "$received" "$search_term"
else
printf "\e[48;2;20;0;0mFAILED: %8s != %-8s for '%s'\e[0m\n" "$expected" "$received" "$search_term"
fi
done
}
test_item_in_array_function item_in_array_long
test_item_in_array_function item_in_array_short
test_item_in_array_function item_in_array
test_item_in_array_function item_in_array2
Side Notes
I created this with the intent of being a faster, smaller solution that would not use bash regex as I did not want to have to try to deal with the consequences of potential situations where I might use this before retrieving $BASH_REMATCH
I have not actually timed this vs the other solutions here, I am just assuming it should be faster given that while of course loops go on somewhere along the path in this code, it is all done in the c implementation of parameter expansion which I assume should make this faster, though I think its possible it is not, due to the increased amount of string comparisons necessary.
Why would I waste the time coming up with such a weird, unique, shell parameter expansion based solution instead of settling for the answer I kept getting; That it wasn't possible? I felt like there must be a way, and after posting a prior solution that was wrong in many edge cases, I decided I had to find the answer, which is this solution that has solved for those previous cases.
Upvotes: 0
Reputation: 4963
Below is a small function for achieving this. The search string is the first argument and the rest are the array elements:
set +e #otherwise the script will exit on error
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
Upvotes: 483
Reputation: 30206
printf '%s\0' "${myarray[@]}" | grep -F -x -z -- 'myvalue'
Explanation
The printf
statement prints each element of the array, delimited by null characters.
The grep
statement uses the following flags to match an item that contains exactly the string given as myvalue
(no more, no less):
-z
/--null-data
- Lines are terminated by a zero byte instead of a newline.-F
/--fixed-strings
- Interpret PATTERNS as fixed strings, not regular expressions.-x
/--line-regexp
- Select only those matches that exactly match the whole line.--
- marks the end of command-line options, making Grep process "myvalue
" as a non-option argument even if it starts with a dashWhy do we use a null byte \0
instead of a newline \n
? Your array may actually contain newlines within its elements. (If you know that it doesn't, feel free to drop the -z
grep option and substitude %s\n
as your first printf arg.)
Usage
To put this into an if ... then
statement:
if printf '%s\0' "${myarray[@]}" | grep -Fxqz -- 'myvalue'; 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."
Update: Thanks, presto8, for pointing out the --line-regexp
flag. Thanks, Tino, for pointing out the case where newlines can exist within array items.
Upvotes: 161
Reputation: 637
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
flag after the -o
for a whole word match:
inarray=$(echo ${haystack[@]} | grep -ow "needle" | wc -w)
Upvotes: 48
Reputation: 7564
These functions account for indexed arrays and associative arrays. They can be improved upon by upgrading the search algorithm from a linear search to a binary one (for large data sets).
##
# Determines if a value exists in an array.
###
function hasArrayValue ()
{
local -r needle="{$1:?}"
local -nr haystack="{$2:?}" # Where you pass by reference to get the entire array in one argument.
# Linear search. Upgrade to binary search for large datasets.
for value in "${haystack[@]}"; do
if [[ "$value" == "$needle" ]]; then
return 0
fi
done
return 1
}
##
# Determines if a value exists in an associative array / map.
###
function hasMapValue ()
{
local -r needle="{$1:?}"
local -nr haystack="{$2:?}"
# Linear search. Upgrade to binary search for large datasets.
for value in "${haystack[@]}"; do
if [[ $value == $needle ]]; then
return 0
fi
done
return 1
}
Yes, same logic, but in dealing with bash, if might (might) be useful to have a function with a name that lets you know what is being iterated over (or not).
Upvotes: 6
Reputation: 153
I see a few ways to approach the problem.
For index array using grep
grep ${value} <<< ${array[*]} && true || false
For associative array keys using grep
grep ${value} <<< "${!array[*]}" && true || false
One could use awk, but it's probably overkill.
awk --assign "v=${value}" '$v~$0 {print true}' <<<"${!array[*]}
Case statement.
case "${array[*]}" in (*${value}*) true ;; (*) false ;; esac
Bash conditional expressions in ksh88 style double square bracket:
[[ ${array[@]} =~ ${value} ]] && true || false
note: the order is important, the regex is on the right side of =~
match operator.
Bash for loop
for ((i=0;i<"${#array[*]}";i++)) ; [[ ${array[i]} = $value ]] && break 0 &> /dev/null || continue; done
Note, in this special case the truthy logic is inversed, I.E. 1=true, 0=false. That is because we use break 0
to force break
builtin to have an exit code besides true, which is always the case unless the break n
parameter is less than 1. We imperatively want to break the loop, and we want a boolean exit code besides default 'true', so in this case we flip the logic. For that reason it would probably make more sense to use a function with return true
semantics.
Upvotes: 2
Reputation: 2955
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
Upvotes: 9
Reputation: 10459
: 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;
bash
v3.1 and above (printf -v
support)bash
)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-variantcall2
is this here.N
=notfound F
=firstmatch L
=lastmatchAs 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
Upvotes: 1
Reputation: 950
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"
Upvotes: 0
Reputation: 323
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
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
Upvotes: 14
Reputation: 295353
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
Upvotes: 1
Reputation: 26471
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:
Upvotes: 5
Reputation: 379
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.
Upvotes: 2
Reputation: 19
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
Upvotes: 0
Reputation: 26467
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"
Upvotes: 22
Reputation: 2473
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 .
Upvotes: -3
Reputation: 2473
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:
IFS
variable value;IFS
value replacement temporary by evaluating our conditional expression in a sub-shell (inside a pair of parentheses)Upvotes: 3
Reputation: 2570
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
Upvotes: 2
Reputation: 122
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 ...
Upvotes: -2
Reputation: 510
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
Upvotes: -2
Reputation: 3878
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:
printf
shell-quoting, %q
. Shell-quoting will ensure that special characters become "shell-safe" by being escaped with backslash \
.%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.,,%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.${array_str}
, compare against ${array_str},,
.Upvotes: 6
Reputation: 46836
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. :-)
Upvotes: 1
Reputation: 7564
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
Upvotes: 4
Reputation: 31929
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"
grep
and printf
:(printf '%s\n' "${array[@]}" | grep -x -q "two words") && <run_your_if_found_command_here>
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>
Upvotes: -1
Reputation: 31929
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.
Upvotes: 2
Reputation: 1532
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
Upvotes: -1
Reputation: 109
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.
Upvotes: -1