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

我有一个和 给你几乎相同的问题。

I have an array which contains aa ab aa ac aa ad, etc. 现在我要从这个数组中选择所有唯一的元素。 Thought, this would be simple with sort | uniq or with sort -u as they mentioned in that other question, but nothing changed in the array... The code is:

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

我做错了什么?

123125 次浏览

有点俗气,不过这个应该可以:

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

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

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

If your shell supports 字符串 (bash should), you can spare an echo process by altering it to:

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

截至2021年8月28日的注释:

根据 ShellCheck 维基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的一部分使用还是作为附加字符串使用。@部分表示“数组中的所有元素”
  • tr ' ' '\n'-将所有空格转换为换行符。因为 shell 将数组视为单行上的元素,用空格分隔; 还因为 sort 希望输入在单行上。
  • 排序并只保留唯一的元素
  • tr '\n' ' ' - convert the newlines we added in earlier back to spaces.
  • $(...)-指令替代
  • 旁白: tr ' ' '\n' <<< "${ids[@]}"是一种更有效的方法: echo "${ids[@]}" | tr ' ' '\n'

如果你运行的是 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 等) ,性能可能会更好。

如果你有信心,你可以避免 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,是因为数组值是在拆字之前确定的。这意味着指令替代的输出被认为是 一个字,而不是一组键 = 值对。

虽然它使用子 shell,但它只使用 bash 内置函数来处理数组值。一定要用批判的眼光来评估你对 eval的使用。如果您不能100% 确信 Chepner 或 Glenn Jackman 或 greycat 不会发现您的代码有任何错误,那么可以使用 for 循环。

我知道这个问题已经有答案了,但是在搜索结果中出现的频率很高,可能会对某些人有所帮助。

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
~>

如果您的数组元素有空格或任何其他 shell 特殊字符(您能确定它们没有空格吗?)然后首先捕获这些(你应该总是这样做)用双引号表示你的数组!例如 "${a[@]}"。Bash 会逐字地将其解释为“单独的 争论中的每个数组元素”。在 bash 中,这种方法总是有效的,总是有效的。

然后,为了得到一个排序的(和唯一的)数组,我们必须将它转换为一个格式排序理解,并能够将它转换回 bash 数组元素。这是我想到的最好的办法:

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

Unfortunately, this fails in the special case of the empty array, turning the empty array into an array of 1 empty element (because printf had 0 arguments but still prints as though it had one empty argument - see explanation). So you have to catch that in an if or something.

Explanation: The %q format for printf "shell escapes" the printed argument, in just such a way as bash can recover in something like eval! 因为每个元素都在自己的行上打印了 shell 转义,所以元素之间的唯一分隔符是换行符,数组赋值将每一行作为一个元素,将转义值解析为文本。

e.g.

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

要将返回到数组中的每个值从转义中去除,就必须使用 eval。

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

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

这也将维持秩序:

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

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

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

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

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

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’可用于对 for 循环的输出进行排序:

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

and eliminate duplicates with "-u":

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

Finally you can just overwrite your array with the unique elements:

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

猫的编号

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

尝试这样做可以获得文件中第一列的 uniq 值

awk -F, '{a[$1];}END{for (i in a)print i;}'

如果你想要一个只使用 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

This will output

bar
foo
bar none
# 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

处理嵌入式空格的另一种选择是使用 printf进行 null 分隔,使用 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'

这个变化怎么样?

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

所有以下工作在 bashsh,在 shellcheck没有错误,但你需要抑制 SC2207

arrOrig=("192.168.3.4" "192.168.3.4" "192.168.3.3")


# NO SORTING
# shellcheck disable=SC2207
arr1=($(tr ' ' '\n' <<<"${arrOrig[@]}" | awk '!u[$0]++' | tr '\n' ' ')) # @estani
len1=${#arr1[@]}
echo "${len1}"
echo "${arr1[*]}"


# SORTING
# shellcheck disable=SC2207
arr2=($(printf '%s\n' "${arrOrig[@]}" | sort -u)) # @das.cyklone
len2=${#arr2[@]}
echo "${len2}"
echo "${arr2[*]}"


# SORTING
# shellcheck disable=SC2207
arr3=($(echo "${arrOrig[@]}" | tr ' ' '\n' | sort -u | tr '\n' ' ')) # @sampson-chen
len3=${#arr3[@]}
echo "${len3}"
echo "${arr3[*]}"


# SORTING
# shellcheck disable=SC2207
arr4=($(for i in "${arrOrig[@]}"; do echo "${i}"; done | sort -u)) # @corbyn42
len4=${#arr4[@]}
echo "${len4}"
echo "${arr4[*]}"


# NO SORTING
# shellcheck disable=SC2207
arr5=($(echo "${arrOrig[@]}" | tr "[:space:]" '\n' | awk '!a[$0]++')) # @faustus
len5=${#arr5[@]}
echo "${len5}"
echo "${arr5[*]}"


# OUTPUTS


# arr1
2 # length
192.168.3.4 192.168.3.3 # items


# arr2
2 # length
192.168.3.3 192.168.3.4 # items


# arr3
2 # length
192.168.3.3 192.168.3.4 # items


# arr4
2 # length
192.168.3.3 192.168.3.4 # items


# arr5
2 # length
192.168.3.4 192.168.3.3 # items

所有这些的输出为2并且正确。这个答案基本上总结和整理了本文中的其他答案,是一个有用的快速参考。给出了原答案的归属。

In zsh you can use (u) flag:

$ ids=(aa ab aa ac aa ad)
$ print ${(u)ids}
aa ab ac ad