检查一个Bash数组是否包含一个值
在Bash中,testing数组是否包含特定值的最简单方法是什么?
编辑 :在答案和评论的帮助下,经过一些testing,我想出了这个:
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
我不知道这是否是最好的解决scheme,但它似乎工作。
有示例代码显示如何从数组中replace子string 。 您可以复制数组并尝试从副本中删除目标值。 如果副本和原件不同,则目标值存在于原始string中。
简单(但可能更耗时)的解决scheme是遍历整个数组,并单独检查每个项目。 这是我通常做的,因为它很容易实现,你可以把它包装在一个函数中(请参阅有关将数组传递给函数的信息 )。
以下是实现这一点的一个小function。 searchstring是第一个参数,其余的是数组元素:
containsElement () { local e match="$1" shift for e; do [[ "$e" == "$match" ]] && return 0; done return 1 }
该函数的testing运行如下所示:
$ array=("something to search for" "a string" "test2000") $ containsElement "a string" "${array[@]}" $ echo $? 0 $ containsElement "blaha" "${array[@]}" $ echo $? 1
这种方法的优点是不需要遍历所有元素(至less不是显式的)。 但是由于array_to_string_internal()
仍然循环数组元素,并将它们连接成一个string,所以它可能并不比所提出的循环解决scheme更高效,但它更具可读性。
if [[ " ${array[@]} " =~ " ${value} " ]]; then # whatever you want to do when arr contains value fi if [[ ! " ${array[@]} " =~ " ${value} " ]]; then # whatever you want to do when arr doesn't contain value fi
请注意,如果您正在search的值是包含空格的数组元素中的某个单词,则会给出误报。 例如
array=("Jack Brown") value="Jack"
正则expression式将看到Jack
在arrays中,即使它不是。 因此,如果您仍想使用此解决scheme,则必须更改IFS
和正则expression式中的分隔符
IFS=$'\t' array=("Jack Brown\tJack Smith") unset IFS value="Jack Smith" if [[ "\t${array[@]}\t" =~ "\t${value}\t" ]]; then echo "yep, it's there" fi
$ myarray=(one two three) $ case "${myarray[@]}" in *"two"*) echo "found" ;; esac found
for i in "${array[@]}" do if [ "$i" -eq "$yourValue" ] ; then echo "Found" fi done
对于string:
for i in "${array[@]}" do if [ "$i" == "$yourValue" ] ; then echo "Found" fi done
我通常只使用:
inarray=$(echo ${haystack[@]} | grep -o "needle" | wc -w)
非零值表示find匹配。
如果您需要性能,那么您不希望在search时遍历整个数组。
在这种情况下,您可以创build一个表示该数组索引的关联数组(哈希表或字典)。 即它将每个数组元素映射到数组中的索引:
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 }
那么你可以像这样使用它:
myarray=('aa' 'bb' 'c c') make_index myarray_index "${myarray[@]}"
并testing成员如下所示:
member="bb" # the "|| echo NOT FOUND" below is needed if you're using "set -e" test "${myarray_index[$member]}" && echo FOUND || echo NOT FOUND
还有:
if [ "${myarray_index[$member]}" ]; then echo FOUND fi
请注意,即使testing值或数组值中有空格,此解决scheme也能正确执行。
作为奖励,你也可以得到数组中的值的索引:
echo "<< ${myarray_index[$member]} >> is the index of $member"
这是一个小小的贡献:
array=(word "two words" words) search_string="two" match=$(echo "${array[@]:0}" | grep -o $search_string) [[ ! -z $match ]] && echo "found !"
注:这种方式不区分“两个字”的情况,但这不是问题中所要求的。
containsElement () { for e in "${@:2}"; do [[ "$e" = "$1" ]] && return 0; done; return 1; }
现在正确处理空数组。
如果你想做一个快速和肮脏的testing,看看是否值得迭代整个数组以获得精确的匹配,Bash可以将数组视为标量。 testing标量中的匹配,如果没有则跳过循环节省时间。 显然你可以得到误报。
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
这将输出“检查”和“匹配”。 用array=(word "two words" something)
它只会输出“检查”。 有了array=(word "two widgets" something)
将不会有输出。
另一个没有function的class轮:
(for e in "${array[@]}"; do [[ "$e" == "searched_item" ]] && exit 0; done) && echo found || not found
感谢@Qwerty的空间!
a=(bcd) if printf '%s\0' "${a[@]}" | grep -Fqxz c then echo 'array “a” contains value “c”' fi
如果你喜欢,你可以使用等效的长期选项:
--fixed-strings --quiet --line-regexp --null-data
这对我有用:
# 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 }
示例调用:
arr=("abc" "xyz" "123") if contains arr "abcx"; then echo "Yes" else echo "No" fi
给出:
array=("something to search for" "a string" "test2000") elem="a string"
那么简单的检查一下:
if c=$'\x1E' && p="${c}${elem} ${c}" && [[ ! "${array[@]/#/${c}} ${c}" =~ $p ]]; then echo "$elem exists in array" fi
哪里
c is element separator p is regex pattern
(单独赋值p的原因,而不是直接在[[]]中使用expression式是为了保持bash的兼容性4)
我通常写这些types的实用程序来操作variables的名称,而不是variables值,主要是因为bash不能通过引用传递variables。
这是一个与数组名称一起工作的版本:
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 }
有了这个,这个问题的例子变成:
array_contains A "one" && echo "contains one"
等等
这是我的承担。
我宁愿不使用bash for循环,如果我可以避免,因为这需要时间来运行。 如果有什么东西需要循环,让它是用低级语言编写而不是shell脚本。
function array_contains { # arrayname value local -A _arr=() local IFS= eval _arr=( $(eval printf '[%q]="1"\ ' "\${$1[@]}") ) return $(( 1 - 0${_arr[$2]} )) }
这可以通过创build一个临时关联数组_arr
,该数组的索引是从input数组的值中派生的。 (请注意,关联数组在bash 4及更高版本中可用,所以此函数在早期版本的bash中不起作用。)我们设置$IFS
以避免在空白处分词。
该函数不包含显式循环,尽pipe内部bash遍历input数组以填充printf
。 printf格式使用%q
来确保input数据被转义,以便它们可以安全地用作数组键。
$ a=("one two" three four) $ array_contains a three && echo BOOYA BOOYA $ array_contains a two && echo FAIL $
请注意,这个函数使用的所有内容都是内置的bash,因此即使在命令扩展中,也没有外部pipe道将您拖下。
如果你不喜欢使用eval
…嗯,你可以自由地使用另一种方法。 🙂
借用Dennis Williamson的答案 ,下面的解决scheme将数组,shell安全引用和正则expression式结合起来,以避免需要:遍历循环; 使用pipe道或其他子stream程; 或使用非bash实用程序。
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
上面的代码通过使用Bash正则expression式来匹配数组内容的string化版本。 有六个重要步骤可以确保正则expression式匹配不会被数组中的巧妙值组合所迷惑:
- 使用Bash的内置
printf
shell引用构build比较string%q
。 shell引用将确保特殊字符通过使用反斜杠\
转义而变成“shell安全”。 - select一个特殊字符作为值分隔符。 分隔符HAS是在使用
%q
时将被转义的特殊字符之一; 这是唯一的方法来保证数组中的值不能用聪明的方式来愚弄正则expression式匹配。 我select逗号,
因为这个angular色是最安全的时候,或以其他意想不到的方式被滥用。 - 将所有数组元素组合成一个string,使用特殊字符的两个实例作为分隔符。 以逗号为例,我用
,,%q
作为printf
的参数。 这一点非常重要,因为特殊字符的两个实例在出现作为分隔符时只能彼此相邻; 所有其他特殊字符的实例都将被转义。 - 将两个分隔符的尾随实例附加到string,以允许匹配数组的最后一个元素。 因此,不要与
${array_str}
进行比较,而应该与${array_str}
进行比较。 - 如果要search的目标string是由用户variables提供的,则必须使用反斜杠转义所有特殊字符的实例。 否则,正则expression式匹配容易被巧妙制造的数组元素所迷惑。
- 对string执行Bash正则expression式匹配。
这是我的这个问题。 这是简短的版本:
function arrayContains() { local haystack=${!1} local needle="$2" printf "%s\n" ${haystack[@]} | grep -q "^$needle$" }
而长版本,我认为在眼睛上更容易。
# 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$" }
例子:
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
使用grep
和printf
在一个新行上格式化每个数组成员,然后grep
行。
if printf '%s\n' "${array[@]}" | grep -x -q "search string"; then echo true; else echo false; fi
例:
$ array=("word", "two words") $ if printf '%s\n' "${array[@]}" | grep -x -q "two words"; then echo true; else echo false; fi true
请注意,这与delimeters和空格没有问题。
在回答之后,我读到了另外一个我特别喜欢的答案,但是这个答案有缺陷,并且是低估的。 我受到启发,这里有两种新的方法,我认为是可行的。
array=("word" "two words") # let's look for "two words"
使用grep
和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>
对于not_found结果添加|| <run_your_if_notfound_command_here>
|| <run_your_if_notfound_command_here>
结合这里提出的一些想法,你可以做一个优雅的,如果没有循环, 确切的词匹配统计 。
$find="myword" $array=(value1 value2 myword) if [[ ! -z $(printf '%s\n' "${array[@]}" | grep -w $find) ]]; then echo "Array contains myword"; fi
这不会触发word
或val
,只有整个单词匹配。 如果每个数组值包含多个单词,则会中断。
下面的代码检查给定的值是否在数组中,并返回从零开始的偏移量:
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
匹配是完整的值,因此设置VALUE =“三”不匹配。
如果你不想迭代,这可能是值得研究的:
#!/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
片段改编自: http : //www.thegeekstuff.com/2010/06/bash-array-tutorial/我觉得这很聪明。
编辑:你可能只是做:
if `echo ${myarray[@]} | grep -q "$wanted"` ; then echo "Value was found" fi
但后者只适用于数组包含唯一值。 在“143”中寻找1会给假阳性,methinks。
虽然这里有好几个很有帮助的答案,但我没有find一个看起来是高性能,跨平台和强大的正确组合。 所以我想分享我为我的代码写的解决scheme:
#!/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 }
从Sean DiSanti的上述回答扩展,我认为以下是一个简单而优雅的解决scheme,避免必须循环arrays,并不会由于部分匹配给出误报
function is_in_array { local ELEMENT="${1}" local DELIM="," printf "${DELIM}%s${DELIM}" "${@:2}" | grep -q "${DELIM}${ELEMENT}${DELIM}" }
这可以这样调用:
$ haystack=("needle1" "needle2" "aneedle" "spaced needle") $ is_in_array "needle" "${haystack[@]}" $ echo $? 1 $ is_in_array "needle1" "${haystack[@]}" $ echo $? 0
我已经提出的正则expression式技术的版本:
values=(foo bar) requestedValue=bar requestedValue=${requestedValue##[[:space:]]} requestedValue=${requestedValue%%[[:space:]]} [[ "${values[@]/#/X-}" =~ "X-${requestedValue}" ]] || echo "Unsupported value"
这里发生的事情是,你将所有支持的值扩展为单词,并在这个例子中预先加上一个特定的string“X-”,并且对所请求的值做同样的处理。 如果这个数组确实包含在数组中,那么结果string将最多匹配其中一个结果记号,或者完全相反。 在后一种情况下,|| 操作符触发器,并且您知道您正在处理不受支持的值。 在这之前,通过标准的shellstring操作,所请求的值被剥离了所有前导和尾随的空白。
我相信这是干净而优雅的,尽pipe我不太确定如果你的支持值数组特别大,它可能是如何performance的。
由Beorn哈里斯和loentar的答案组合提供了一个更有趣的单线testing:
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
这一个不使用额外的function,不会replacetesting,并添加额外的保护,以防止偶尔的错误匹配使用控制代码作为分隔符。
有点晚,但你可以使用这个:
#!/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
我想出了这个,结果只能在zsh中工作,但我认为一般的方法是好的。
arr=( "hello world" "find me" "what?" ) if [[ "${arr[@]/#%find me/}" != "${arr[@]}" ]]; then echo "found!" else echo "not found!" fi
只有当它启动${arr[@]/#pattern/}
或${arr[@]/%pattern/}
,才从每个元素中取出模式。 这两个replace工作在bash中,但是同时${arr[@]/#%pattern/}
只能在zsh中工作。
如果修改过的数组等于原始数组,则不包含该元素。
编辑:
这个在bash中工作:
function contains () { local arr=(${@:2}) local el=$1 local marr=(${arr[@]/#$el/}) [[ "${#arr[@]}" != "${#marr[@]}" ]] }
replace后,比较两个arrays的长度。 如果数组包含元素,则replace将完全删除它,并且计数将有所不同。
另存为file => build.sh
PROFILES=(docker local) if [[ " ${PROFILES[*]} " =~ " $1 " ]]; then echo "$1 is a valid profile" else echo "$1 is an invalid profile" fi
运行命令sh build.sh docker