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


93

我和这里的问题差不多。

我有一个包含aa ab aa ac aa ad等的数组。现在,我想从该数组中选择所有唯一元素。思想,这将是简单的用sort | uniqsort -u因为他们在其他问题中提到,但没有在数组中改变...的代码是:

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

我究竟做错了什么?

Answers:


131

有点hacky,但是应该这样做:

echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '

要将排序后的唯一结果保存回数组中,请执行数组分配

sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))

如果您的外壳支持herestringsbash应该),则可以echo通过将其更改为以下内容来节省进程:

tr ' ' '\n' <<< "${ids[@]}" | sort -u | tr '\n' ' '

输入:

ids=(aa ab aa ac aa ad)

输出:

aa ab ac ad

说明:

  • "${ids[@]}"-使用shell数组的语法,无论是作为一部分echo还是在此处使用。该@部分的意思是“数组中的所有元素”
  • tr ' ' '\n'-将所有空格转换为换行符。因为您的数组被shell视为一行上的元素,并用空格分隔;并且因为sort期望输入在单独的行上。
  • sort -u -仅排序和保留唯一元素
  • tr '\n' ' ' -将我们先前添加的换行符转换回空格。
  • $(...)-命令替换
  • 除了:tr ' ' '\n' <<< "${ids[@]}"是一种更有效的方法:echo "${ids[@]}" | tr ' ' '\n'

37
+1。稍微uniq=($(printf "%s\n" "${ids[@]}" | sort -u)); echo "${uniq[@]}"
讲究

@glennjackman哦,整洁!我什至没有意识到您可以使用printf这种方式(给出比格式字符串更多的参数)
sampson-chen 2012年

4
+1我不确定这是否是一个孤立的情况,但是将唯一项放回数组中需要附加括号,例如:sorted_unique_ids=($(echo "${ids[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' '))。没有额外的括号,它以字符串形式给出。
2014年

3
如果您不想更改元素的顺序,请使用... | uniq | ...代替... | sort -u | ...
杰西·奇斯霍尔姆

2
@Jesse,uniq仅删除连续的重复项。在此答案的示例中,sorted_unique_ids最终将与原始相同ids。要保留顺序,请尝试... | awk '!seen[$0]++'。另请参见stackoverflow.com/questions/1444406/…
罗伯·肯尼迪

29

如果您运行的是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循环到达aain的第二个值时a[2],它将覆盖b[aa]最初为设置的值a[0]

与使用管道和外部工具(例如sort和)相比,使用本机bash进行处理的速度可能更快uniq,但是对于较大的数据集,如果使用诸如awk,python等更强大的语言,则可能会看到更好的性能。

如果您有信心,可以for使用printf的能力为多个参数回收其格式,从而避免循环,尽管这似乎是必需的eval。(如果您满意,请立即停止阅读。)

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

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

尽管这使用了子外壳程序,但它仅使用bash内置函数来处理数组值。一定要用eval肉眼评估您的使用情况。如果您不是100%相信chepner或glenn jackman或greycat不会发现您的代码有问题,请改用for循环。


产生错误:表达式递归级别超出
Benubird 2014年

1
@Benubird-也许您可以粘贴终端内容?它非常适合我,因此我最好的猜测是,您有(1)错字,(2)旧版本的bash(向v4添加了关联数组),或(3)大量的宇宙背景涌入邻居地下室中的量子黑洞引起的辐射,对计算机内的信号产生干扰。
ghoti 2014年

1
不能,没有保留无效的那个。但是,我刚刚尝试运行您的设备,并且它起作用了,所以大概是宇宙辐射的事情了。
Benubird 2014年

猜测此答案使用了bash v4(关联数组),如果有人尝试使用bash v3,它将无法正常工作(@Benubird看到的可能不行)。Bash v3在许多环境中仍然是默认设置
nhed 2015年

1
@nhed,要点。我看到我的最新优胜美地Macbook在基础上具有相同版本,尽管我已经从macports安装了v4。这个问题被标记为“ linux”,但是我已经更新了答案以指出要求。
ghoti 2015年

18

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

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值!或者它搅乱其他东西..
水瓶座电源

@Jetse这应该是公认的答案,因为它仅使用两个命令,没有循环,没有评估,并且是最紧凑的版本。
mgutt

1
@AquariusPower小心,您基本上在做:IFS=$'\n'; ids2=(...),因为不可能在变量分配之前进行临时分配。而是使用以下构造:IFS=$'\n' read -r -a ids2 <<<"$(printf "%s\n" "${ids[@]}" | sort -u)"
雪人

13

如果您的数组元素有空格或任何其他shell特殊字符(您可以确定它们没有吗?),则首先要捕获这些字符(并且您应该始终这样做)用双引号将数组表示出来!例如"${a[@]}"。Bash从字面上将其解释为“单独参数中的每个数组元素”。在bash中,这始终会始终有效。

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

eval a=($(printf "%q\n" "${a[@]}" | sort -u))

不幸的是,在空数组的特殊情况下,这失败了,将空数组变成了一个包含1个空元素的数组(因为printf的参数为0,但仍然打印时好像它的参数为一个空参数-参见说明)。因此,您必须抓住它,如果有的话。

说明:printf的%q格式“ shell”转义了打印的参数,就像bash可以以eval之类的方式恢复一样!由于每个元素都是在自己的行上进行转义的脱壳shell,因此元素之间的唯一分隔符是换行符,并且数组赋值将每行作为元素,将转义的值解析为文字文本。

例如

> a=("foo bar" baz)
> printf "%q\n" "${a[@]}"
'foo bar'
baz
> printf "%q\n"
''

eval对于删除转回数组的每个值是必需的。


这是唯一对我有用的代码,因为我的字符串数组中有空格。%q是解决问题的办法。谢谢:)
Somaiah Kumbera

如果您不想更改元素的顺序,请使用uniq代替sort -u
Jesse Chisholm

请注意,uniq这在未排序的列表上无法正常工作,因此必须始终与结合使用sort
让·保罗

未排序列表上的uniq将删除连续的重复项。不会删除由其他之间分隔的相同列表元素。uniq可能足够有用,这取决于预期的数据和维持原始顺序的需求。
冯特拉普

10

'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

3

这也将保留顺序:

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

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

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

不要使用uniq。它需要排序,而awk不需要排序,此答案的目的是在输入未排序时保留排序。
bukzor

2

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

删除重复的条目(进行排序)

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

@JesseChisholmuniq仅合并相邻的重复行,因此与相同awk '!x[$0]++'

@JesseChisholm请删除误导性评论。
布克佐

2

猫编号.txt

1 2 3 4 4 3 2 5 6

将行打印到列中: 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

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

1
5
6

1

在不失去原始订购的情况下:

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

1

如果要使用仅使用bash内部函数的解决方案,则可以将值设置为关联数组中的键,然后提取键:

declare -A uniqs
list=(foo bar bar "bar none")
for f in "${list[@]}"; do 
  uniqs["${f}"]=""
done

for thing in "${!uniqs[@]}"; do
  echo "${thing}"
done

这将输出

bar
foo
bar none

我只是注意到这基本上与上面的@ghotis答案相同,除了他的解决方案不考虑带有空格的列表项。
rln

好点子。我在解决方案中添加了引号,因此它现在可以处理空格。我最初写它只是为了处理问题中的样本数据,但是涵盖这样的偶发事件总是很好的。谢谢你的建议。
ghoti

1

处理嵌入式空白的另一种方法是用printf,,进行分隔sort,然后使用循环将其包装回数组中:

input=(a b c "$(printf "d\ne")" b c "$(printf "d\ne")")
output=()

while read -rd $'' element
do 
  output+=("$element")
done < <(printf "%s\0" "${input[@]}" | sort -uz)

最后,inputoutput包含所需的值(提供的顺序并不重要):

$ printf "%q\n" "${input[@]}"
a
b
c
$'d\ne'
b
c
$'d\ne'

$ printf "%q\n" "${output[@]}"
a
b
c
$'d\ne'

1

这种变化如何?

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

然后sorted_arr=($(printf '%s\n' "${ids[@]}" | sort -u)
藻类


-3
# Read a file into variable
lines=$(cat /path/to/my/file)

# Go through each line the file put in the variable, and assign it a variable called $line
for line in $lines; do
  # Print the line
  echo $line
# End the loop, then sort it (add -u to have unique lines)
done | sort -u
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.