Answers:
您实际上并不需要那么多代码:
IFS=$'\n' sorted=($(sort <<<"${array[*]}"))
unset IFS
在元素中支持空格(只要它不是换行符),并在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]
注: @sorontar已经指出,如果元素包含通配符,如需要小心*
或?
:
sorted =($(...))部分正在使用“ split and glob”运算符。您应该关闭glob:
set -f
或set -o noglob
或shopt -op noglob
或数组中的一个元素,如*
将会扩展到文件列表。
结果是按顺序发生了六件事:
IFS=$'\n'
"${array[*]}"
<<<
sort
sorted=($(...))
unset IFS
IFS=$'\n'
这是我们操作的重要组成部分,它通过以下方式影响2和5的结果:
鉴于:
"${array[*]}"
扩展到以的第一个字符分隔的每个元素 IFS
sorted=()
通过分割的每个字符来创建元素 IFS
IFS=$'\n'
进行设置,以便使用新行作为分隔符扩展元素,然后以每行成为元素的方式创建元素。(即在新行上拆分。)
用新行定界很重要,因为这就是sort
操作方式(按行排序)。仅用换行分隔不是很重要,但是需要保留包含空格或制表符的元素。
的默认值IFS
是space,tab,后跟换行,并且不适合我们的操作。
sort <<<"${array[*]}"
部分<<<
,在这里称为字符串,接受的扩展"${array[*]}"
,如上所述,并将其输入的标准输入sort
。
在我们的示例中,sort
以下字符串被馈入:
a c
b
f
3 5
由于sort
sort,它产生:
3 5
a c
b
f
sorted=($(...))
部分该$(...)
部分称为命令替换,使它的内容(sort <<<"${array[*]}
)作为普通命令运行,同时将所得的 标准输出作为到达原处的文字$(...)
。
在我们的示例中,这类似于简单地编写:
sorted=(3 5
a c
b
f
)
sorted
然后变成通过在每个新行上拆分此文字而创建的数组。
unset IFS
这会将的值重置为IFS
默认值,这只是一个好习惯。
这是为了确保我们不会IFS
对脚本中以后依赖的任何内容造成麻烦。(否则,我们需要记住,我们已经改变了一切,这对于复杂的脚本来说可能是不切实际的。)
IFS
,如果元素中只有一种特定类型的空白,它将把您的元素分成小块。好; 不完美:-)
unset IFS
必要吗?我认为IFS=
在执行命令之前,仅将更改范围限定在该命令上,然后自动返回到其先前的值。
sorted=()
它不是命令,而是第二个变量分配。
原始回复:
array=(a c b "f f" 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
f f
请注意,此版本处理包含特殊字符或空格(换行符除外)的值
注意 bash 4+支持readarray。
编辑根据建议通过@Dimitre我有它更新为:
readarray -t sorted < <(printf '%s\0' "${array[@]}" | sort -z | xargs -0n1)
这样做的好处是甚至可以理解带有正确嵌入换行符的排序元素。不幸的是,正如@ruakh正确表示的,这并不意味着的结果readarray
将是正确的,因为readarray
没有选择使用NUL
常规换行符代替换行符。
readarray -t sorted < <(printf '%s\n' "${array[@]}" | sort)
sort -z
是有用的改进,我想该-z
选项是GNU排序范围。
sorted=(); while read -d $'\0' elem; do sorted[${#sorted[@]}]=$elem; done < <(printf '%s\0' "${array[@]}" | sort -z)
。这在使用bash v3而不是bash v4的情况下也适用,因为readarray在bash v3中不可用。
<
)与进程替换 结合在一起<(...)
。或直觉地说:因为(printf "bla")
不是文件。
这是一个纯Bash快速排序实现:
#!/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=(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")'
此实现是递归的...因此,这是一个迭代的快速排序:
#!/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
}
在这两种情况下,您都可以更改使用的顺序:我使用了字符串比较,但是可以使用算术比较,比较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
}
然后,您可以具有以下比较功能:
compare_mtime() { [[ $1 -nt $2 ]]; }
并使用:
$ qsort compare_mtime *
$ declare -p qsort_ret
使当前文件夹中的文件按修改时间(最新的)排序。
注意。这些功能是纯Bash!没有外部实用程序,也没有子外壳!如果您有任何有趣的符号(空格,换行符,全局字符等),则它们是安全的。
sort
就足够了,那么sort
+ read -a
解决方案将从大约20个项目开始更快,并且随着您要处理的元素越来越多,而且越来越快。例如,在我2012年末运行OSX 10.11.1,带有Fusion Drive的iMac上:100个元素的阵列:ca. 0.03秒 (qsort()
)vs. 0.005秒 (sort
+ read -a
); 1000个元素的数组:0.375秒 (qsort()
)vs. 0.014秒(sort
+ read -a
)。
if [ "$i" -lt "$pivot" ]; then
是必需的,否则解析的“ 2” <“ 10”返回true。我相信这是POSIX与Lexicographical;或内联链接。
如果您不需要处理数组元素中的特殊外壳字符:
array=(a c b f 3 5)
sorted=($(printf '%s\n' "${array[@]}"|sort))
无论如何,使用bash都需要一个外部排序程序。
使用zsh不需要外部程序,并且可以轻松处理特殊的shell字符:
% array=('a a' c b f 3 5); printf '%s\n' "${(o)array[@]}"
3
5
a a
b
c
f
ksh必须set -s
按ASCIIbetchy排序。
set -A array x 'a a' d; set -s -- "${array[@]}"; set -A sorted "$@"
而且,当然,该组命令将如有重置当前位置参数。
tl; dr:
对数组进行排序a_in
并将结果存储在其中a_out
(元素不得嵌入换行符[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解决方案相比的优势:
您不必担心意外的globbing(将数组元素误解释为文件名模式),因此不需要额外的命令来禁用globbing(set -f
,并set +f
在以后进行恢复)。
你不必担心重新IFS
使用unset IFS
。[2]
上面的代码结合了Bash代码和外部实用程序,sort
以提供一种解决方案,该解决方案可以处理任意单行元素以及词法或数字排序(可选地按字段):
性能:对于大约20个或更多元素,这将比纯Bash解决方案要快 -显着并且越来越多,因此一旦超过100个元素。
(确切的阈值将取决于您的特定输入,机器和平台。)
printf '%s\n' "${a_in[@]}" | sort
执行排序(默认为词法化-参见sort
POSIX spec):
"${a_in[@]}"
安全地扩展为数组的元素a_in
作为单独的参数,无论它们包含什么(包括空格)。
printf '%s\n'
然后按原样打印每个参数(即每个数组元素)。
请注意,使用进程替代(<(...)
)将排序后的输出作为read
/的输入readarray
(通过重定向到stdin,<
)提供,因为read
/ readarray
必须在当前 shell中运行(一定不能在subshell中运行),以便输出变量a_out
可见到当前外壳程序(使变量在脚本的其余部分中保持定义)。
将sort
输出读取到数组变量中:
Bash v4 +:readarray -t a_out
将by所输出的各行读sort
入array variable的元素中a_out
,而不\n
在每个元素(-t
)中包括尾部。
Bash v3:readarray
不存在,因此read
必须使用:
IFS=$'\n' read -d '' -r -a a_out
告诉read
读入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[@]}"
由于使用了sort
不带选项的选项,因此会产生词法排序(数字在字母前排序,而数字序列在词法上视为数字,而不是数字):
*
10
5
a c
b
f
如果要按第一个字段进行数字排序,则可以使用sort -k1,1n
而不是just sort
,这样会产生结果(非数字先于数字排序,然后数字正确排序):
*
a c
b
f
5
10
[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
更改是全局的。
在从慕尼黑到法兰克福的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[@]}
打印:
3 5 a b c z y
从创建相同的输出
BSORT=(a c b 'z y' 3 5)
bubble_sort
echo ${BSORT[@]}
请注意,Bash可能在内部使用智能指针,因此交换操作可能很便宜(尽管我对此表示怀疑)。但是,这bubble_sort
表明merge_sort
Shell语言还可以实现更高级的功能。
local -n BSORT="$1"
在功能开始时。然后,您可以运行bubble_sort myarray
对myarray进行排序。
另一种解决方案是使用external sort
并处理任何特殊字符(NUL除外)。应该与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)
首先查看最后的输入重定向。我们正在使用printf
内置函数来写出零终止的数组元素。引用确保数组元素按原样传递,并且shell的特定性printf
导致它为每个其余参数重用格式字符串的最后一部分。也就是说,它等效于:
for e in "${array[@]}"; do
printf "%s\0" "${e}"
done
然后将以null终止的元素列表传递给sort
。该-z
选项使它读取以null终止的元素,对其进行排序并输出以null终止的元素。如果您只需要获取唯一元素,则可以通过,-u
因为它比更具可移植性uniq -z
。该LC_ALL=C
独立保证稳定的排序顺序的语言环境-有时脚本很有用。如果要sort
尊重语言环境,请删除该语言环境。
该<()
构造获取从生成的管道读取的描述符,并将循环<
的标准输入重定向到该描述符while
。如果您需要访问管道内部的标准输入,则可以使用另一个描述符-读者的练习:)。
现在,回到开始。该read
内置读取从标准输入重定向输出。设置为空IFS
将禁用单词拆分,这在这里是不必要的-结果,read
将读取整个“行”输入到单个提供的变量。-r
选项也会禁用此处不希望的转义处理。最后,-d ''
将行定界符设置为NUL,即告诉read
读取以零结尾的字符串。
结果,循环对于每个连续的零终止数组元素执行一次,其值存储在中e
。该示例只是将项目放置在另一个数组中,但是您可能更喜欢直接处理它们:)。
当然,这只是实现同一目标的多种方式之一。如我所见,它比在bash中实现完整的排序算法更简单,并且在某些情况下会更快。它处理包括换行符在内的所有特殊字符,并且应可在大多数常见系统上使用。最重要的是,它可能会教给您有关bash的新知识和很棒的知识:)。
e
并设置空的IFS,而使用REPLY变量。
如果您可以为数组中的每个元素计算唯一的整数,如下所示:
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=(a c b f 3 5)
for el in "${array[@]}"; do
sexy_int "$el"
sorted[$?]="$el"
done
echo "${sorted[@]}"
最小排序:
#!/bin/bash
array=(.....)
index_of_element1=0
while (( ${index_of_element1} < ${#array[@]} )); do
element_1="${array[${index_of_element1}]}"
index_of_element2=$((index_of_element1 + 1))
index_of_min=${index_of_element1}
min_element="${element_1}"
for element_2 in "${array[@]:$((index_of_element1 + 1))}"; do
min_element="`printf "%s\n%s" "${min_element}" "${element_2}" | sort | head -n+1`"
if [[ "${min_element}" == "${element_2}" ]]; then
index_of_min=${index_of_element2}
fi
let index_of_element2++
done
array[${index_of_element1}]="${min_element}"
array[${index_of_min}]="${element_1}"
let index_of_element1++
done
对于空格和换行符的常见问题,有一种解决方法:
使用字符不是原始数组中(如$'\1'
或$'\4'
或类似的)。
此功能可以完成工作:
# 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
}
这将对数组进行排序:
$ array=( a b 'c d' $'e\nf' $'g\1h')
$ sortarray "${array[@]}"
$ printf '<%s>\n' "${sorted[@]}"
<a>
<b>
<c d>
<e
f>
<gh>
这将抱怨源数组包含解决方法字符:
$ array=( a b 'c d' $'e\nf' $'g\4h')
$ sortarray "${array[@]}"
./script: error: array contains the workaround char
wa
(替代方法char)和一个空IFS$*
。[[ $* =~ [$wa] ]]
。exit 1
set -f
IFS=$'\n'
)的新值,循环变量x
和换行var(nl=$'\n'
)。$@
)。"${@//$nl/$wa}"
。sort -n
。set --
。for x
sorted+=(…)
"${x//$wa/$nl}"
。这个问题看起来密切相关。顺便说一句,这是Bash中的一个mergesort(没有外部过程):
mergesort() {
local -n -r input_reference="$1"
local -n output_reference="$2"
local -r -i size="${#input_reference[@]}"
local merge previous
local -a -i runs indices
local -i index previous_idx merged_idx \
run_a_idx run_a_stop \
run_b_idx run_b_stop
output_reference=("${input_reference[@]}")
if ((size == 0)); then return; fi
previous="${output_reference[0]}"
runs=(0)
for ((index = 0;;)) do
for ((++index;; ++index)); do
if ((index >= size)); then break 2; fi
if [[ "${output_reference[index]}" < "$previous" ]]; then break; fi
previous="${output_reference[index]}"
done
previous="${output_reference[index]}"
runs+=(index)
done
runs+=(size)
while (("${#runs[@]}" > 2)); do
indices=("${!runs[@]}")
merge=("${output_reference[@]}")
for ((index = 0; index < "${#indices[@]}" - 2; index += 2)); do
merged_idx=runs[indices[index]]
run_a_idx=merged_idx
previous_idx=indices[$((index + 1))]
run_a_stop=runs[previous_idx]
run_b_idx=runs[previous_idx]
run_b_stop=runs[indices[$((index + 2))]]
unset runs[previous_idx]
while ((run_a_idx < run_a_stop && run_b_idx < run_b_stop)); do
if [[ "${merge[run_a_idx]}" < "${merge[run_b_idx]}" ]]; then
output_reference[merged_idx++]="${merge[run_a_idx++]}"
else
output_reference[merged_idx++]="${merge[run_b_idx++]}"
fi
done
while ((run_a_idx < run_a_stop)); do
output_reference[merged_idx++]="${merge[run_a_idx++]}"
done
while ((run_b_idx < run_b_stop)); do
output_reference[merged_idx++]="${merge[run_b_idx++]}"
done
done
done
}
declare -ar input=({z..a}{z..a})
declare -a output
mergesort input output
echo "${input[@]}"
echo "${output[@]}"
我不认为您需要在Bash中使用外部排序程序。
这是我对简单冒泡排序算法的实现。
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=(a c b f 3 5)
echo " input: ${array[@]}"
echo "output: $(bubble_sort ${array[@]})"
这将打印:
input: a c b f 3 5
output: 3 5 a b c f
O(n^2)
。我似乎记得大多数排序算法都使用O(n lg(n))
直到最后一打元素左右。对于最后的元素,使用选择排序。
sorted=($(echo ${array[@]} | tr " " "\n" | sort))
本着bash / linux的精神,我将为每个步骤提供最佳的命令行工具。sort
可以完成主要工作,但需要用换行符代替空格而不是空格,因此上面非常简单的管道可以做到:
回声数组内容->用换行符替换空间->排序
$()
是回应结果
($())
是将“回显结果”放入数组中
注意:@sorontar在对另一个问题的评论中提到:
sorted =($(...))部分正在使用“ split and glob”运算符。您应该关闭glob:set -f或set -o noglob或shopt -op noglob或类似*的数组元素将扩展为文件列表。
mapfile -t sorted < <(printf '%s\n' "${array[@]}" | sort)
否则sorted=(); while IFS= read -r line; do sorted+=( "$line" ); done < <(printf '%s\n' | sort)
。
echo ${array[@]} | tr " " "\n"
如果数组的字段包含空格和glob字符,则此模式将中断。此外,它产生一个子shell并使用一个无用的外部命令。而且由于echo
是愚蠢的,这将打破,如果你的阵列开始-e
,-E
或-n
。而是使用:printf '%s\n' "${array[@]}"
。另一个反模式是:($())
将“回显结果”放入数组中。当然不是!这是一个可怕的反模式,由于路径名扩展(globbing)和单词拆分而中断。切勿使用这种恐怖手段。
IFS
,如果元素中包含空格,则会将您的元素拆分为小块。尝试如用IFS=$'\n'
省略吧!