如何在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 -f
或set -o noglob
或shopt -op noglob
或像*
这样的数组元素将被扩展为文件列表。
发生了什么:
结果是六个事情按照这个顺序发生:
-
IFS=$'\n'
-
"${array[*]}"
-
<<<
-
sort
-
sorted=($(...))
-
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_in
sorting并将结果存储在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 -f
,set +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
readarray
input(通过redirect到stdin,<
),因为read
/readarray
必须在当前 shell中运行(不能运行在一个子shell ),以便输出variablesa_out
对当前shell是可见的(对于variables在脚本的其余部分保持定义)。 -
将
sort
的输出读入一个数组variables :-
Bash v4 +:
readarray -t a_out
将sort
的各行输出读入数组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
描述
- 我们设置了两个局部variables
wa
(变通方法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或像*这样的数组元素将被扩展为文件列表。