如何在Bash中从数组中获取唯一值?

135

我有一个包含aa ab aa ac aa ad等元素的数组。现在我想从这个数组中选择所有唯一的元素。尽管在那个问题中提到可以使用sort | uniqsort -u来简单地实现,但是数组中没有任何变化... 代码如下:

我几乎和here一样有同样的问题。

echo `echo "${ids[@]}" | sort | uniq`

我做错了什么?
16个回答

182
有点巧妙,但应该可以解决问题:
echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '

为了将排序后的唯一结果保存回数组中,请执行数组赋值
sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))

如果你的Shell支持herestringsbash应该支持),你可以通过将其改为以下内容来节省一个echo进程:
tr ' ' '\n' <<< "${ids[@]}" | sort -u | tr '\n' ' '

2021年8月28日更新:

根据ShellCheck wiki 2207,应使用read -a管道以避免拆分。 因此,在bash中,命令应为:

IFS=" " read -r -a ids <<< "$(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')"

或者

IFS=" " read -r -a ids <<< "$(tr ' ' '\n' <<< "${ids[@]}" | sort -u | tr '\n' ' ')"

输入:

ids=(aa ab aa ac aa ad)

输出:

aa ab ac ad

解释:

  • "${ids[@]}" - 用于处理 shell 数组的语法,无论是作为 echo 还是 here 字符串的一部分。@ 部分表示“数组中的所有元素”
  • tr ' ' '\n' - 将所有空格转换为换行符。因为 shell 看到的数组是由空格分隔的单行元素;而 sort 命令期望输入在不同的行上。
  • sort -u - 排序并保留唯一元素
  • tr '\n' ' ' - 将之前添加的换行符转换回空格。
  • $(...) - 命令替换
  • 附: tr ' ' '\n' <<< "${ids[@]}" 是更有效的方法:echo "${ids[@]}" | tr ' ' '\n'

46
稍微整理一下:将唯一元素存储在一个新数组中:uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}" - glenn jackman
5
我不确定这是否是个例外情况,但将独特的项目放回数组需要额外的括号,例如:sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))。如果没有额外的括号,它会将其作为字符串处理。 - whla
3
如果您不想改变元素的排序顺序,可以使用... | uniq | ...代替... | sort -u | ... - Jesse Chisholm
2
@Jesse,“uniq”仅删除_连续_重复项。在这个回答的示例中,“sorted_unique_ids”最终将与原始“ids”相同。为了保持顺序,请尝试“… | awk'!seen [$0] ++'”。另请参见https://dev59.com/QXM_5IYBdhLWcg3wSBAU。 - Rob Kennedy
3
这会将包含空格的数组元素分成多个值,而使用数组的主要好处之一就是能够区分空格分隔字符串中的各个部分。 - bukzor
显示剩余6条评论

43

如果您正在运行 Bash 4 或更高版本(在任何现代 Linux 版本中应该都是如此),则可以通过创建一个新的关联数组来获取 bash 中的唯一数组值,其中包含原始数组的每个值。类似于这样:

$ a=(aa ac aa ad "ac ad")
$ declare -A b
$ for i in "${a[@]}"; do b["$i"]=1; done
$ printf '%s\n' "${!b[@]}"
ac ad
ac
aa
ad

这种方法可行是因为在任何数组(无论是关联数组还是普通数组,在任何语言中)中,每个键只能出现一次。当for循环到a[2]中的第二个值aa时,它覆盖了最初为a[0]设置的b[aa]

在本机bash中执行操作可能比使用管道和外部工具如sortuniq更快,但对于较大的数据集,如果您使用像awk、python等更强大的语言,您可能会看到更好的性能。

如果您感到自信,可以通过使用printf的格式重复利用其多个参数的能力来避免for循环,尽管似乎需要使用eval。(如果您对此满意,请停止阅读。)

$ eval b=( $(printf ' ["%s"]=1' "${a[@]}") )
$ declare -p b
declare -A b=(["ac ad"]="1" [ac]="1" [aa]="1" [ad]="1" )

这个解决方案需要使用 eval 的原因是数组值在单词拆分之前确定。这意味着命令替换的输出被视为单个单词而不是一组键值对。

虽然这里使用了子shell,但只使用bash内置命令来处理数组值。请审慎评估您对eval的使用。如果您不百分之百确信chepner、glenn jackman或greycat不会对您的代码有任何问题,请使用for循环。


2
@Benubird - 你能否将你的终端内容粘贴到pastebin上?它对我来说完美地运行,所以我最好的猜测是你(1)打错了字,(2)使用了旧版本的bash(关联数组添加到v4),或者(3)由于你邻居地下室的量子黑洞产生的大量宇宙背景辐射引起的干扰,导致你计算机内部的信号受到影响。 - ghoti
1
不能,没有保留那个不起作用的。但是,我刚刚尝试运行了你的代码,它可以工作,所以可能是宇宙辐射的原因。 - Benubird
猜测这个答案使用了bash v4(关联数组),如果有人尝试在bash v3中运行它,它将无法工作(可能不是@Benubird看到的)。Bash v3仍然是许多环境中的默认版本。 - nhed
@ghoti 我的 MacBook 是去年底购买的,预设 shell 是 $ bash --version GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin14) Copyright (C) 2007 Free Software Foundation, Inc. 尽管我最喜欢这个答案,但它存在可移植性问题。 - nhed
1
@nhed,我明白了。我发现我的最新的Yosemite Macbook基本上是同一个版本,尽管我已经从macports安装了v4。这个问题被标记为“linux”,但我已经更新了我的答案以指出要求。 - ghoti
显示剩余2条评论

29

我知道这个问题已经有了答案,但它在搜索结果中排名很高,可能会对某些人有所帮助。

printf "%s\n" "${IDS[@]}" | sort -u

例子:

~> IDS=( "aa" "ab" "aa" "ac" "aa" "ad" )
~> echo  "${IDS[@]}"
aa ab aa ac aa ad
~>
~> printf "%s\n" "${IDS[@]}" | sort -u
aa
ab
ac
ad
~> UNIQ_IDS=($(printf "%s\n" "${IDS[@]}" | sort -u))
~> echo "${UNIQ_IDS[@]}"
aa ab ac ad
~>

1
为了修复数组,我被迫这样做:ids=(ab "a a" ac aa ad ac aa);IFS=$'\n' ids2=(\printf "%s\n" "${ids[@]}" |sort -u`),所以我添加了 IFS=$'\n'`,正如 @gniourf_gniourf 建议的那样。 - Aquarius Power
我还必须备份并在命令之后恢复IFS值!否则会影响其他事情。 - Aquarius Power
@Jetse 这应该是被接受的答案,因为它只使用了两个命令,没有循环,没有 eval,并且是最紧凑的版本。 - mgutt
1
@AquariusPower 注意,你基本上是在执行:IFS=$'\n'; ids2=(...),因为在变量赋值之前临时赋值是不可能的。相反,请使用这个结构:IFS=$'\n' read -r -a ids2 <<<"$(printf "%s\n" "${ids[@]}" | sort -u)" - Yeti

18

如果您的数组元素包含空格或其他shell特殊字符(您能确定它们没有吗?),那么首先要捕获这些字符(而且您应该总是这样做),请用双引号表达您的数组!例如:"${a[@]}"。Bash会把它解释为“每个数组元素都是单独的参数”。在bash中,这始终有效。

然后,为了获得已排序(并且唯一)的数组,我们必须将其转换为sort理解的格式,并能够将其转换回bash数组元素。这是我想出的最好方法:

eval a=($(printf "%q\n" "${a[@]}" | sort -u))
很不幸,这在空数组的特殊情况下失败了,将空数组转换为一个由1个空元素组成的数组(因为printf没有参数但仍然像有一个空参数一样打印 - 请参见解释)。所以你必须在if语句或其他地方捕捉它。
说明: printf的%q格式会对打印的参数进行“shell转义”,以一种bash可以在类似于eval的情况下恢复的方式!因为每个元素都被单独打印出来,并通过换行符作为分隔符,而数组赋值则将每行作为一个元素,将转义后的值解析为文字。
例如。
> a=("foo bar" baz)
> printf "%q\n" "${a[@]}"
'foo bar'
baz
> printf "%q\n"
''

eval是必要的,以便从返回数组中的每个值中去除转义。


这是唯一能为我工作的代码,因为我的字符串数组中有空格。%q 是解决问题的关键。谢谢 :) - Somaiah Kumbera
如果您不想改变元素的顺序,请使用 uniq 而不是 sort -u - Jesse Chisholm
2
请注意,uniq 在未排序的列表上无法正常工作,因此必须始终与 sort 结合使用。 - Jean Paul
1
在未排序的列表上使用uniq将删除连续重复项。它不会删除被其他元素分隔开的相同列表元素。根据预期的数据和保持原始顺序的愿望,uniq可能足够有用。 - vontrapp
有人能告诉我为什么我需要使用%q而不是%s吗? - Changdae Park

14

'sort'可以用于对for循环的输出进行排序:

for i in ${ids[@]}; do echo $i; done | sort

使用"-u"来消除重复项:

for i in ${ids[@]}; do echo $i; done | sort -u

最后,您可以使用唯一元素覆盖数组:

ids=( `for i in ${ids[@]}; do echo $i; done | sort -u` )

如果您不想改变剩下的顺序,也可以不这样做:ids=( \for i in ${ids[@]}; do echo $i; done | uniq` )` - Jesse Chisholm
2
请注意,如果您不改变顺序,那么您也无法获得所需的结果,因为uniq仅删除相邻的重复行。 - Jason Kohles

11

这个也将保留顺序:

echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'

并使用唯一值修改原始数组:

ARRAY=($(echo ${ARRAY[@]} | tr [:space:] '\n' | awk '!a[$0]++'))

不要使用uniq。它需要排序,而awk不需要,此答案的目的是在输入未排序时保留顺序。 - bukzor
顺便提一下,这个例子因这篇博客文章而出名:https://catonmat.net/awk-one-liners-explained-part-two。多么迷人的awk一行代码。 - smac89

9
为创建由唯一值组成的新数组,请确保您的数组不为空,然后执行以下操作之一:

删除重复条目(带排序)

readarray -t NewArray < <(printf '%s\n' "${OriginalArray[@]}" | sort -u)

移除重复项(不排序)

readarray -t NewArray < <(printf '%s\n' "${OriginalArray[@]}" | awk '!x[$0]++')

警告:不要尝试像NewArray=( $(printf '%s\n' "${OriginalArray[@]}" | sort -u) )这样做。因为空格会导致出错。


删除重复条目(不排序)就像(排序)一样,只需将“sort -u”更改为“uniq”。 - Jesse Chisholm
1
@JesseChisholm uniq 只合并相邻的重复行,因此它与 awk '!x[$0]++' 不同。 - Six
1
@JesseChisholm 请删除误导性评论。 - bukzor

7
这个变体怎么样?
printf '%s\n' "${ids[@]}" | sort -u

然后 sorted_arr=($(printf '%s\n' "${ids[@]}" | sort -u) - algae
与@das.cyklone的答案相同。 - danday74

5

在不失去原始顺序的情况下:

uniques=($(tr ' ' '\n' <<<"${original[@]}" | awk '!u[$0]++' | tr '\n' ' '))

5

cat number.txt

1 2 3 4 4 3 2 5 6

用awk将一行打印成多列:cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}'
1
2
3
4
4
3
2
5
6

寻找重复记录:cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk 'x[$0]++'
4
3
2

替换重复记录:cat number.txt | awk '{for(i=1;i<=NF;i++) print $i}' |awk '!x[$0]++'
1
2
3
4
5
6

查找唯一的记录: cat number.txt | awk '{for(i=1;i<=NF;i++) print $i|"sort|uniq -u"}

1
5
6

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