【问题标题】:Sort by function using bash/coreutils instead of perl使用 bash/coreutils 而不是 perl 按函数排序
【发布时间】:2014-01-18 14:48:17
【问题描述】:

我发现,如果在将文件放入 tar 存档之前按文件扩展名而不是字母顺序对文件列表进行排序,则可以显着提高压缩率(尤其是对于可能有大量 .c 的大型源代码树, .o 和 .h 文件)。

我找不到一种简单的方法来使用 shell 对文件进行排序,该 shell 在每种情况下都能按我期望的方式工作。诸如find | rev | sort | rev 之类的简单解决方案可以完成这项工作,但是文件以奇怪的顺序出现,并且它没有很好地排列它们以获得最佳压缩比。 ls -X 等其他工具不适用于find,并且当文件的文件名中包含多个句点(例如 version-1.5.tar)时,sort -t. -k 2,2 -k 1,1 会出错。另一个快速-n-dirty 选项,使用sed 将最后一个句点替换为/(从不在文件名中出现),然后排序,沿/ 拆分:

sed 's/\(\.[^.]*\)$/\/\1/' | sort -t/ -k 2,2 -k 1,1  |  sed 's/\/\([^/]*\)$/\1/'

但是,再次使用 find 的输出(名称中包含 /s,并且在 *nix 中的文件名中允许使用所有其他字符(0 除外))时,这将不起作用。

我发现使用 Perl,您可以编写一个自定义比较子例程,使用与 cmp 相同的输出(类似于 C 中的 strcmp),然后运行 ​​perl 排序函数,传递您自己的自定义比较,即使用 perl 正则表达式易于编写。这正是我所做的:我现在有一个 perl 脚本,它调用

@lines = <STDIN>;
print sort myComparisonFunction @lines;

然而,perl 不像 bash 那样可移植,所以我希望能够使用 shell 脚本。此外,find 没有在目录名称后面加上 /,因此脚本认为目录与没有扩展名的文件相同。理想情况下,我想让tar 首先读取所有目录,然后是常规文件(并对它们进行排序),然后是我可以通过

实现的符号链接
cat <(find -type d) <(find -type f | perl exsort.pl) <(find -not -type d -and -not -type f) | tar --no-recursion -T - -cvf myfile.tar

但我仍然遇到这样的问题,要么我必须每次都输入这个怪物,要么我有一个用于这个长行的 shell 脚本和一个用于排序的 perl 脚本,而 perl 并不是随处可用,所以把所有东西都塞进一个 perl 脚本也不是一个很好的解决方案。 (我主要关注较旧的计算机,因为现在所有现代 Linux 和 OSX 都带有足够新的 perl 版本。

我希望能够将所有内容放在一个 shell 脚本中,但我不知道如何将自定义函数传递给 GNU 排序工具。 我是不是运气不好,必须使用一个 perl 脚本?或者我可以用一个 shell 脚本来做到这一点吗?

编辑:感谢 Schwartizan 变换的想法。我使用了一种稍微不同的方法,使用sed。我的最终排序程序如下:

sed 's_^\(\([^/]*/\)*\)\(.*\)\(\.[^\./]*\)$_\4/\3/\1_' | sed 's_^\(\([^/]*/\)*\)\([^\./]\+\)$_/\3/\1_' | sort -t/ -k1,1 -k2,2 -k3,3 | sed 's_^\([^/]*\)/\([^/]*\)/\(.*\)$_\3\2\1_'

这会处理文件名中的特殊字符(例如 *),并首先放置没有扩展名的文件,因为它们通常是文本文件。 (生成文件、复制、自述文件、配置等)。

附:如果有人想要我原来的比较功能或认为我可以改进它,这里是:

sub comparison {
    my $first = $a;
    my $second = $b;
    my $fdir = $first =~ s/^(([^\/]*\/)*)([^\/]*)$/$1/r;
    my $sdir = $second =~ s/^(([^\/]*\/)*)([^\/]*)$/$1/r;
    my $fname = $first =~ s/^([^\/]*\/)*([^\/]*)$/$2/r;
    my $sname = $second =~ s/^([^\/]*\/)*([^\/]*)$/$2/r;
    my $fbase = $fname =~ s/^(([^\.]*\.)*)([^\.]*)$/$1/r;
    my $sbase = $sname =~ s/^(([^\.]*\.)*)([^\.]*)$/$1/r;
    my $fext = $fname =~ s/^([^\.]*\.)*([^\.]*)$/$2/r;
    my $sext = $sname =~ s/^([^\.]*\.)*([^\.]*)$/$2/r;
    if ($fbase eq "" && $sbase ne ""){
        return -1;
    }
    if ($sbase eq "" && $fbase ne ""){
        return 1;
    }
    (($fext cmp $sext) or ($fbase cmp $sbase)) or ($fdir cmp $sdir)
}

【问题讨论】:

  • "Perl 不如 bash 便携..." 什么???
  • @ThisSuitIsBlackNot 在旧计算机上。例如,旧版本的 Mac OS 默认不附带它。
  • 这和便携不一样。 Windows 也不附带 bash。
  • +1 表示整体问题和有关焦油效率的信息!。您能否提取扩展名,将其附加为临时第一个字段,并使用-k1 -k2(k2 是完整路径名)对其进行排序,然后从传递给 tar 的最终列表中删除 tmp 字段?
  • find 可以包含 /:find PATHS -type d -printf '%p/\n' -o -print(在非 GNU 中也可以使用 -exec printf 进行查找,但更难)

标签: regex perl bash shell sorting


【解决方案1】:

如果您熟悉 Perl,也可以在 BASH 中使用 Schwartzian Tranform

Schwartian 变换只是将您想要的排序键添加到您的排序信息中,进行排序,然后删除排序键。它由Randal Schwartz 创建并在 Perl 中大量使用。不过,它也可以用在其他语言中:

您想按扩展名对文件进行排序:

find . -type f 2> /dev/null | while read file   #Assuming no strange characters or white space
do
    suffix=${file##*.}
    printf "%-10.10s %s\n" "$suffix" "$file"
done | sort | awk '{print substr( $0, 8 ) }' > files_to_tar.txt

我正在使用find 读取每个文件。我使用printf 在我的文件名前加上我想要排序的后缀。然后,我做我的排序。我的awk 去掉了我的排序键,只留下我的文件名,这些文件名仍然按后缀排序。

现在,您的 files_to_tar.txt 文件包含按后缀排序的文件名称。可以使用tar-T参数从这个文件中读取文件名:

$ tar -czvf backup.tar.gz -T files_to_tar.txt

【讨论】:

  • 我最终用 sed 完成了它,所以我不必担心文件名中的括号。我的排序功能是这样的:sed 's_^\(\([^/]*/\)*\)\(.*\)\(\.[^\./]*\)$_\4/\3/\1_' | sed 's_^\(\([^/]*/\)*\)\([^\./]\+\)$_/\3/\1_' | sort -t/ -k1,1 -k2,2 -k3,3 | sed 's_^\([^/]*\)/\([^/]*\)/\(.*\)$_\3\2\1_'
  • +1,您可能还想处理没有扩展名的文件
  • @Zaid 我上面使用的 sed 例程首先放置没有扩展名的文件,因为其中大多数是文本文件(Makefile、COPYING、configure 等)
【解决方案2】:

您可以使用xargsfind 的结果传递给ls -X,(阅读手册页here)应该按扩展名对它们进行排序,

cat <(find -type d) <(find -type f | xargs ls -X ) <(find -not -type d -and -not -type f) | tar --no-recursion -T - -cvf myfile.tar

【讨论】:

  • +1 用于优雅的解决方案,但它并不完美。出于某种原因,即使使用ls -1Xdls 仍然主要根据文件扩展名对文件进行分组,但没有完全对它们进行分组,并且它不会按扩展名对文件进行分组,然后在每个扩展名中按名称对文件进行分组(这当您有多个版本的源代码树时,这是一个问题,因此您可能有 thisfilename.c 和多个 thatfilename.c 的多个副本,它们大多是相同的)。
【解决方案3】:

要按扩展名排序以对相似文件进行分组, 然后我的 md5sum 对相同的文件进行分组:

find $your_dir | xargs md5sum | sed 's/  /\x00/; s/\.[^.]$/&\x00&/' | sort -t'\0' -k3,3 | cut -d '' -f2

注意 sort -k3,3 是扩展排序,默认的“最后的手段”排序会按照 md5sum 对文件进行分组。

如果您担心空间问题,也可以考虑 xz 而不是 gz

【讨论】:

  • “假设文件中没有制表符或空格”对不起,但这是一个安全假设的可能性很大“如果您担心空间,请考虑 xz 而不是 gz”当然。但是已排序的 .tar.xz 和未排序的 .tar.xz 之间的区别是巨大的。
  • 那是为了说明。我已经修改为使用 NUL 分隔符来实现稳健的操作。
  • 另外,我完全不知道您为什么要使用 MD5 校验和。校验和被设计为尽可能不连续。排序时这绝对没有意义。 (这个想法不是将相同的文件收集在一起。它是收集具有相似内容的文件,按文件扩展名排序。#include 在 .c 文件中显示很多,因此应将它们组合在一起以获得最大压缩比。它们不会必须相同。)
  • md5sum 排序仅在扩展排序之后进行。 IE。扩展排序用于隐式地将相似的文件放在一起,而 md5sum 只是一个可选的附加功能,用于将相同的文件组合在一起。如果你想要介于两者之间的东西,你可以使用相似性哈希来代替stackoverflow.com/questions/4323977/…,但恕我直言,这太过分了,因为你实际上是在压缩程序本身中复制逻辑。它可能对不太复杂的(解)压缩器很有用
  • 如果您使用的是 XZ,那么 64 MiB 的字典就足以看到冗余。这对于 gzip 的 32k 字典可能是必要的,但 XZ 的字典足够大,因此不需要这样做。
猜你喜欢
  • 2011-03-23
  • 2016-01-26
  • 1970-01-01
  • 2015-11-13
  • 1970-01-01
  • 1970-01-01
  • 2021-01-24
  • 1970-01-01
  • 2019-05-16
相关资源
最近更新 更多