(Quick-)在POSIX sh中对文件列表进行排序



很多时候,我发现自己在想,最好有一个尽可能便携的通用解决方案,"如果我在一个奇怪的或约束的机器上需要这个";。

我一直在寻找一种方法,可以或多或少地以相反的顺序对目录中的文件列表进行高效排序,只使用POSIX-sh和工具。

这应该适用于任意命名的文件,包括名称中带有控制代码字符(例如换行符(的文件。

因此,我们的想法是以字典相反的顺序处理一组文件。正如您所知,由于文件名怪异,您无法解析ls。由于这是POSIX,所以我们没有数组。因此,这里有一个可行的解决方案。

全局表达式按字典顺序返回可能的文件名列表。所以你可以做一些类似的事情

for file in /path/to/dir/*; do
[ -e "${file}" ] || continue
some_command "${file}"
done

如果你想逆转它,你可以做:

set -- /path/to/dir/*
i=$#
while [ "$i" -gt 0 ]; do 
eval "file=${${i}}"; i=$((i-1));
[ -e "${file}" ] || continue
some_command "$file"
done

注意:我们必须使用邪恶的eval来评估位置变量。

更新:位置变量可能已经在使用中。在这种情况下,您可以执行以下操作:

j=$#
set -- /path/to/dir/* "$@"
i=$(($#-$j))
while [ "$i" -gt 0 ]; do 
eval "file=${${i}}"; i=$((i-1));
[ -e "${file}" ] || continue
some_command "$file"
done
shift "$i"

据我所知,这段代码完全符合POSIX。唯一与POSIX不兼容的部分是使用pwgen生成测试数据。我不想在这篇文章上做得太过火,因为我不认为它是实际代码的一部分——这只是为了。。。

好的部分:

  • 它使用的是数组!(实际上,它确实以兼容的方式模拟数组。(
  • 迭代的几乎具有随机枢轴选择的原地快速排序算法。这真的是灰胡子答案的改编版本。">几乎";部分来自于使用堆栈模拟来跟踪间隔,如果我没有弄错的话,它平均使用大约O(log(n))的额外空间
  • 文件名可以包括任何字符(但NUL,这是POSIXshell中的一个常见问题,通常被文件系统禁止(
  • 比较函数comp_lex_rev可以很容易地交换为不同的函数/修改为其他行为,例如,如果您需要按顺序进行数字排序,即它是模块化的

丑陋的部分:

  • 基于rand()的随机数生成,但其他任何事情都很难移植。我最初的代码用于读取/dev/urandom并解析该数据,但当然,这不是POSIX的一部分
  • 奇怪的、以函数为前缀的变量名。这是POSIX-sh不支持局部变量的直接结果。它们可以通过保存旧值、使用您认为合适的变量并最终恢复原始值来模拟(在某种程度上(,但这确实会使代码变得更加不可读(并且容易出错,因为您要么需要在函数中有一个固定的退出点,要么在每个可能的退出点重复恢复功能(。在变量前面加一个唯一的(嗯哼(字符串可以解决这个问题,用可读性换取可扩展性
  • 需要直接访问全局输入伪数组。在子shell中调用函数而不处理结果只会浪费CPU时间,因为父进程中的原始数据不会更改。这是shell脚本中的一个常见问题,但可能值得一提
  • 整数很棘手。最初,我明确地将随机数获取部分限制为2^16 - 1,因为这是最小的保证整数大小——这可能意味着外壳也必须支持它。然而,在那里实施限制是没有意义的。相反,我在生成输入数组时选择了某种溢出检测。请记住,shell中可用的最大整数大小是特定于实现的,有一个硬下限
  • 由于是基于shell的,与C中的直接二进制实现相比,它相当慢。expr以速度慢而臭名昭著,并且向其他程序分叉会使shell脚本变得更慢。作为一则轶事,我通过用zsh内部正则表达式处理替换grepsed调用,大大减少了zsh脚本中的处理时间。同样,这一点更像是shell脚本的一个常见问题,与我的代码无关,但记住这一点也很好
#!/bin/sh
#set -x
comp_lex_rev () {
comp_lex_rev_a="${1:?'No lhs passed to comp_lex_rev(), this is invalid.'}"
comp_lex_rev_b="${2:?'No rhs passed to comp_lex_rev(), this is invalid.'}"
comp_lex_rev_expr_out="$('expr' "x${comp_lex_rev_a}" '>' "x${comp_lex_rev_b}")"
if [ '1' -eq "${comp_lex_rev_expr_out}" ]; then
return '0'
else
return '1'
fi
}
get_rand () {
get_rand_min="${1:?'No minimum value passed to get_rand(), this is invalid.'}"
get_rand_max="${2:?'No maximum value passed to get_rand(), this is invalid.'}"
# Minimum value must be positive.
if [ '0' -gt "${get_rand_min}" ]; then
return '1'
fi
# Max > min doesn't make sense... (we could just swap them here, but meh.)
if [ "${get_rand_min}" -gt "${get_rand_max}" ]; then
return '1'
fi
# Not much to do if both are the same value.
if [ "${get_rand_min}" -eq "${get_rand_max}" ]; then
'printf' '%sn' "${get_rand_min}"
return '0'
fi
# Just be extra careful.
get_rand_out=''
while [ -z "${get_rand_out}" ] || [ "${get_rand_min}" -gt "${get_rand_out}" ] || [ "${get_rand_max}" -lt "${get_rand_out}" ]; do
get_rand_out="$('awk' '
BEGIN {
srand ();
printf ("%u", (rand () * (('"${get_rand_max}"' - '"${get_rand_min}"') + 1)) + '"${get_rand_min}"');
}')"
done
'printf' '%sn' "${get_rand_out}"
}
qsort () {
qsort_arr="${1:?'No array base passed to qsort(), this is invalid.'}"
qsort_n="${2:?'No array length passed to qsort(), this is invalid.'}"
if [ '2' -gt "${qsort_n}" ]; then
# One or zero elements are always sorted.
return '0'
fi
qsort_range_0='0'
qsort_range_1="$((${qsort_n} - 1))"
qsort_range_n='2'
# Must have at least one pair entry in the range "stack".
while [ '1' -lt "${qsort_range_n}" ]; do
qsort_range_n="$((${qsort_range_n} - 1))"
eval 'qsort_high="${qsort_range_'"${qsort_range_n}"'}"'
qsort_range_n="$((${qsort_range_n} - 1))"
eval 'qsort_low="${qsort_range_'"${qsort_range_n}"'}"'
qsort_cur_i="${qsort_low}"
qsort_pivot_i="$('get_rand' "${qsort_low}" "${qsort_high}")"
if [ '0' -ne "${?}" ]; then
# Fetching random value failed, fall back to rightmost element.
qsort_pivot_i="${qsort_high}"
fi
eval 'qsort_pivot="${'"${qsort_arr}"'_'"${qsort_pivot_i}"'}"'
# Move pivot up if it isn't already.
if [ "${qsort_high}" != "${qsort_pivot_i}" ]; then
eval "${qsort_arr}"'_'"${qsort_pivot_i}"'="${'"${qsort_arr}"'_'"${qsort_high}"'}"'
eval "${qsort_arr}"'_'"${qsort_high}"'="${qsort_pivot}"'
qsort_pivot_i="${qsort_high}"
fi
eval 'qsort_cur="${'"${qsort_arr}"'_'"${qsort_cur_i}"'}"'
while [ "${qsort_pivot_i}" -gt "${qsort_cur_i}" ]; do
if 'comp_lex_rev' "${qsort_cur}" "${qsort_pivot}"; then
qsort_cur_i="$((${qsort_cur_i} + 1))"
eval 'qsort_cur="${'"${qsort_arr}"'_'"${qsort_cur_i}"'}"'
else
eval "${qsort_arr}"'_'"${qsort_pivot_i}"'="${qsort_cur}"'
qsort_pivot_i="$((${qsort_pivot_i} - 1))"
eval "${qsort_arr}"'_'"${qsort_cur_i}"'="${'"${qsort_arr}"'_'"${qsort_pivot_i}"'}"'
eval 'qsort_cur="${'"${qsort_arr}"'_'"${qsort_pivot_i}"'}"'
fi
done
eval "${qsort_arr}"'_'"${qsort_pivot_i}"'="${qsort_pivot}"'
qsort_lhs_size="$((${qsort_pivot_i} - ${qsort_low}))"
qsort_rhs_size="$((${qsort_high} - ${qsort_pivot_i}))"
if [ "${qsort_lhs_size}" -le "${qsort_rhs_size}" ]; then
if [ '1' -lt "${qsort_lhs_size}" ]; then
eval 'qsort_range_'"${qsort_range_n}"'="$((${qsort_pivot_i} + 1))"'
qsort_range_n="$((${qsort_range_n} + 1))"
eval 'qsort_range_'"${qsort_range_n}"'="${qsort_high}"'
qsort_range_n="$((${qsort_range_n} + 1))"
eval 'qsort_range_'"${qsort_range_n}"'="${qsort_low}"'
qsort_range_n="$((${qsort_range_n} + 1))"
eval 'qsort_range_'"${qsort_range_n}"'="$((${qsort_pivot_i} - 1))"'
qsort_range_n="$((${qsort_range_n} + 1))"
fi
else
eval 'qsort_range_'"${qsort_range_n}"'="${qsort_low}"'
qsort_range_n="$((${qsort_range_n} + 1))"
eval 'qsort_range_'"${qsort_range_n}"'="$((${qsort_pivot_i} - 1))"'
qsort_range_n="$((${qsort_range_n} + 1))"
fi
if [ '1' -lt "${qsort_rhs_size}" ]; then
eval 'qsort_range_'"${qsort_range_n}"'="$((${qsort_pivot_i} + 1))"'
qsort_range_n="$((${qsort_range_n} + 1))"
eval 'qsort_range_'"${qsort_range_n}"'="${qsort_high}"'
qsort_range_n="$((${qsort_range_n} + 1))"
fi
done
}
print_arr () {
print_arr_arr="${1:?'No array base passed to print_arr(), this is invalid.'}"
print_arr_n="${2:?'No array length passed to print_arr(), this is invalid.'}"
print_arr_i='0'
while [ "${print_arr_n}" -gt "${print_arr_i}" ]; do
if [ '0' -ne "${print_arr_i}" ]; then
'printf' '===n'
fi
eval "'"'printf'"'"' '"'"'%sn'"'"' "${'"${print_arr_arr}"'_'"${print_arr_i}"'}"'
print_arr_i="$((${print_arr_i} + 1))"
done
}
generate_testdata () {
generate_testdata_dir="${1:?'No testdata directory passed to generate_testdata(), this is invalid.'}"
generate_testdata_i='0'
generate_testdata_n='100'
(
'mkdir' "${generate_testdata_dir}"
'cd' "${generate_testdata_dir}"
while [ "${generate_testdata_n}" -gt "${generate_testdata_i}" ]; do
# We'll map the first underscore character to a space character and
# ditto for right curly bracket vs. newline since pwgen generates no
# such characters by default.
':' > "$('pwgen' '-s' '-y' '-c' '-n' '-r' '/' '100' '1' | 'sed' '-e' 's#_# #' '-e' 's#}#
#')"
generate_testdata_i="$((${generate_testdata_i} + 1))"
done
)
}
main () {
main_testdir='testdata'
if [ ! -d "${main_testdir}" ]; then
'generate_testdata' "${main_testdir}"
fi
main_cur_file=''
main_flist_n='0'
main_flist_old_n="${main_flist_n}"
for main_cur_file in "${main_testdir}"/*; do
if [ -f "${main_cur_file}" ]; then
eval 'main_flist_'"${main_flist_n}"'="${main_cur_file}"'
main_flist_n="$((${main_flist_n} + 1))"
if [ "${main_flist_old_n}" -ge "${main_flist_n}" ]; then
# Overflow (or... none-flow?), stop working.
'printf' 'Too many files to handle, aborting.n' >&'2'
return '1'
else
main_flist_old_n="${main_flist_n}"
fi
fi
done
# Sort, in reverse order.
'qsort' 'main_flist' "${main_flist_n}"
# And finally print out.
'print_arr' 'main_flist' "${main_flist_n}"
# In GNU terms,
# 'find' "${main_testdir}" '-type' 'f' '-print0' | 'sort' '-rz' | 'sed' '-e' 's#x0#n===n#g'
# should return about the same data, only with an additional separator at the end.
}
# Actual main code.
'main'

最新更新