Bash中的数组交集

21

如何比较Bash中的两个数组以找到所有交集的值?

假设:
array1包含值1和2
array2包含值2和3

我应该得到2作为结果。

我的答案:

for item1 in $array1; do
    for item2 in $array2; do
        if [[ $item1 = $item2 ]]; then
            result=$result" "$item1
        fi
    done
done

我也在寻找其他的解决方案。


我认为你不会找到更好的方法来完成这个任务。Bash并不是专门用于数组操作的,我也想不出有哪个命令行工具可以用于查找两个数组的交集。 - Daniel Brockman
5个回答

19

将列表1的元素用作在列表2中查找的正则表达式(表示为字符串:${list2 [*]}):

list1=( 1 2 3 4   6 7 8 9 10 11 12)
list2=( 1 2 3   5 6   8 9    11 )

l2=" ${list2[*]} "                    # add framing blanks
for item in ${list1[@]}; do
  if [[ $l2 =~ " $item " ]] ; then    # use $item as regexp
    result+=($item)
  fi
done
echo  ${result[@]}
结果为
1 2 3 6 8 9 11

尽管对于这个问题提供的许多答案似乎适用于数组或列表交集。我选择这个答案,因为它不需要perl,并且似乎通过正则表达式提供了不使用第二个循环的捷径。它还回答了原始问题的数组交集,尽管我正在寻找列表交集,但我应该将列表重写为数组。谢谢大家。 - dabest1
如果数组包含转义空格的元素,则此解决方案将无法正常工作。 - Robsdedude

10

采用@Raihan的答案,并使其适用于非文件(尽管会创建FD)。 我知道这有点取巧,但似乎是个好的替代方案

副作用是输出数组将按字典顺序排序,希望这没问题 (另外我不知道你有什么类型的数据,所以我只测试了数字,如果你有带特殊字符的字符串等特殊情况可能需要进行额外的处理)

result=($(comm -12 <(for X in "${array1[@]}"; do echo "${X}"; done|sort)  <(for X in "${array2[@]}"; do echo "${X}"; done|sort)))

测试:

$ array1=(1 17 33 99 109)
$ array2=(1 2 17 31 98 109)

result=($(comm -12 <(for X in "${array1[@]}"; do echo "${X}"; done|sort)  <(for X in "${array2[@]}"; do echo "${X}"; done|sort)))

$ echo ${result[@]}
1 109 17

p.s. 我确定有一种方法可以让数组每行输出一个值,而不需要使用for循环,只是我忘记了(IFS?)


非常好的解决方案——我对在子shell中使用两个标准输入文件发生了什么感到困惑——看起来它以某种方式使用了/proc/self/fd,但我无法让它与其他任何东西(例如cat/echo)一起工作。 - Soren
@Soren:请参阅http://www.gnu.org/s/bash/manual/bash.html#Process-Substitution。尽管看起来类似于标准输入重定向,但这些表达式实际上被替换为文件名。我不知道为什么你无法通过`cat`使其正常工作。在我的系统上,`cat <(echo foo) <(echo bar)输出foo bar`(分两行)。在你的系统上不是这样吗? - ruakh
6
printf -- '%s\n' "${array[@]}"将输出数组中的每个元素,每个元素占一行。 - Noel Yap

5
你的答案有两个问题:
  • $array1 只会扩展为 array1 的第一个元素。至少在我安装的 Bash 版本中是这样的。这似乎不是文档化的行为,所以可能是版本相关的怪癖。
  • 在第一个元素被添加到 result 后,result 将包含一个空格,因此下一次运行 result=$result" "$item1 时将出现严重问题。(它不会附加到 result,而是执行由前两个项目组成的命令,并将环境变量 result 设置为空字符串。)更正:事实证明,我错了:单词分割不会在赋值内部发生。(请参见下面的评论。)
你需要的是这个:
result=()
for item1 in "${array1[@]}"; do
    for item2 in "${array2[@]}"; do
        if [[ $item1 = $item2 ]]; then
            result+=("$item1")
        fi
    done
done

也许我混淆了数组和列表。在Bash中,数组和列表有什么区别吗? - dabest1
1
@dabest1: "List" 在 Bash 中不是一个技术术语。如果你不是指 "array",那么我认为你可能是指一些模糊的东西,比如 "包含空格的字符串,其中空格应该被解释为分隔字符串的组件"。显然,这没有一个单词的术语。 :-) 如果你发布一些周围代码,显示这些 "arrays" 是如何初始化的,以及你如何使用它们,那可能会澄清很多问题。 - ruakh
无论你的意图是什么,你的这行代码 result=$result" "$item1 不会按照你想象的那样运行,除非你已经将 IFS 变量设置为某些奇怪的值,但我真的很怀疑你这样做了。 (如果你确实将 IFS 变量设置为某些奇怪的值,那么你就有不同的问题!) - ruakh
@ruahk:谢谢,我对问题不是很清楚。我正在使用由空格分隔的项目列表,result=$result" "$item1似乎可以正常工作,即使我没有设置IFS为任何值。我将保留问题不变,因为这仍将帮助其他人解决数组比较问题。 - dabest1
@dabest:好的,所以你没有使用“数组”。关于“result=$result $item1似乎工作得很好”的问题:糟糕,我的错误:事实证明(根据http://www.gnu.org/s/bash/manual/bash.html#Shell-Parameters),变量赋值时不执行单词拆分。我错了。 - ruakh

3

如果您要查找相交线路的两个文件(而不是数组),则可以使用comm命令。

$ comm -12 file1 file2

4
只有在文件被排序的情况下,这才起作用。 - ndnenkov

0

现在我理解了你所说的“数组”,首先,我认为你应该考虑使用实际的Bash数组。它们更加灵活,例如,数组元素可以包含空格,并且您可以避免*?触发文件名扩展的风险。

但是,如果您更喜欢使用现有的基于空格分隔的字符串方法,则我同意RHT的建议,使用Perl:

result=$(perl -e 'my %array2 = map +($_ => 1), split /\s+/, $ARGV[1];
                  print join " ", grep $array2{$_}, split /\s+/, $ARGV[0]
                 ' "$array1" "$array2")

在上面的Bash命令中,嵌入的Perl程序创建了一个名为%array2的哈希表,其中包含第二个数组的元素,然后它打印出任何存在于%array2中的第一个数组的元素。

这将与您的代码在如何处理第二个数组中的重复值方面略有不同;在您的代码中,如果array1两次包含x,而array2包含x三次,则result将包含x六次,而在我的代码中,result将只包含x两次。我不知道这是否重要,因为我不知道您的确切要求。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接