【问题标题】:How can I check if a string is in an array without iterating over the elements?如何在不迭代元素的情况下检查字符串是否在数组中?
【发布时间】:2012-07-09 14:10:05
【问题描述】:

有没有一种方法可以检查字符串是否存在于字符串数组中 - 无需遍历数组?

例如,给定下面的脚本,如何正确实现它来测试存储在变量 $test 中的值是否存在于 $array 中?

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

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

注意

我正在使用 bash 4.1.5

【问题讨论】:

  • 我 100% 肯定这里已经存在一个相同的问题。不过还没找到。
  • @CharlesDuffy:这可能是您所指的:stackoverflow.com/questions/3685970/… 但是,我不喜欢该解决方案有两个原因:1. 它涉及遍历数组,2. 自定义函数必须写。我更喜欢使用“内置”bash 函数
  • @HomunculusReticulli 哦。如果你只想要内置函数,答案是“不,你不能那样做”——你应该在你的问题中指定它。
  • ...好吧,让我们更清楚一点 - 你不能想出一个非迭代的解决方案不使用关联数组

标签: bash


【解决方案1】:

总会有技术上的迭代,但它可以归结为shell的底层数组代码。 Shell expansions 提供隐藏实现细节的抽象,并避免在 shell 脚本中显式循环的必要性。

使用 fgrep 可以更轻松地处理此用例的字边界,它具有处理全字固定字符串的内置功能。正则表达式匹配更难正确,但下面的示例适用于提供的语料库。

外部 Grep 进程

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

Bash 正则表达式测试

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

【讨论】:

  • “总是”有点强。关联数组查找是 O(1),而不是 O(n)。
【解决方案2】:

使用 bash 4,您可以做的最接近的事情就是使用关联数组。

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

...与 exact 完全相同:

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

...后跟:

tgt=henry
if [[ ${map["$tgt"]} ]] ; then
  : found
fi

【讨论】:

    【解决方案3】:

    在大多数情况下,以下方法会起作用。当然它有限制和限制,但易于阅读和理解。

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

    【讨论】:

      【解决方案4】:

      可以使用参数扩展将指定的字符串作为数组项删除,而不是迭代数组元素(更多信息和示例请参见Messing with arrays in bashModify 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
      )
      

      【讨论】:

        【解决方案5】:
        array=('hello' 'world' 'my' 'name' 'is' 'perseus')
        regex="^($(IFS=\|; echo "${array[*]}"))$"
        
        test='henry'
        [[ $test =~ $regex ]] && echo "found" || echo "not found"
        

        【讨论】:

        • 从数组中构建正则表达式,我想你会在这里赢得胜利。
        • @Charles-Duffy:使用正则表达式更新
        • 按原样没有特别概括——需要在扩展过程中转义任何数组内容,以免数组包含任何与正则表达式不直接匹配的内容。一种方法来做到这一点,虽然涉及一些性能损失:requote() { sed 's/[^^]/[&amp;]/g; s/\^/\\^/g' &lt;&lt;&lt; "$1"; }
        【解决方案6】:

        阅读您的帖子,我认为您不仅想知道数组中是否存在字符串(如标题所示),还想知道该字符串是否实际上对应于该数组的元素。如果是这种情况,请继续阅读。

        我找到了一种似乎可以正常工作的方法。

        如果您像我一样使用 bash 3.2(但也在 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  
        

        让我解释一下:

        此解决方法基于"${array[*]}"(注意双引号和星号)扩展为由 IFS 的第一个字符分隔的数组元素列表的原理。

        1. 因此,我们必须将 IFS 设置为我们想要用作边界的任何内容(在我的例子中是冒号):

          IFS=:
          
        2. 然后我们将要查找的元素包裹在同一个字符中:

          test=:henry:
          
        3. 最后我们在数组中寻找它。请注意我在进行测试时遵循的规则(它们都是强制性的):数组是双引号,使用 *(@ 不好)并且它包含在我之前设置的 IFS 的边界字符中:

          [[ ":${array[*]}:" =~ $test ]] && echo found || echo "not found :("
          not found :(
          
        4. 如果我们寻找一个存在的元素:

          test=:perseus:
          [[ ":${array[*]}:" =~ $test ]] && echo "found! :)" || echo "not found :("
          found! :)
          
        5. 对于另一个测试,我们可以将最后一个元素 'perseus' 更改为 'perseus smith'(带有空格的元素),只是为了检查它是否匹配(不应该):

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

          太棒了!这是预期的结果,因为“perseus”本身不再是一个元素。

        6. 重要提示!:完成测试后,请记住取消设置 IFS 以将其恢复为默认值(未设置):

          unset IFS
          

        到目前为止,这种方法似乎有效,您只需要小心并为 IFS 选择一个您确定您的元素不会包含的字符。

        希望对大家有所帮助!

        问候, 弗雷德

        【讨论】:

        • 我知道我迟到了,但我想补充一点,“ascii unit separator”是 IFS 使用的一个很好的候选人 :-) 这正是这个非- 可打印字符被发明。要在 vim 中键入它,请按 Ctrl+V,然后按 031。我喜欢将它分配给只读变量并在需要时使用它。
        【解决方案7】:
        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
        

        【讨论】:

        • 这个答案是迭代的(你认为${foo[@]/bar/} 是如何工作的?)和不准确的(不区分(1 "2 3" 4)(1 2 3 4)
        • 替换发生在每个数组条目然后连接它们而不是连接它们并进行替换,我检查了(这并不是说这不是一个糟糕的方法)。
        • 正确 -- 替换每个条目,然后连接。因此,如果您尝试测试 2 是否是一个条目,您不希望 2 3 被修改,在本例中就是这样。
        【解决方案8】:

        您可以使用关联数组,因为您使用的是 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
        

        如果数组元素未设置,则参数扩展替换为“X”(但如果它为空则不会)。通过这样做并检查结果是否与原始值不同,我们可以判断键是否存在而不管其值如何。

        【讨论】:

        • 认为我比你领先... 25 秒? :)
        • @DennisWilliamson:这是我希望的那种方法。这是否适用于任何 bash 数组。请参阅我之前的问题 (stackoverflow.com/questions/11395776/bash-string-interpolation),了解我是如何构建阵列的。如果您的解决方案适用于所有 bash 数组类型(不知道为什么),那么这是我的首选解决方案。
        • @HomunculusReticulli:它仅适用于关联数组(或常规数组,如果您正在测试是否存在数字索引)。
        • @HomunculusReticulli:我会用 Python 写整个东西。
        • 这完全是迂腐的,但我想指出的是,关联数组仍然在实现级别执行迭代。 shell 程序员只是不必手动实现索引操作。 :)
        【解决方案9】:
        #!/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"
        

        只需将扩展数组视为字符串并检查子字符串,但要隔离第一个和最后一个元素以确保它们不匹配为较少包含的子字符串的一部分,必须单独测试它们。

        【讨论】:

        • 你可以很容易地得到误报,如果你的数组在一个条目中有单词边界,你甚至不能制作一个正则表达式,你可以肯定工作
        • 那应该收紧一点。
        • 你能走过[[ "${array[@]}" =~ "${i:0:$((${#test}))}" ]]吗,我从哪里来?
        • 在当前答案中应该是 no i。我测试了一个循环和没有。复制了错误的行:p
        • @CalvinDuyCanhTran - 从[[ "${array[@]}" =~ " $test " ]] 中删除空格以使[[ "${array[@]}" =~ "$test" ]] 匹配没有空格的字符串。
        【解决方案10】:

        如果! grep -q "$item"

        应该可以正常工作。

        【讨论】:

        • 欢迎来到 SO,您能否为您的答案添加更多解释。
        猜你喜欢
        • 1970-01-01
        • 2022-01-24
        • 2022-07-30
        • 1970-01-01
        • 2013-10-31
        • 1970-01-01
        • 1970-01-01
        • 2020-03-27
        • 2016-01-17
        相关资源
        最近更新 更多