如何在Bash中对数组进行sorting

我在Bash有一个数组,例如:

array=(acbf 3 5) 

我需要sorting数组。 不仅仅是以sorting的方式显示内容,而是使用sorting的元素来获取新的数组。 新的sorting数组可以是一个全新的或旧的。

你并不需要太多的代码:

 IFS=$'\n' sorted=($(sort <<<"${array[*]}")) unset IFS 

支持元素中的空白(只要它不是换行符), 在Bash 3.x中工作。

例如:

 $ array=("ac" bf "3 5") $ IFS=$'\n' sorted=($(sort <<<"${array[*]}")) $ printf "[%s]\n" "${sorted[@]}" [3 5] [ac] [b] [f] 

注意: @sorontar 指出 ,如果元素包含通配符(如*? ,则需要小心 :

sorted =($(…))部分正在使用“分割和水平”运算符。 您应该closuresglob: set -fset -o noglobshopt -op noglob或像*这样的数组元素将被扩展为文件列表。

发生了什么:

结果是六个事情按照这个顺序发生:

  1. IFS=$'\n'
  2. "${array[*]}"
  3. <<<
  4. sort
  5. sorted=($(...))
  6. unset IFS

首先, IFS=$'\n'

这是我们的操作的一个重要组成部分,以下列方式影响2和5的结果:

鉴于:

  • "${array[*]}"扩展到由IFS的第一个字符分隔的每个元素
  • sorted=()通过分割IFS每个字符来创build元素

IFS=$'\n' 设置事件,以便使用新行作为分隔符扩展元素,然后以每行成为元素的方式创build元素。 (即拆分新的一行)

用一条新线划界非常重要,因为这就是sort操作(每行分类)。 只用一个新行来拆分并不重要,但需要保留包含空格或制表符的元素。

IFS的默认值是一个空格一个制表符 ,后跟一个新行 ,并且不适合我们的操作。

接下来, sort <<<"${array[*]}"部分

<<<在这里称为string ,如上所述,扩展"${array[*]}" ,并将其input到sort的标准input中。

以我们的例子来说, sort是以下面的string:

 ac b f 3 5 

由于sort ,它会产生:

 3 5 ac b f 

接下来, sorted=($(...))部分

被称为命令replace$(...)部分导致其内容( sort <<<"${array[*]} )作为普通命令运行,同时将生成的标准输出作为文字$(...)是。

在我们的例子中,这产生了类似于简单写作的东西:

 sorted=(3 5 ac b f ) 

然后sorted成为一个数组,通过在每一个新行分割这个文字。

最后,未设定的unset IFS

这将重置IFS的值为默认值,这只是一个好的做法。

这是为了确保我们稍后在脚本中不会为依赖于IFS任何事情造成麻烦。 (否则,我们需要记住,我们已经切换了东西 – 对于复杂的脚本来说可能是不切实际的。)

原始回复:

 array=(acb "ff" 3 5) readarray -t sorted < <(for a in "${array[@]}"; do echo "$a"; done | sort) 

输出:

 $ for a in "${sorted[@]}"; do echo "$a"; done 3 5 a b c ff 

注意这个版本处理包含特殊字符或空格的值( 除了换行符)

注意 bash 4+支持readarray。


编辑基于@Dimitre的build议我已经更新到:

 readarray -t sorted < <(printf '%s\0' "${array[@]}" | sort -z | xargs -0n1) 

这有利于甚至理解正确embedded换行符的sorting元素。 不幸的是,正如@ruakh正确表示的那样,这并不意味着readarray的结果是正确的 ,因为readarray没有select使用NUL而不是常规换行符作为行分隔符。

这是一个纯粹的Bash quicksort实现:

 #!/bin/bash # quicksorts positional arguments # return is in array qsort_ret qsort() { local pivot i smaller=() larger=() qsort_ret=() (($#==0)) && return 0 pivot=$1 shift for i; do if [[ $i < $pivot ]]; then smaller+=( "$i" ) else larger+=( "$i" ) fi done qsort "${smaller[@]}" smaller=( "${qsort_ret[@]}" ) qsort "${larger[@]}" larger=( "${qsort_ret[@]}" ) qsort_ret=( "${smaller[@]}" "$pivot" "${larger[@]}" ) } 

作为,例如,

 $ array=(acbf 3 5) $ qsort "${array[@]}" $ declare -p qsort_ret declare -a qsort_ret='([0]="3" [1]="5" [2]="a" [3]="b" [4]="c" [5]="f")' 

这个实现是recursion的…所以这里是一个迭代的快速sorting:

 #!/bin/bash # quicksorts positional arguments # return is in array qsort_ret # Note: iterative, NOT recursive! :) qsort() { (($#==0)) && return 0 local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger qsort_ret=("$@") while ((${#stack[@]})); do beg=${stack[0]} end=${stack[1]} stack=( "${stack[@]:2}" ) smaller=() larger=() pivot=${qsort_ret[beg]} for ((i=beg+1;i<=end;++i)); do if [[ "${qsort_ret[i]}" < "$pivot" ]]; then smaller+=( "${qsort_ret[i]}" ) else larger+=( "${qsort_ret[i]}" ) fi done qsort_ret=( "${qsort_ret[@]:0:beg}" "${smaller[@]}" "$pivot" "${larger[@]}" "${qsort_ret[@]:end+1}" ) if ((${#smaller[@]}>=2)); then stack+=( "$beg" "$((beg+${#smaller[@]}-1))" ); fi if ((${#larger[@]}>=2)); then stack+=( "$((end-${#larger[@]}+1))" "$end" ); fi done } 

在这两种情况下,您都可以更改您使用的顺序:我使用了string比较,但是可以使用算术比较,比较wrt文件修改时间等。 你甚至可以使它更通用,并使用testing函数使用的第一个参数,例如,

 #!/bin/bash # quicksorts positional arguments # return is in array qsort_ret # Note: iterative, NOT recursive! :) # First argument is a function name that takes two arguments and compares them qsort() { (($#<=1)) && return 0 local compare_fun=$1 shift local stack=( 0 $(($#-1)) ) beg end i pivot smaller larger qsort_ret=("$@") while ((${#stack[@]})); do beg=${stack[0]} end=${stack[1]} stack=( "${stack[@]:2}" ) smaller=() larger=() pivot=${qsort_ret[beg]} for ((i=beg+1;i<=end;++i)); do if "$compare_fun" "${qsort_ret[i]}" "$pivot"; then smaller+=( "${qsort_ret[i]}" ) else larger+=( "${qsort_ret[i]}" ) fi done qsort_ret=( "${qsort_ret[@]:0:beg}" "${smaller[@]}" "$pivot" "${larger[@]}" "${qsort_ret[@]:end+1}" ) if ((${#smaller[@]}>=2)); then stack+=( "$beg" "$((beg+${#smaller[@]}-1))" ); fi if ((${#larger[@]}>=2)); then stack+=( "$((end-${#larger[@]}+1))" "$end" ); fi done } 

那么你可以有这个比较function:

 compare_mtime() { [[ $1 -nt $2 ]]; } 

并使用:

 $ qsort compare_mtime * $ declare -p qsort_ret 

使当前文件夹中的文件按修改时间sorting(最新的第一个)。

注意。 这些function是纯粹的Bash! 没有外部工具,也没有子壳! 他们是安全的任何有趣的符号,你可能有(空格,换行符,glob字符等)。

如果不需要在数组元素中处理特殊的shell字符:

 array=(acbf 3 5) sorted=($(printf '%s\n' "${array[@]}"|sort)) 

bash,你将需要一个外部sorting程序。

使用zsh不需要外部程序,特殊的shell字符很容易处理:

 % array=('aa' cbf 3 5); printf '%s\n' "${(o)array[@]}" 3 5 aa b c f 

ksh已经set -s了对ASCII码进行sorting。

在从慕尼黑到法兰克福的三个小时的火车之旅(我很难达成,因为明天慕尼黑啤酒节开始),我正在考虑我的第一个职位。 使用全局数组对于一般sortingfunction来说是一个更好的主意。 以下函数处理任意string(换行符,空格等):

 declare BSORT=() function bubble_sort() { # # @param [ARGUMENTS]... # # Sort all positional arguments and store them in global array BSORT. # Without arguments sort this array. Return the number of iterations made. # # Bubble sorting lets the heaviest element sink to the bottom. # (($# > 0)) && BSORT=("$@") local j=0 ubound=$((${#BSORT[*]} - 1)) while ((ubound > 0)) do local i=0 while ((i < ubound)) do if [ "${BSORT[$i]}" \> "${BSORT[$((i + 1))]}" ] then local t="${BSORT[$i]}" BSORT[$i]="${BSORT[$((i + 1))]}" BSORT[$((i + 1))]="$t" fi ((++i)) done ((++j)) ((--ubound)) done echo $j } bubble_sort acb 'zy' 3 5 echo ${BSORT[@]} 

这打印:

 3 5 abczy 

从相同的输出创build

 BSORT=(acb 'zy' 3 5) bubble_sort echo ${BSORT[@]} 

请注意,可能Bash内部使用智能指针,所以交换操作可能便宜(虽然我怀疑它)。 然而, bubble_sort表明像merge_sort这样的更高级的function也是shell语言所能达到的。

tl; dr

对数组a_insorting并将结果存储在a_out (元素不能embedded换行符[1] ):

Bash v4 +:

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

Bash v3:

 IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "${a_in[@]}" | sort) 

antak的解决scheme的优点:

  • 您不必担心意外的匹配(意外解释数组元素作为文件名模式),因此不需要额外的命令来禁用globbing( set -fset +f再恢复)。

  • 您不必担心重置IFS与未unset IFS[2]


可选阅读:说明和示例代码

上面结合了Bash代码和外部实用程序的sort可以使用任意的单行元素词法或数字sorting(可以select字段)

  • 性能 :对于大约20个或更多的元素 ,这将比纯粹的Bash解决scheme更快 – 一旦超过100个元素,就会显着增加。
    (确切的阈值将取决于您的具体input,机器和平台。)

    • 它的速度很快的原因是它避免了Bash循环
  • printf '%s\n' "${a_in[@]}" | sort printf '%s\n' "${a_in[@]}" | sort 执行sorting (词汇​​,默认情况下 – 请参阅sort的POSIX规范 ):

    • "${a_in[@]}"安全地扩展到数组a_in的元素作为单独的参数 ,无论它们包含什么(包括空格)。

    • printf '%s\n'然后按原样打印每个参数,即每个数组元素。

  • 注意使用进程replace( <(...)来提供sorting后的输出作为read / read readarrayinput(通过redirect到stdin, < ),因为read / readarray必须在当前 shell中运行(不能运行在一个子shell ),以便输出variablesa_out对当前shell是可见的(对于variables在脚本的其余部分保持定义)。

  • sort的输出读入一个数组variables

    • Bash v4 +: readarray -t a_outsort的各行输出读入数组variablesa_out的元素,而不在每个元素( -t )中包含尾部的\n

    • Bash v3: readarray不存在,所以必须使用read
      IFS=$'\n' read -d '' -r -a a_out告诉read数组( -a )variablesa_out ,读取整个input, a_out-d '' ),但是将其分割成数组元素通过换行符( IFS=$'\n'$'\n' ,它产生一个文字换行符(LF),是一个所谓的ANSI C引号string )。
      -r ,这个选项实际上应该总是和read一起使用,禁止意外处理\ characters。)

注释示例代码:

 #!/usr/bin/env bash # Define input array `a_in`: # Note the element with embedded whitespace ('a c')and the element that looks like # a glob ('*'), chosen to demonstrate that elements with line-internal whitespace # and glob-like contents are correctly preserved. a_in=( 'ac' bf 5 '*' 10 ) # Sort and store output in array `a_out` # Saving back into `a_in` is also an option. IFS=$'\n' read -d '' -r -a a_out < <(printf '%s\n' "${a_in[@]}" | sort) # Bash 4.x: use the simpler `readarray -t`: # readarray -t a_out < <(printf '%s\n' "${a_in[@]}" | sort) # Print sorted output array, line by line: printf '%s\n' "${a_out[@]}" 

由于使用没有选项的sort ,这会产生词法分类(数字在字母之前sorting,数字序列在词汇上而不是数字):

 * 10 5 ac b f 

如果你想通过第一个字段进行数字sorting,你可以使用sort -k1,1n而不是sort ,这会产生(非数字在数字之前sorting,数字sorting正确):

 * ac b f 5 10 

[1]要处理embedded换行符的元素,请使用以下变体(Bash v4 +,使用GNU sort ):
readarray -d '' -t a_out < <(printf '%s\0' "${a_in[@]}" | sort -z)
MichałGórny的有用答案有一个Bash v3解决scheme。

[2]虽然IFS 设置在Bash v3版本中,但更改的范围仅限于命令
相比之下, IFS=$'\n'在antak的答案中是一个赋值而不是命令,在这种情况下, IFS变化是全局的

尝试这个:

 echo ${array[@]} | awk 'BEGIN{RS=" ";} {print $1}' | sort 

输出将是:

 3
五
一个
 b
 C
 F

问题解决了。

另一个解决scheme,使用外部sort和处理任何特殊字符(NULs :)除外)。 应该使用bash-3.2和GNU或BSD进行sort (可悲的是,POSIX不包括-z )。

 local e new_array=() while IFS= read -r -d '' e; do new_array+=( "${e}" ) done < <(printf "%s\0" "${array[@]}" | LC_ALL=C sort -z) 

首先看看最后的inputredirect。 我们使用内置的printf来写出数组元素,零终止。 引用确保数组元素是按原样传递的,并且shell printf具体情况会导致它为每个剩余的参数重用格式string的最后部分。 也就是说,它相当于这样的东西:

 for e in "${array[@]}"; do printf "%s\0" "${e}" done 

然后传递null-terminated元素列表进行sort-z选项使它读取以null结尾的元素,对它们进行sorting并输出以null结尾的消息。 如果您只需要获取唯一元素,则可以传递-u因为它比uniq -z更便于携带。 LC_ALL=C可以确保稳定的sorting顺序而不受语言环境的影响 – 有时对脚本很有用。 如果你想sort尊重语言环境,删除。

<()构造获得从派生pipe道中读取的描述符, <while循环的标准inputredirect到它。 如果您需要访问pipe道中的标准input,则可以使用另一个描述符 – 练习读者:)。

现在回到开始。 read内置读取输出从redirect的stdin。 设置空的IFS会禁用这里不必要的分词,因此read会将input的整个“行”读取到单个提供的variables中。 -r选项禁止在此处不希望的转义处理。 最后, -d ''将行分隔符设置为NUL – 也就是说, read读取零终止的string。

结果,对于每个连续的以零终止的数组元素,循环被执行一次,其值被存储在e 。 该示例只是将项目放在另一个数组中,但是您可能更愿意直接处理它们:)。

当然,这只是实现同一目标的众多方法之一。 正如我所看到的,它比在bash中实现完整的sortingalgorithm更简单,在某些情况下会更快。 它处理所有的特殊字符,包括换行符,并应该在大多数通用系统上工作。 最重要的是,它可能教你一些新的和令人敬畏的bash :)。

 array=(acbf 3 5) new_array=($(echo "${array[@]}" | sed 's/ /\n/g' | sort)) echo ${new_array[@]} 

new_array的echo内容将是:

 3 5 abcf 

如果可以为数组中的每个元素计算唯一的整数,如下所示:

 tab='0123456789abcdefghijklmnopqrstuvwxyz' # build the reversed ordinal map for ((i = 0; i < ${#tab}; i++)); do declare -g ord_${tab:i:1}=$i done function sexy_int() { local sum=0 local i ch ref for ((i = 0; i < ${#1}; i++)); do ch="${1:i:1}" ref="ord_$ch" (( sum += ${!ref} )) done return $sum } sexy_int hello echo "hello -> $?" sexy_int world echo "world -> $?" 

那么你可以使用这些整数作为数组索引,因为Bash总是使用稀疏数组,所以不用担心没有使用的索引:

 array=(acbf 3 5) for el in "${array[@]}"; do sexy_int "$el" sorted[$?]="$el" done echo "${sorted[@]}" 
  • 优点。 快速。
  • 缺点。 重复的元素被合并,并且将内容映射到32位唯一整数是不可能的。

我不相信你会在Bash中需要一个外部的sorting程序。

这里是我简单的气泡sortingalgorithm的实现。

 function bubble_sort() { # # Sorts all positional arguments and echoes them back. # # Bubble sorting lets the heaviest (longest) element sink to the bottom. # local array=($@) max=$(($# - 1)) while ((max > 0)) do local i=0 while ((i < max)) do if [ ${array[$i]} \> ${array[$((i + 1))]} ] then local t=${array[$i]} array[$i]=${array[$((i + 1))]} array[$((i + 1))]=$t fi ((i += 1)) done ((max -= 1)) done echo ${array[@]} } array=(acbf 3 5) echo " input: ${array[@]}" echo "output: $(bubble_sort ${array[@]})" 

这应该打印:

  input: acbf 3 5 output: 3 5 abcf 
 a=(eb 'c d') shuf -e "${a[@]}" | sort >/tmp/f mapfile -tg </tmp/f 

对于空格和换行符的常见问题有一个解决方法:

使用不在原始数组中的字符(如$'\1'$'\4'或类似字符)。

这个function可以完成这项工作:

 # Sort an Array may have spaces or newlines with a workaround (wa=$'\4') sortarray(){ local wa=$'\4' IFS='' if [[ $* =~ [$wa] ]]; then echo "$0: error: array contains the workaround char" >&2 exit 1 fi set -f; local IFS=$'\n' x nl=$'\n' set -- $(printf '%s\n' "${@//$nl/$wa}" | sort -n) for x do sorted+=("${x//$wa/$nl}") done } 

这将sorting数组:

 $ array=( ab 'cd' $'e\nf' $'g\1h') $ sortarray "${array[@]}" $ printf '<%s>\n' "${sorted[@]}" <a> <b> <c d> <e f> <gh> 

这会抱怨源数组包含解决方法字符:

 $ array=( ab 'cd' $'e\nf' $'g\4h') $ sortarray "${array[@]}" ./script: error: array contains the workaround char 

描述

  • 我们设置了两个局部variableswa (变通方法char)和一个空IFS
  • 然后(用ifs null)我们testing整个数组$*
  • 不包含任何woraround char [[ $* =~ [$wa] ]]
  • 如果确实如此,则发出消息并发出错误信息: exit 1
  • 避免文件名扩展: set -f
  • 设置IFS的新值( IFS=$'\n' )循环variablesx和换行符var( nl=$'\n' )。
  • 我们打印所有收到的参数值(input数组$@ )。
  • 但是我们用替代字符"${@//$nl/$wa}"replace任何新行。
  • 发送这些值进行sort -n
  • 并将位置参数set --所有sorting值放回。
  • 然后我们逐一分配每个参数(保留换行符)。
  • for x的循环for x
  • 到一个新的数组: sorted+=(…)
  • 里面的引号来保存任何现有的换行符。
  • 将解决方法恢复为换行符"${x//$wa/$nl}"
  • DONE

sorted=($(echo ${array[@]} | tr " " "\n" | sort))

本着bash / linux的精神,我会为每个步骤提供最好的命令行工具。 sort做的主要工作,但需要由换行而不是空间分隔的input,所以上面非常简单的pipe道只是做:

回声数组内容 – >用换行符replace空格 – >sorting

$()是回显结果

($())是把“回显结果”放在一个数组中

注意 :正如@sorontar在一个不同的问题的评论中提到的:

sorted =($(…))部分正在使用“分割和水平”运算符。 您应该closuresglob:set -f或set -o noglob或shopt -op noglob或像*这样的数组元素将被扩展为文件列表。