如何在Bash中对数组进行排序

u32*_*004 123 arrays sorting bash shell

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

array=(a c b f 3 5)
Run Code Online (Sandbox Code Playgroud)

我需要对数组进行排序.不只是以排序的方式显示内容,而是获取带有已排序元素的新数组.新排序的数组可以是全新的也可以是旧的.

ant*_*tak 176

你真的不需要那么多代码:

IFS=$'\n' sorted=($(sort <<<"${array[*]}"))
unset IFS
Run Code Online (Sandbox Code Playgroud)

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

例如:

$ array=("a c" b f "3 5")
$ IFS=$'\n' sorted=($(sort <<<"${array[*]}")); unset IFS
$ printf "[%s]\n" "${sorted[@]}"
[3 5]
[a c]
[b]
[f]
Run Code Online (Sandbox Code Playgroud)

注意: @sorontar 指出,如果元素包含通配符,例如*?:

sorted =($(...))部分使用"split and glob"运算符.你应该关掉glob:set -for set -o noglob或者shopt -op noglob数组的元素就像*将扩展为文件列表一样.

发生了什么:

结果是按此顺序发生的六件事:

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

首先, IFS=$'\n'

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

鉴于:

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

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

通过新线划分非常重要,因为这就是sort操作方式(每行排序).按新行拆分并不重要,但需要保留包含空格或制表符的元素.

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

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

<<<在这里称为字符串,"${array[*]}"如上所述,进行扩展,并将其输入到标准输入中sort.

在我们的示例中,sort输入以下字符串:

a c
b
f
3 5
Run Code Online (Sandbox Code Playgroud)

自从sort 分类以来,它产生:

3 5
a c
b
f
Run Code Online (Sandbox Code Playgroud)

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

这个$(...)名为命令替换的部分使其content(sort <<<"${array[*]})作为普通命令运行,同时将得到的 标准输出作为文字随处可见$(...).

在我们的示例中,这会产生类似于简单编写的内容:

sorted=(3 5
a c
b
f
)
Run Code Online (Sandbox Code Playgroud)

sorted 然后成为通过在每个新行上拆分此文字而创建的数组.

最后, unset IFS

这会将值重置为IFS默认值,这只是一种很好的做法.

这是为了确保我们不会在IFS以后的脚本中依赖任何东西.(否则我们需要记住,我们已经切换了一些东西 - 对于复杂的脚本来说可能是不切实际的.)

  • @MarkH这是必要的,因为`sorted =()`不是命令,而是第二个变量赋值. (10认同)
  • 是否需要"未设置IFS"?我认为将`IFS =`添加到命令范围仅限于对该命令的更改,然后自动返回其先前的值. (4认同)
  • 非常好.你可以向普通的bash用户解释这个解决方案的工作原理吗? (3认同)
  • 没有'IFS`的@xxor,如果它们中有空格,它会将你的元素分成小块.尝试*eg*with`IFS = $'\n'`省略,看看! (2认同)
  • 现在,使用`IFS`,如果元素中只有一种特殊的空白,它会将元素分成小块.好; 不完美 :-) (2认同)
  • 顺便说一句,如果我们在GNU系统上(或者在其他地方使用`sort -z`),那么NUL分隔条目会更安全 - 这样我们就不会将带有换行文字的条目分成多个条目新创建的数组.`out =(); 而IFS =读-r -d''条目; 做出+ =("$ entry"); 完成<<(printf'%s\0'"$ {array [@]}"| sort -z)` (2认同)

seh*_*ehe 35

原始回复:

array=(a c b "f f" 3 5)
readarray -t sorted < <(for a in "${array[@]}"; do echo "$a"; done | sort)
Run Code Online (Sandbox Code Playgroud)

输出:

$ for a in "${sorted[@]}"; do echo "$a"; done
3
5
a
b
c
f f
Run Code Online (Sandbox Code Playgroud)

请注意,此版本处理包含特殊字符或空格的值(换行符除外)

注意 bash 4+支持readarray.


编辑根据@Dimitre的建议,我已将其更新为:

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

这有利于甚至理解正确嵌入换行符的排序元素.不幸的是,正如@ruakh正确发出信号这并不意味着结果readarray正确的,因为readarray没有选择使用NUL而不是常规换行作为行分隔符.

  • 很好,还应该注意到readarray从bash版本4开始可用.它可以缩短一点:`readarray -t sorted <<(printf'%s \n'"$ {array [@]}"| sort)` (5认同)
  • 如果您想处理嵌入式换行,您可以自己进行读取.例如:`sorted =(); 读取-d $'\ 0'elem; 排序[$ {#sorted [@]}] = $ elem; 完成<<(printf'%s\0'"$ {array [@]}"| sort -z)`.这也适用于使用bash v3而不是bash v4,因为bash v3中没有readarray. (2认同)
  • 启动 bash 4.4 并编写 `help readarray` (mapfile),发现它接受一个 `-d` 选项来选择 `delimiter`。否则,[阅读此处](http://wiki.bash-hackers.org/scripting/bashchanges);`“mapfile”新选项“-d”4.4-alpha`。也在[这个答案的末尾](http://stackoverflow.com/a/24426608/6843677)。 (2认同)

gni*_*urf 34

这是一个纯粹的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[@]}" )
}
Run Code Online (Sandbox Code Playgroud)

用作例如,

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

这个实现是递归的......所以这里是一个迭代的快速排序:

#!/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
}
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,您都可以更改您使用的顺序:我使用字符串比较,但您可以使用算术比较,比较wrt文件修改时间等,只需使用相应的测试; 你甚至可以使它更通用,并让它使用第一个参数,即测试函数使用,例如,

#!/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
}
Run Code Online (Sandbox Code Playgroud)

然后你可以有这个比较功能:

compare_mtime() { [[ $1 -nt $2 ]]; }
Run Code Online (Sandbox Code Playgroud)

并使用:

$ qsort compare_mtime *
$ declare -p qsort_ret
Run Code Online (Sandbox Code Playgroud)

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

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


Dim*_*lov 27

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

array=(a c b f 3 5)
sorted=($(printf '%s\n' "${array[@]}"|sort))
Run Code Online (Sandbox Code Playgroud)

使用bash,无论如何你都需要一个外部排序程序.

使用zsh不需要外部程序,并且可以轻松处理特殊的shell字符:

% array=('a a' c b f 3 5); printf '%s\n' "${(o)array[@]}" 
3
5
a a
b
c
f
Run Code Online (Sandbox Code Playgroud)

ksh必须set -sASCII排序.


mkl*_*nt0 9

tl;博士:

对数组进行排序a_in并将结果存储a_out(元素不得嵌入换行符[1] ):

Bash v4 +:

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

Bash v3:

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

antak解决方案相比的优势:

  • 您不必担心意外的globbing(将数组元素意外解释为文件名模式),因此不需要额外的命令来禁用globbing(set -fset +f在以后恢复它).

  • 你不必担心重新IFS使用unset IFS.[2]


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

上面将Bash代码与外部实用程序结合使用,sort以获得适用于任意单行元素以及词法或数字排序(可选地按字段)的解决方案:

  • 性能:对于大约20个或更多元素,这将比纯Bash解决方案更快 - 一旦超过大约100个元素,显着且越来越多.
    (确切的阈值取决于您的具体输入,机器和平台.)

    • 它快速的原因是它避免了Bash循环.
  • printf '%s\n' "${a_in[@]}" | sort 执行排序(词法上,默认情况下 - 请参阅sortPOSIX规范):

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

    • printf '%s\n' 然后按原样打印每个参数 - 即每个数组元素 - 在它自己的行上.

  • 注意使用进程substitution(<(...))来提供排序输出作为read/的输入readarray(通过重定向到stdin <),因为read/ readarray必须在当前 shell中运行(不能在子shell中运行),以便输出变量a_out可见到当前shell(使变量保持在脚本的其余部分中定义).

  • sort输出读入数组变量:

    • Bash v4 +:readarray -t a_out将输出的各行读sort入数组变量的元素a_out,而不包括\n每个元素中的尾部(-t).

    • Bash v3:readarray不存在,所以read必须使用:
      IFS=$'\n' read -d '' -r -a a_outtell readto read into array(-a)变量a_out,读取整个输入,跨行(-d ''),但是通过换行符将它拆分为数组元素(IFS=$'\n'.$'\n',生成一个文字换行符(LF) ),是所谓的ANSI C引用字符串).
      (-r,实际上应该与之一起使用的选项read,禁用对\字符的意外处理.)

带注释的示例代码:

#!/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=( 'a c' b f 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[@]}"
Run Code Online (Sandbox Code Playgroud)

由于使用了sort无选项,这会产生词法排序(字母前的数字排序,数字序列在词汇上处理,而不是数字):

*
10
5
a c
b
f
Run Code Online (Sandbox Code Playgroud)

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

*
a c
b
f
5
10
Run Code Online (Sandbox Code Playgroud)

[1]为了处理元件具有嵌入换行符,使用下列变型(击V4 +,与GNU sort):
readarray -d '' -t a_out < <(printf '%s\0' "${a_in[@]}" | sort -z).
MichałGórny的有用答案有一个Bash v3解决方案.

[2]当IFS 在Bash V3变体设置,所述变化是作用域到命令.
相比之下,IFS=$'\n' 在antak的回答中接下来的是赋值而不是命令,在这种情况下,IFS变化是全局的.


And*_*ler 8

在从慕尼黑到法兰克福的3小时火车旅行中(由于慕尼黑啤酒节明天开始,我很难到达)我正在考虑我的第一篇文章.对于一般排序函数,使用全局数组是一个更好的想法.以下函数处理任意字符串(换行符,空格等):

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 a c b 'z y' 3 5
echo ${BSORT[@]}
Run Code Online (Sandbox Code Playgroud)

这打印:

3 5 a b c z y
Run Code Online (Sandbox Code Playgroud)

从中创建相同的输出

BSORT=(a c b 'z y' 3 5) 
bubble_sort
echo ${BSORT[@]}
Run Code Online (Sandbox Code Playgroud)

请注意,Bash内部可能使用智能指针,因此交换操作可能很便宜(尽管我对此表示怀疑).但是,bubble_sort证明更多高级函数merge_sort也在shell语言的范围内.

  • 泡泡排序?哇哦奥巴马说"泡泡排序将是错误的方式" - > http://www.youtube.com/watch?v=k4RRi_ntQc8 (5认同)

Mic*_*rny 7

另一个使用外部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)
Run Code Online (Sandbox Code Playgroud)

首先看一下最后的输入重定向.我们使用printf内置来写出数组元素,零终止.引用确保数组元素按原样传递,并且shell的细节printf使其为每个剩余参数重用格式字符串的最后部分.也就是说,它等同于:

for e in "${array[@]}"; do
    printf "%s\0" "${e}"
done
Run Code Online (Sandbox Code Playgroud)

然后传递以null结尾的元素列表sort.该-z选项使它读取以null结尾的元素,对它们进行排序并输出以null结尾的元素.如果你只需要获得独特的元素,你可以通过,-u因为它比它更便携uniq -z.该LC_ALL=C独立保证稳定的排序顺序的语言环境-有时脚本很有用.如果您想要sort尊重语言环境,请删除它.

<()构造获得要从生成的管道读取的描述符,并将循环<的标准输入重定向while到它.如果您需要访问管道内的标准输入,您可以使用另一个描述符 - 阅读器练习:).

现在,回到开头.该read内置读取重定向标准输入输出.设置为空IFS将禁用在此处不必要的分词 - 因此,read将输入的整个"行"读取到单个提供的变量.-r选项也禁用此处不需要的转义处理.最后,-d ''将行分隔符设置为NUL - 即告诉read读取以零结尾的字符串.

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

当然,这只是实现同一目标的众多方式之一.在我看来,它比在bash中实现完整的排序算法更简单,在某些情况下它会更快.它处理所有特殊字符,包括换行符,并且应该适用于大多数常见系统.最重要的是,它可能会教你一些关于bash的新东西和令人敬畏的东西:).