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


8

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

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
这不是一个好主意,但是您可以 按bash排序
Jeff Schaller

如果您有dir1 / 42.pdf和dir2 / 42.pdf,请小心输入基名称的数组
Jeff Schaller

在我的情况下,不会发生(具有相同基名的不同路径名)。但是,如果bash脚本可以处理它,那就太好了。我对如何对具有相同基本名称的路径名进行排序没有很好的要求,也许其他人也可以。dir1 dir2只是组成的,它们实际上是任意路径名。
蒂姆(Tim)

Answers:


4

与ksh或zsh相反,bash不支持对数组或任意字符串列表进行排序。它可以排序水珠或输出aliassettypeset(虽然这最后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 ''也可以perl输出正确引用的路径列表,以便将其传递给eval "array=($(perl...))"

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

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

通过reply=($filearray)我们实际上迫使全局扩展(最初只是/)成为数组的元素。然后,我们根据文件名的尾部定义排序顺序。

对于strcmp()样顺序,固定区域设置为C.对于数值排序(类似于GNU sort -V,不sort -n进行比较时,这使得一个显著差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))

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

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

¹如果strcmp()可以接受基于-的排序,并且对于短字符串,则可以awk在传递给字符串之前将其转换为十六进制编码,然后sort在排序之后进行转换。


请参阅下面的答案,以获得出色的bash 单线
kael

9

sort在GNU中,coreutils允许自定义字段分隔符和键。您设置/为字段分隔符,然后根据第二个字段进行排序,以对基本名称而不是整个路径进行排序。

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

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

4
这是的标准选项sort,而不是GNU扩展。如果路径的长度都相同,这将起作用。
库萨兰达

在同一时间相同的答案:)
MiniMax

2
仅当路径每个都包含一个目录时,此方法才有效。那some/long/path/0011.pdf呢 据我在手册页上看到的,sort没有包含按最后一个字段排序的选项。
Federico Poloni

5

gawk表达式排序(受bash的支持readarray):

包含空格的文件名示例数组:

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

readarray -t sortedfilearr < <(printf '%s\n' "${filearray[@]}" | awk -F'/' '
   BEGIN{PROCINFO["sorted_in"]="@val_num_asc"}
   { a[$0]=$NF }
   END{ for(i in a) print i}')

输出:

echo "${sortedfilearr[*]}"
dir2/name 0003.pdf dir1/name 0010.pdf dir3/name 0040.pdf

访问单个项目:

echo "${sortedfilearr[1]}"
dir1/name 0010.pdf

假定没有文件路径包含换行符。请注意,中的值的数字排序@val_num_asc仅适用于键的前导数字部分(在本示例中为无),而回退到关系的词法比较(基于strcmp(),而不是语言环境的排序顺序)。


4
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数组。


@StéphaneChazelas有点毛茸茸,但是还可以...
库萨兰达

请注意,可以说,它为路径计算了错误的基本名称/some/dir/
斯特凡Chazelas

@StéphaneChazelas是的,但是OP专门说他有文件路径,所以我只假设路径末尾有一个正确的基名。
库萨兰达

注意,在一个典型的GNU非C语言环境,a/x.c++ b/x.c-- c/x.c++会按顺序进行排序,即使-各种各样之前+因为-+/的主要重量是忽略(这样比较x.c++/a/x.c++x.c--/b/x.c++第一比较xcaxc反对xcbxc的,只有在关系的情况下,将其他权重(其中,-来之前+)将被考虑。
斯特凡Chazelas

可以通过join /x/代替来解决/,但不能解决基于ASCII的系统在C语言环境中进行a/foo排序a/foo.txt的情况,例如,由于进行/排序.
斯特凡Chazelas

4

由于“ dir1and dir2是任意路径名”,我们不能指望它们由单个目录(或相同数量的目录)组成。因此,我们需要将路径名中的最后一个斜杠转换为路径名中其他地方不会出现的内容。假设@您的数据中没有该字符,则可以按以下基本名称进行排序:

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

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


哈!这很棒!我通过对一个非显示字符进行了编码,使其变得更加健壮(并且略显丑陋),例如:cat pathnames | sed 's|\(.*\)/|\1'$'\4''|' | sort -t$'\4' -k+2nr | sed 's|'$'\4''|/|'。(我刚刚\4从ascii桌子上抓起。显然是“文本
结尾

@kael \4^D(control-D)。除非您自己在终端上键入它,否则它是普通的控制字符。换句话说,以这种方式可以安全使用。
Alexis

3

简短(且较快)的解决方案:通过将数组索引附加到文件名上并对其进行排序,我们稍后可以基于排序的索引创建排序的版本。

该解决方案仅需要bash内置函数和sort二进制文件,并且还可以用于不包含换行符的所有文件名\n

index=0 sortedfilearray=()
while read -r line ; do
    sortedfilearray+=("${filearray[${line##* }]}")
done <<< "$(for i in "${filearray[@]}" ; do
    echo "$(basename "$i") $((index++))"
done | sort -n)"

对于每个文件,我们都将其基本名称与初始索引相呼应,如下所示:

0010.pdf 0
0003.pdf 1
0040.pdf 2

然后通过发送sort -n

0003.pdf 1
0010.pdf 0
0040.pdf 2

之后,我们遍历输出行,使用bash变量扩展提取旧索引,${line##* }然后将此元素插入新数组的末尾。


1
+1无需传递每个文件的全名进行排序的解决方案
roaima

3

排序方式是在文件路径名前加上基本名称,然后按数字排序,然后从字符串开头剥离基本名称:

#!/bin/bash
#
filearray=("dir1/0010.pdf" "dir2/0003.pdf" "dir3/0040.pdf" "dir4/0003.pdf")

sortarray=($(
    for file in "${filearray[@]}"
    do
        echo "$file"
    done |
        sed -r 's!^(.*)/([[:digit:]]*)(.*)$!\2 \1/\2\3!' |
        sort -t $'\t' -n |
        sed -r 's![^ ]* !!'
))

for item in "${sortarray[@]}"
do
    echo "> $item <"
done

如果您将文件名放在可以直接通过管道而不是作为shell数组传递的列表中,则效率会更高,因为实际的工作是由sed | sort | sed结构完成的,但这足够了。

在Perl中进行编码时,我首先遇到了这种技术。在这种语言中,它被称为Schwartzian变换

在Bash中,如果文件的基本名称中包含非数字,则在我的代码中给出的转换将失败。在Perl中,可以更安全地对其进行编码。


谢谢。bash中的“列表”是什么?它与bash数组不同吗?我从没听说过,那就太好了。是的,将文件名存储在“列表”中可能是个好主意。我从运行脚本的命令行参数获得文件名,$@$*可以从命令行参数获取文件名
Tim

将文件名存储在文件中可以使用外部实用程序,但也可能会误解例如换行符。
杰夫·谢勒

如《四人帮》中的《设计模式》一书中介绍的那样,是否使用Schwartzian变换对某种设计模式进行排序,例如模板,策略,...模式?
蒂姆(Tim)

@JeffSchaller幸运的是,没有数字换行。如果我编写的是完全通用的文件名安全代码,则很可能不会使用bash。
roaima

3

对于相等深度的文件名。

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

sorted_file_array=($(printf "%s\n" "${filearray[@]}" | sort -n -t'/' -k2))

说明

-k POS1 [,POS2] -推荐的POSIX选项,用于指定排序字段。该字段由POS1和POS2之间的线路的一部分的(该行的末尾,或者如果省略POS2),包容。字段和字符位置从1开始编号。因此,要在第二个字段上排序,请使用-k 2,2。

-t SEPARATOR 在每行中查找排序键时,请使用字符SEPARATOR作为字段分隔符。默认情况下,非空白字符和空白字符之间的字段由空字符串分隔。

信息来自排序人员。

结果阵列打印

printf "%s\n" "${sorted_file_array[@]}"
dir2/0003.pdf
dir1/0010.pdf
dir3/0014.pdf
dir3/0040.pdf
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.