在Bash中比较/区分两个数组的差异

92

在Bash中,是否可以对两个数组进行差异计算?有什么好的方法吗?

代码:

Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" ) 

Array3 =diff(Array1, Array2)

Array3 ideally should be :
Array3=( "key7" "key8" "key9" "key10" )

浏览了解决方案后,我决定在需要进行差异比较的情况下不使用数组。 - x-yuri
10个回答

204
echo ${Array1[@]} ${Array2[@]} | tr ' ' '\n' | sort | uniq -u

输出

key10
key7
key8
key9

如果您需要的话,可以添加排序。


58
他进来了,他掌控了局面,然后他离开了。如果有人想知道如何将数值保存到数组中,请尝试使用以下命令:Array3=(\echo ${Array1[@]} ${Array2[@]} | tr ' ' '\n' | sort | uniq -u `)`。 - Anake
11
这就是 shell 编程的精髓:保持简单,利用已有的工具。如果你想要尝试其他解决方案,也可以,但使用一个更强大的编程语言可能会更容易些。 - wrangler
25
太棒了。对于那些需要“非对称差异”的人,可以通过输出“对称差异”和您感兴趣的数组的重复项来获得它。例如,如果您想要在Array1中不存在但在Array2中存在的值。“echo ${Array2[@]} ${Array3[@]} | tr' ' '\n' | sort | uniq -D | uniq”其中Array3是上述命令的输出。此外,如果去除数组符号并假定变量是空格分隔的字符串,则此方法符合posix shell标准。 - Arwyn
9
很棒的解决方案。如果数组元素可能包含空格,可以稍作改进:printf '%s\n' "${Array1[@]}" "${Array2[@]}" | sort | uniq -u - misberner
23
为了简化 @Arwyn 的建议,您可以将忽略的数组添加两次,以确保仅显示 Array2 中的差异。echo ${Array1[@]} ${Array1[@]} ${Array2[@]} | tr ' ' '\n' | sort | uniq -u - Christopher Markieta
显示剩余9条评论

49

如果你严格要求Array1 - Array2,那么

Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" )

Array3=()
for i in "${Array1[@]}"; do
    skip=
    for j in "${Array2[@]}"; do
        [[ $i == $j ]] && { skip=1; break; }
    done
    [[ -n $skip ]] || Array3+=("$i")
done
declare -p Array3

使用关联数组可能会提高运行时效率,但我个人不觉得值得麻烦。如果您需要处理足够多的数据才需要考虑这一点,那么shell就不是正确的工具。


对于像Dennis的答案一样的对称差异,现有的工具如comm可以使用,只要我们稍微调整输入和输出(因为它们适用于基于行的文件,而不是shell变量)。

在这里,我们告诉shell使用换行符将数组连接成单个字符串,并在从comm读取行时丢弃制表符到一个数组中。

$ oldIFS=$IFS IFS=$'\n\t'
$ Array3=($(comm -3 <(echo "${Array1[*]}") <(echo "${Array2[*]}")))
comm: file 1 is not in sorted order
$ IFS=$oldIFS
$ declare -p Array3
declare -a Array3='([0]="key7" [1]="key8" [2]="key9" [3]="key10")'

它会发出警告,因为按字典顺序排序,key1 < … < key9 > key10。但由于两个输入数组的排序方式相似,忽略该警告是可以的。您可以使用--nocheck-order来消除警告,或者在<(…)进程替换中添加| sort -u,如果无法保证输入数组的顺序和唯一性。


2
第一个片段可以处理带有嵌入式空格的元素,因此得到+1。第二个片段仅适用于带有嵌入式空格的元素。如果您直接在“Array3 = ...”命令之前添加“IFS=$'\n\t'”,则可以省略保存和恢复“$ IFS”。 - mklement0
2
@mklement0 你所建议的命令:IFS=$'\n\t' Array3=( ... ) 将会 全局设置 IFS。试试看吧! - gniourf_gniourf
@gniourf_gniourf:感谢您指出这个问题!因为我的错误可能会误导其他人,所以我将保留原始评论并在此解释:虽然在简单命令前添加_ad-hoc、命令本地变量赋值_是一种常见且有用的习惯用法,但在这里却行不通,因为我的_command完全由赋值组成。没有命令名称(外部可执行文件、内置命令)跟随这些赋值,这使得它们全部成为当前shell上下文中的_global_变量;请参阅man bash,第SIMPLE COMMAND EXPANSION节。 - mklement0
@ephemient,感谢您的帖子! - Stefan
第一个片段在使用空格方面效果很好!尽管我不得不将$j放在双引号中。我有一行代码aws ec2 --profile=profile_name describe-instances --instance-id i-0a8d8b5d1b8b9xxxx --query "Reservations[].Instances[].EnaSupport",尽管在两个数组1和2中都存在,但它被显示为不同之处。我使用ShellCheck进行了检查,它建议在"$j"周围添加双引号。 - undefined
显示剩余4条评论

15

每当出现涉及到可能未排序的唯一值的问题时,我立刻想到了awk。这是我的解决方案。

代码

#!/bin/bash

diff(){
  awk 'BEGIN{RS=ORS=" "}
       {NR==FNR?a[$0]++:a[$0]--}
       END{for(k in a)if(a[k])print k}' <(echo -n "${!1}") <(echo -n "${!2}")
}

Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" )
Array3=($(diff Array1[@] Array2[@]))
echo ${Array3[@]}

输出

$ ./diffArray.sh
key10 key7 key8 key9

*注意**:和其它答案一样,如果一个数组中有重复的键,它们只会被报告一次;这可能是你要寻找的行为,也可能不是。处理这种情况的awk代码会更加混乱且不够简洁。


总结行为和限制:(a) 执行对称差异:输出一个单一的数组,其中包含仅存在于任一输入数组中的元素(在 OP 的示例数据中与仅输出第一个数组中唯一元素相同),(b) 仅适用于没有嵌入空格的元素(满足 OP 的要求),(c) 输出数组中元素的顺序与输入元素的顺序没有保证的关系,因为 awk 无条件使用关联数组 - 如示例输出所示。 - mklement0
此外,这个回答使用了一个巧妙且值得注意但是如果不加解释就会让人困惑的bash回避方法,用于传递“_数组_”作为参数:Array1[@]Array2[@]以“_字符串_”形式被传递到shell函数diff()(像通常一样作为参数$1$2),数组名加上所有下标后缀[@]。然后shell函数使用bash的变量_间接引用_({!...})_间接地_引用原始数组的所有元素(${!1}${!2})。 - mklement0
如何将字符串 "a b C" 转换为数组? - brauliobo
发现一个错误:Array2中的元素不在Array1中将会在diff()函数中显示。 - brauliobo
此解决方案无法处理包含空格的数组元素。由于未使用引号的字符串被 shell 展开,示例脚本可能会以多种方式失败。如果在运行脚本之前执行 touch Array1@,则它会失败,因为未加引号的 shell GLOB 模式使用了字符串 Array1[@]Array2[@]。如果一个数组包含元素 *,则会失败,因为该未加引号的 GLOB 模式将匹配当前目录中的所有文件。 - Ian D. Allen

11

使用comm处理参数为ARR1ARR2的任务,并使用mapfile将其放回RESULT数组:

ARR1=("key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10")
ARR2=("key1" "key2" "key3" "key4" "key5" "key6")

mapfile -t RESULT < \
    <(comm -23 \
        <(IFS=$'\n'; echo "${ARR1[*]}" | sort) \
        <(IFS=$'\n'; echo "${ARR2[*]}" | sort) \
    )

echo "${RESULT[@]}" # outputs "key10 key7 key8 key9"

请注意,结果可能与源顺序不符。
奖励(也就是你在这里的目的):
function array_diff {
    eval local ARR1=\(\"\${$2[@]}\"\)
    eval local ARR2=\(\"\${$3[@]}\"\)
    local IFS=$'\n'
    mapfile -t $1 < <(comm -23 <(echo "${ARR1[*]}" | sort) <(echo "${ARR2[*]}" | sort))
}

# usage:
array_diff RESULT ARR1 ARR2
echo "${RESULT[@]}" # outputs "key10 key7 key8 key9"

使用eval是在bash中处理数组参数传递的最不坏的选择之一。此外,查看comm手册页;基于此代码,很容易实现例如array_intersect:只需使用-12作为comm选项。

注意 mapfile 需要 Bash 4。 - lantrix
2
@lantrix,“mapfile”可以很容易地用“while..read”替换,甚至在不需要数组作为结果的情况下完全删除。所有的魔法都在“comm”中发生。 - Alex Offshore

9
在Bash 4中:
declare -A temp    # associative array
for element in "${Array1[@]}" "${Array2[@]}"
do
    ((temp[$element]++))
done
for element in "${!temp[@]}"
do
    if (( ${temp[$element]} > 1 ))
    then
        unset "temp[$element]"
    fi
done
Array3=(${!temp[@]})    # retrieve the keys as values

编辑:

ephemient 指出了一个可能严重的错误。如果一个元素在一个数组中存在一个或多个副本,而在另一个数组中根本不存在,则它将被错误地从唯一值列表中删除。下面的版本尝试处理这种情况。

declare -A temp1 temp2    # associative arrays
for element in "${Array1[@]}"
do
    ((temp1[$element]++))
done

for element in "${Array2[@]}"
do
    ((temp2[$element]++))
done

for element in "${!temp1[@]}"
do
    if (( ${temp1[$element]} >= 1 && ${temp2[$element]-0} >= 1 ))
    then
        unset "temp1[$element]" "temp2[$element]"
    fi
done
Array3=(${!temp1[@]} ${!temp2[@]})

@ephemient: 对,类比的话就是 diff(1),也是对称的。此外,这个脚本可以通过将要查找唯一元素的数组添加到第一个版本第二行的列表中来查找任意数量数组的唯一元素。我添加了一个编辑,提供了一个处理一个数组中未出现在另一个数组中的重复项的版本。 - Dennis Williamson
你的第二个片段不会起作用,因为 > 只在 (( ... )) 中起作用,而不是在 [[ ... ]] 中;在后者中,它应该是 -gt;然而,由于你可能意味着 >= 而不是 >,所以 > 应该被替换为 -ge。在这个上下文中明确“对称”的含义是:输出是一个包含仅属于任一数组的唯一值的 单个 数组。 - mklement0
1
@mklement0: > 在双方括号内确实有效,但是它是按词法而不是按数字进行比较的。因此,在比较整数时,应该使用双括号 - 所以在这方面你是正确的。我已经相应地更新了我的答案。 - Dennis Williamson
@DennisWilliamson:感谢您对于[[ ... ]]中的>进行澄清,以及感谢您更新您的答案。然而,我认为应该是>= 1而不是> 1。更重要的是,((...))条件语句将会在不存在(空)的temp2元素中出现错误,因此您需要使用${temp2[$element]-0}或坚持使用[[...]]-ge - mklement0
由于我没有收到回复,也不想让一个明显有问题的答案存在,所以我冒昧修正了它。如果您认为这个修正是不正确或不合适的,请告诉我。总的来说,这个答案只适用于没有嵌入空格的数组元素(这符合OP的要求)。 - mklement0
显示剩余2条评论

8

还可以使用正则表达式(参考其他答案:在Bash中获取数组交集):

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[@]}:

结果:

$ bash diff-arrays.sh 
4 7 10 12

2
似乎很奇怪这条评论毫无理由地被踩了。如果有问题,请指出来,让所有人受益。 - philwalk
@philwalk,我个人没有给这个点赞,但它对外部列表中的每个项目都进行了完整的字符串迭代。从大O的角度来看,这是非常低效的——随着内容变得越来越长,速度会比必要的更慢。 - undefined

3
Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" )
Array3=( "key1" "key2" "key3" "key4" "key5" "key6" "key11" )
a1=${Array1[@]};a2=${Array2[@]}; a3=${Array3[@]}
diff(){
    a1="$1"
    a2="$2"
    awk -va1="$a1" -va2="$a2" '
     BEGIN{
       m= split(a1, A1," ")
       n= split(a2, t," ")
       for(i=1;i<=n;i++) { A2[t[i]] }
       for (i=1;i<=m;i++){
            if( ! (A1[i] in A2)  ){
                printf A1[i]" "
            }
        }
    }'
}
Array4=( $(diff "$a1" "$a2") )  #compare a1 against a2
echo "Array4: ${Array4[@]}"
Array4=( $(diff "$a3" "$a1") )  #compare a3 against a1
echo "Array4: ${Array4[@]}"

输出

$ ./shell.sh
Array4: key7 key8 key9 key10
Array4: key11

3

@ilya-bystrov最受欢迎的答案计算了Array1Array2之间的差异。请注意,这与从Array1中删除在Array2中也存在的项目是不同的。@ilya-bystrov的解决方案将两个列表连接起来并删除非唯一值。当Array2包含不在Array1中的项目时,这是一个巨大的区别:Array3将包含在Array2中但不在Array1中的值。

这里有一个纯Bash解决方案,用于从Array1中删除在Array2中也存在的项目(请注意Array2中的附加"key11"):

Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" "key11" )
Array3=( $(printf "%s\n" "${Array1[@]}" "${Array2[@]}" "${Array2[@]}" | sort | uniq -u) )

Array3 将由 "key7" "key8" "key9" "key10" 组成,并在尝试从 Array1 中删除项目时排除意外的 "key11"

如果您的数组项可能包含空格,请使用 mapfile 构建 Array3,如 @David 建议的那样:

Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" "key11" )
mapfile -t Array3 < <(printf "%s\n" "${Array1[@]}" "${Array2[@]}" "${Array2[@]}" | sort | uniq -u)

请注意:这假设Array1中的所有值都是唯一的。否则它们将不会出现在Array3中。如果Array1包含重复的值,则必须先删除重复项(请注意Array1中的重复"key10";如果您的项目包含空格,则可能使用mapfile):
Array1=( "key1" "key2" "key3" "key4" "key5" "key6" "key7" "key8" "key9" "key10" "key10" )
Array2=( "key1" "key2" "key3" "key4" "key5" "key6" "key11" )
Array3=( $({ printf "%s\n" "${Array1[@]} | sort -u; printf "%s\n" "${Array2[@]}" "${Array2[@]}"; } | sort | uniq -u) )

如果你想要将Array1中的重复项复制到Array2中,可以选择使用@ephemient的答案。如果Array1Array2非常大,同样适用:对于很多项来说,这是一个非常低效的解决方案,尽管对于少量项(小于100)来说可以忽略不计。如果你需要处理大型数组,请不要使用Bash。

1
不错!你应该使用mapfile来构建Array3,因为sloppyA=(..)扩展可能会在空格上出现问题: a=(string\ 1 string\ 2 string\ 3); b=(string\ 2);mapfile -t c < <(printf "%s\n" "${a[@]}" "${b[@]}" "${b[@]}" | sort | uniq -u); - David

0

这段代码替换为diff

echo ${test1[@]} ${test2[@]} | sed 's/ /\n/g' | sort | uniq -u

要逆序排列结果,请使用uniq -d


0
一个简洁的解决方案,可以处理包含空格的数组条目。
readarray -t array3 < <( grep --invert-match --fixed-strings --line-regexp --file=<( IFS=$'\n'; echo "${array2[*]}" ) <<<"$( IFS=$'\n'; echo "${array1[*]}" )" )

使用IFS和echo来显示array1和array2,每个条目一行。 使用grep显示array1中所有不在array2中的条目(使用--invert-match和--file=来删除条目)。 使用readarray将条目放入array3,以便处理每行一个条目。 这个解决方案保持array3的排序与array1相同。

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