给定一个包含以下内容的目录:
note 1.txt
,最后修改于昨天note 2.txt
,最后修改是前天note 3.txt
,今天最后修改
获取数组的最佳方法是什么note 3
note 1
note 2
?
为了定义“最佳”,我更关心的是鲁棒性(在 macOS 中的 Zsh 环境中),而不是效率和可移植性。
预期的用例是一个包含数百或数千个纯文本文件的目录,但是(冒着混淆问题的风险)这是我所遇到的一个更一般问题的特定情况,即在文件路径上执行字符串操作的最佳实践是什么通过ls
、find
、 和等命令打印mdfind
。
我一直在使用一个调用此命令的宏来实现上述目的:
ls -t | sed -e 's/.[^.]*$//'
它从来没有失败过,但是:
使用find
(使用字符而不是换行符安全地分隔文件路径NUL
)和参数扩展来提取基本名称,这会生成一个未排序的列表:
find . -type f -print0 | while IFS= read -d '' -r l ; do print "${${l%.*}##*/}" ; done
但按修改日期排序似乎需要调用stat
和sort
,因为 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,但是移植起来也非常痛苦,除非您求助于perl
或python
。
但是,如果您可以依赖perl
或python
在那里,您将能够让它们生成和排序文件列表,并以 NUL 分隔输出(尽管如果您想支持亚秒精度,则可能不太容易移植)。
ls -t | sed -e 's/.[^.]*$//'
对于包含换行符的文件名将无法正常工作(IIRC 某些版本的 macOS/etc
默认情况下附带了此类文件名)。对于包含未形成有效字符的字节序列的文件名,它也可能会失败,.
或者[^.]
可能无法匹配它们。但它可能不适用于 macOS,并且可以通过将区域设置设置为C
/ POSIX
for 来修复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
将代替foo
并bar
成为./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 图形)。
虽然仍有一些粗糙的地方,但这些工具已经足够好,可以开始使用它们了。
答案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
,我们甚至将文件名中的不可打印字符(制表符、换行符、回车符等)转换为可读文本。