在Bash中,测试数组是否包含某个值的最简单方法是什么?


当前回答

我看到了一些解决这个问题的方法。

对于使用grep的索引数组

grep ${value} <<< ${array[*]} && true || false

对于使用grep的关联数组键

grep ${value} <<< "${!array[*]}" && true || false

我们可以使用awk,但它可能太过了。

awk --assign "v=${value}" '$v~$0 {print true}' <<<"${!array[*]}

Case语句。

case "${array[*]}" in (*${value}*) true ;; (*) false ;; esac

Bash条件表达式在ksh88风格双方括号:

[[ ${array[@]} =~ ${value} ]] && true || false

注意:顺序很重要,正则表达式在=~ match运算符的右边。

Bash for循环

for ((i=0;i<"${#array[*]}";i++)) ; [[ ${array[i]} = $value ]] && break 0 &> /dev/null || continue; done

注意,在这种特殊情况下,真逻辑是颠倒的,即1=真,0=假。这是因为我们使用break 0强制break内置除true之外的退出代码,除非break n参数小于1,否则总是如此。我们必须要打破循环,我们想要一个布尔退出码除了默认的'true',所以在这种情况下,我们翻转了逻辑。因此,使用具有返回true语义的函数可能更有意义。

其他回答

以下是我的看法。

如果可以避免的话,我宁愿不使用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]} ))
}

这是通过创建一个临时关联数组_arr来实现的,它的索引是从输入数组的值派生出来的。(请注意,关联数组在bash 4及以上版本中可用,因此此函数在bash的早期版本中无效。)我们设置$IFS以避免在空格上分词。

该函数不包含显式循环,不过bash内部会遍历输入数组以填充printf。printf格式使用%q来确保输入数据被转义,这样它们就可以安全地用作数组键。

$ a=("one two" three four)
$ array_contains a three && echo BOOYA
BOOYA
$ array_contains a two && echo FAIL
$

注意,这个函数使用的所有东西都是bash内置的,因此没有外部管道拖您的后腿,即使在命令展开中也是如此。

如果你不喜欢使用eval…你可以自由地使用另一种方法。: -)

@ghostdog74关于使用大小写逻辑检查数组包含特定值的回答的一个小补充:

myarray=(one two three)
word=two
case "${myarray[@]}" in  ("$word "*|*" $word "*|*" $word") echo "found" ;; esac

或者打开extglob选项,你可以这样做:

myarray=(one two three)
word=two
shopt -s extglob
case "${myarray[@]}" in ?(*" ")"$word"?(" "*)) echo "found" ;; esac

我们也可以用if语句:

myarray=(one two three)
word=two
if [[ $(printf "_[%s]_" "${myarray[@]}") =~ .*_\[$word\]_.* ]]; then echo "found"; fi

下面是实现这一点的一个小函数。搜索字符串是第一个参数,其余是数组元素:

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
}

该函数的测试运行如下:

$ array=("something to search for" "a string" "test2000")
$ containsElement "a string" "${array[@]}"
$ echo $?
0
$ containsElement "blaha" "${array[@]}"
$ echo $?
1

回答完之后,我读到了另一个我特别喜欢的答案,但它有缺陷,遭到了不好评。我受到了启发,这里有两种可行的新方法。

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

虽然这里有几个很好的和有用的答案,但我没有找到一个似乎是性能、跨平台和健壮性的正确组合;所以我想分享一下我为我的代码编写的解决方案:

#!/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
}