【问题标题】:(Quick-) sorting a list of files in POSIX sh(快速)在 POSIX sh 中对文件列表进行排序
【发布时间】:2021-07-08 04:08:07
【问题描述】:

很多时候,我发现自己认为最好有一个尽可能便携的通用解决方案,“如果我需要在奇怪或受限的机器上使用它”。

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

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

【问题讨论】:

  • 为了清楚起见,您只想根据LC_COLLATE 中设置的规则对文件名进行排序(默认按字典顺序)(ls -r)?或者你想对文件的内容进行排序(sort -r)?你想用它们做什么?
  • 最初,按字典顺序对文件名进行排序,就像ls -r 一样,如果不按时间排序,是的。但是,如果您不想区分文件名(请参阅任意名称),则无法解析 ls 输出。文件的内容无关紧要。最终,我需要迭代这个排序列表并将每个元素单独传递给一个对它有用的函数。我的实际脚本以复杂的方式生成 grub 配置/引导条目 - 最新的优先。但如何处理排序数据最终取决于用户。 :)
  • 另外,请注意,这个问题更像是问答式的。我的实际意图是分享这一点,因为尚未在 SO 上公开发布任何实现。希望对其他人有所帮助。
  • 通常在 POSIX 环境中,您可以安装 ruby​​/perl/python/C 编译器并在那里完成工作。

标签: sorting sh posix quicksort


【解决方案1】:

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

好的部分:

  • 它正在使用数组! (实际上,它确实以兼容的方式模拟了数组。)
  • 迭代,几乎具有随机枢轴选择的就地快速排序算法。这真的是greybeard'sanswer的改编版。 “几乎”部分来自于使用堆栈模拟来跟踪间隔,如果我没记错的话,它平均使用大约 O(log(n)) 额外空间。
  • 文件名可以包含任何字符(但NUL,这是 POSIX shell 中的一个常见问题,并且通常被文件系统所禁止)。
  • 比较函数 comp_lex_rev 可以轻松地换成不同的/修改为其他行为,例如,如果您需要按顺序进行数字排序 - 即它是模块化的。

丑陋的部分:

  • 基于rand() 的随机数生成,但其他任何事情都很难便携。我最初的代码曾经从 /dev/urandom 读取并解析该数据,但当然,这不是 POSIX 的一部分。
  • 奇怪的、以函数为前缀的变量名。这是 POSIX sh 不支持局部变量的直接后果。可以通过保存旧值、使用您认为合适的变量并最终恢复原始值来模拟它们(在某种程度上),但这确实使代码更加不可读(并且容易出错,因为您要么需要有一个修复函数中的退出点或在每个可能的退出点复制恢复功能)。使用唯一 (ahem) 字符串为变量添加前缀可以解决此问题,以可读性换取范围。
  • 需要直接访问全局输入伪数组。在子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' '%s\n' "${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' '%s\n' "${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'"'"' '"'"'%s\n'"'"' "${'"${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'

【讨论】:

    【解决方案2】:

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

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

    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
    

    注意:我们必须使用evil 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"
    

    【讨论】:

    • 好点。 Glob 表达式必须根据排序规则进行排序。所以第一个解决方案适用于“正常”订单。 for ((...)) 构造(类似于数字 C 的 for 循环)虽然是一种 bashism,但不可移植。通过用 while 循环替换它很容易使其与 POSIX 兼容。将文件名设置为位置参数并从后到前迭代是一种巧妙的处理方式。
    • @Ionic。还有一些问题。我想他们现在都解决了。
    • 看起来不错。您可以再次编辑它以修复 eval 行吗?应该是eval "file=\${${i}}"; - 否则它会因i > 9 而中断。我无法保存我的编辑,因为更改小于 6 个字符...
    • 我会将您的解决方案标记为已接受,因为它以干净、快速、优雅、简短和聪明的方式解决了最初的问题。对于其他情况(例如,非字典顺序),我的帖子提供了一个可以调整的更通用的解决方案。这样,我们应该能够用我们的两种算法覆盖所有情况。 :)
    猜你喜欢
    • 2019-09-25
    • 1970-01-01
    • 1970-01-01
    • 2020-03-03
    • 2018-07-03
    • 1970-01-01
    • 2021-12-15
    • 2016-01-06
    • 2016-05-09
    相关资源
    最近更新 更多