列出目录中每个基本名称(按修改日期排序)的最可靠方法?

列出目录中每个基本名称(按修改日期排序)的最可靠方法?

给定一个包含以下内容的目录:

  • note 1.txt,最后修改于昨天
  • note 2.txt,最后修改是前天
  • note 3.txt,今天最后修改

获取数组的最佳方法是什么note 3 note 1 note 2

为了定义“最佳”,我更关心的是鲁棒性(在 macOS 中的 Zsh 环境中),而不是效率和可移植性。

预期的用例是一个包含数百或数千个纯文本文件的目录,但是(冒着混淆问题的风险)这是我所遇到的一个更一般问题的特定情况,即在文件路径上执行字符串操作的最佳实践是什么通过lsfind、 和等命令打印mdfind


我一直在使用一个调用此命令的宏来实现上述目的:

ls -t | sed -e 's/.[^.]*$//'

它从来没有失败过,但是:

  • 格雷格的维基强烈建议不要解析 的输出ls。 (解析ls;实践,在“5.永远不要这样做”下)。
  • sed在参数扩展可以做的地方调用效率低下吗?

使用find(使用字符而不是换行符安全地分隔文件路径NUL)和参数扩展来提取基本名称,这会生成一个未排序的列表:

find . -type f -print0 | while IFS= read -d '' -r l ; do print "${${l%.*}##*/}" ; done

但按修改日期排序似乎需要调用statsort,因为 macOSfind缺少-printf标记否则可能会很好用

最后,使用 Zsh 的全局限定符

for f in *(om) ; do print "${f%.*}" ; done

虽然不可移植,但最后一种方法对我来说似乎是最强大和最有效的。这是正确的find吗?当我实际执行搜索而不是简单地列出目录中的文件时,是否有任何理由不应该使用上述命令的修改版本?

答案1

zsh

list=(*(Nom:r))

绝对是最稳健的。

print -rC1 -- *(Nom:r)

每行打印一个,或者

print -rNC1 -- *(Nom:r)

作为 NUL 分隔的记录,以便能够对该输出执行任何操作,因为 NUL 是文件路径中唯一不允许的字符。

*(N-om:r)如果要考虑修改时间则改为符号链接解析(目标的运行时间,而不是像 那样的符号链接ls -Lt)。

:r(为了name) 是历史修饰符(来自csh),用于删除扩展名。请注意,它会变成空字符串,只有启用该选项.bashrc时才会出现这种情况。dotglob

更改为**/*(N-om:t:r)以递归方式执行(:t对于尾巴(basename),即删除目录组件)。

对任意文件名可靠地执行此操作ls将非常痛苦。

一种方法可能是运行ls -td -- ./*(假设文件名列表符合 arg 列表限制)并解析该输出,依赖于每个文件名以 开头的事实./,并生成 NUL 分隔列表或 shell 引用列表将其传递给 shell,但是移植起来也非常痛苦,除非您求助于perlpython

但是,如果您可以依赖perlpython在那里,您将能够让它们生成和排序文件列表,并以 NUL 分隔输出(尽管如果您想支持亚秒精度,则可能不太容易移植)。

ls -t | sed -e 's/.[^.]*$//'

对于包含换行符的文件名将无法正常工作(IIRC 某些版本的 macOS/etc默认情况下附带了此类文件名)。对于包含未形成有效字符的字节序列的文件名,它也可能会失败,.或者[^.]可能无法匹配它们。但它可能不适用于 macOS,并且可以通过将区域设置设置为C/ POSIXfor 来修复sed

应该.转义 ( s/\.[^.]*$//),因为它是匹配任何字符的正则表达式运算符,否则,它将无点文件转换foobar为空字符串。

注意打印一个字符串生的, 它是:

print -r -- "$string"

print "$string"$string对于以 开头的值将会失败-,甚至会引入命令注入漏洞(例如尝试使用string='-va[$(uname>&2)1]',此处使用无害的uname命令)。并且会破坏包含\字符的值。

你的:

find . -type f -print0 | while IFS= read -d '' -r l ; do print "${${l%.*}##*/}" ; done

还有一个问题是你剥离了.* 删除目录组件。因此,例如 a./foo.d/bar将代替foobar成为./foo空字符串。

find关于在各种 shell 中处理输出的安全方法,请参阅为什么循环查找的输出是不好的做法?

答案2

IMNSHO 稳健性和 shell 脚本是不兼容的概念(IFS 只是一个 hack,抱歉)。我认为只有两种方法可以以稳健的方式完成您想要的事情:要么用某种理智的语言(Python、C 等)编写程序,要么使用专门为稳健性而构建的工具。

使用 csv-nix-tools (*),您可以通过以下方式实现此目的:

csv-ls -c name,mtime_sec,mtime_nsec | 
csv-sort -c mtime_sec,mtime_nsec | 
csv-cut -c name |
csv-add-split -c name -e . -n base,ext -r | 
csv-cut -c base |
csv-header --remove

相当不言自明。

如果您只想查看文件的基本名称,那就足够了,但通常情况下,您想用刚刚获得的数据做一些有用的事情。这就是水槽工具的用处。目前,有 3 个:csv-exec(对每一行执行命令)、csv-show(以人类可读的形式格式化数据)和 csv-plot(使用 gnuplot 生成 2D 或 3D 图形)。

虽然仍有一些粗糙的地方,但这些工具已经足够好,可以开始使用它们了。

(*)https://github.com/mslusarz/csv-nix-tools

答案3

我很惊讶没有看到已经涵盖的另一种方法,该方法适用于任何采用相当广泛的 ksh 扩展(包括 bash 和 zsh)的 shell,在具有 GNU 工具的系统上:

while IFS= read -r -d ' ' time && IFS= read -r -d '' filename; do
  printf 'Filename %q, with epoch time %s\n' "$filename" "$time"
done < <(find . -mindepth 1 -maxdepth 1 -printf '%T@ %P\0' | sort -gz)

解释它是如何工作的:

  • 格式find字符串%T@ %P\0为每个文件打印一个十进制时间戳(可以选择亚秒精度)、一个空格、该文件的基本名称,然后是一个 NUL。
  • 在 中sort -gz-g是正确处理浮点数值的广义排序;并-z期望 NUL 而不是换行符作为分隔符。
  • 在 中IFS= read -r -d ' ' time && IFS= read -r -d '' filename,我们在第一个空格处终止时间的读取;而我们在第一个 NUL 处终止文件名的读取。
  • 在使用格式 string 打印结果时%q,我们甚至将文件名中的不可打印字符(制表符、换行符、回车符等)转换为可读文本。

相关内容