按文件的基名对文件路径名数组进行排序

按文件的基名对文件路径名数组进行排序

假设我有存储在数组中的文件路径名列表

filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" ) 

我想根据文件名的基名按数字顺序对数组中的元素进行排序

sortedfilearray=("dir2/0003.pdf" "dir1/0010.pdf" "dir3/0040.pdf") 

我怎样才能做到这一点?

我只能对它们的基本名称部分进行排序:

basenames=()
for file in "${filearray[@]}"
do
    filename=${file##*/}
    basenames+=(${filename%.*})
done
sortedbasenamearr=($(printf '%s\n' "${basenames[@]}" | sort -n))

我正在考虑

  • 创建一个关联数组,其键是基本名称,值是路径名,因此对路径名的访问始终通过基本名称完成。
  • 仅为基本名称创建另一个数组,并应用于sort基本名称数组。

谢谢。

答案1

sortGNU coreutils 允许自定义字段分隔符和键。您设置/为字段分隔符并根据第二个字段进行排序以按基本名称而不是整个路径进行排序。

printf "%s\n" "${filearray[@]}" | sort -t/ -k2将产生

dir2/0003.pdf
dir1/0010.pdf
dir3/0040.pdf

答案2

oldIFS="$IFS"; IFS=$'\n'
if [[ -o noglob ]]; then
  setglob=1; set -o noglob
else
  setglob=0
fi

sorted=( $(printf '%s\n' "${filearray[@]}" |
            awk '{ print $NF, $0 }' FS='/' OFS='/' |
            sort | cut -d'/' -f2- ) )

IFS="$oldIFS"; unset oldIFS
(( setglob == 1 )) && set +o noglob
unset setglob

文件名排序换行符以他们的名字命名会导致这sort一步出现问题。

它生成一个/以 - 分隔的列表,awk其中包含第一列中的基本名称和其余列中的完整路径:

0003.pdf/dir2/0003.pdf
0010.pdf/dir1/0010.pdf
0040.pdf/dir3/0040.pdf

这就是排序的内容,cut用于删除第一个/- 分隔的列。结果被转换成一个新的bash数组。

答案3

由于“dir1dir2是任意路径名”,我们不能指望它们由单个目录(或相同数量的目录)组成。所以我们需要将最后的路径名中的斜杠指向路径名中其他地方未出现的内容。假设该字符@没有出现在您的数据中,您可以按基本名称排序,如下所示:

cat pathnames | sed 's|\(.*\)/|\1@|' | sort -t@ -k+2 | sed 's|@|/|'

第一个sed命令替换最后的每个路径名中带有所选分隔符的斜杠,第二个将反转更改。 (为简单起见,我假设路径名可以每行传递一个。如果它们位于 shell 变量中,请首先将它们转换为每行一个格式。)

答案4

与 ksh 或 zsh 相反,bash 没有对数组或任意字符串列表进行排序的内置支持。它可以对 glob 或aliasor setor的输出进行排序typeset(尽管最后 3 个不在用户的区域设置排序顺序中),但这实际上不能在这里使用。

POSIX 工具箱中没有任何东西可以轻松地对任意字符串列表进行排序(sort对行进行排序,因此只有除 NUL 和换行符之外的短字符序列(LINE_MAX 通常短于 PATH_MAX),而文件路径是其他非空字节序列大于 0)。

因此,虽然您可以实现自己的排序算法awk(使用<字符串比较运算符)或甚至bash(使用[[ < ]]),对于 中的任意路径bash,可移植,最简单的可能是求助于perl

有了bash4.4+,你可以这样做:

readarray -td '' sorted_filearray < <(perl -MFile::Basename -l0 -e '
  print for sort {basename($a) cmp basename($b)} @ARGV' -- "${filearray[@]}")

这给出了strcmp()类似的命令。对于基于区域设置排序规则的顺序(例如在 glob 或 的输出中)ls,请-Mlocale向 中添加一个参数perl。对于数字排序(更像 GNU,sort -g因为它支持 等数字+31.2e-5而不是千位分隔符,尽管不是十六进制),请使用<=>代替cmp(并且再次-Mlocale像命令一样尊重用户的小数标记sort)。

您将受到命令参数最大大小的限制。为了避免这种情况,您可以将文件列表传递到perl其标准输入,而不是通过参数:

readarray -td '' sorted_filearray < <(
  printf '%s\0' "${filearray[@]}" | perl -MFile::Basename -0le '
    chomp(@files = <STDIN>);
    print for sort {basename($a) cmp basename($b)} @files')

对于旧版本的bash,您可以使用while IFS= read -rd ''循环代替readarray -d ''或 getperl输出正确引用的路径列表,以便您可以将其传递给eval "array=($(perl...))".

使用zsh,您可以伪造一个全局扩展,您可以为其定义排序顺序:

sorted_filearray=(/(e{'reply=($filearray)'}oe{'REPLY=$REPLY:t'}))

我们reply=($filearray)实际上强制 glob 扩展(最初只是/)成为数组的元素。然后我们定义基于文件名尾部的排序顺序。

对于strcmp()类似于 - 的顺序,请将区域设置固定为 C。对于数字排序(类似于 GNU sort -V,在比较和(例如,在区域设置中,其中是小数点)sort -n时不会产生显着差异),添加glob 限定符。1.41.23.n

除了 之外oe{expression},您还可以使用函数来定义排序顺序,例如:

by_tail() REPLY=$REPLY:t

或更高级的,例如:

by_numbers_in_tail() REPLY=${(j:,:)${(s:,:)${REPLY:t}//[^0-9]/,}}

(因此a/foo2bar3.pdf(2,3 个数字) 排序在b/bar1foo3.pdf(1,3) 之后但在c/baz2zzz10.pdf(2,10) 之前)并用作:

sorted_filearray=(/(e{'reply=($filearray)'}no+by_numbers_in_tail))

当然,这些可以应用于真实的球体,因为这就是它们的主要用途。例如,对于pdf任何目录中的文件列表,按基本名称/尾部排序:

pdfs=(**/*.pdf(N.oe+by_tail))

1 如果基于 - 的排序是可接受的,并且对于短字符串,您可以在传递到之前strcmp()将字符串转换为其十六进制编码,并在排序后转换回来。awksort

相关内容