我们是否会“找到”名称被“find”更改的文件?为什么不?

我们是否会“找到”名称被“find”更改的文件?为什么不?

回答的同时一个较旧的问题让我惊讶的是find,在下面的示例中,似乎可能会多次处理文件:

find dir -type f -name '*.txt' \
    -exec sh -c 'mv "$1" "${1%.txt}_hello.txt"' sh {} ';'

或者更有效率的

find dir -type f -name '*.txt' \
    -exec sh -c 'for n; do mv "$n" "${n%.txt}_hello.txt"; done' sh {} +

该命令查找.txt文件并将其文件名后缀从 更改.txt_hello.txt.

这样做时,目录将开始积累名称与*.txt模式匹配的新文件,即这些_hello.txt文件。

问题:为什么它们实际上没有被 处理find?因为根据我的经验,它们不是,我们也不希望它们是,因为这会引入一种无限循环。顺便说一句,mv替换为也是这种情况。cp

POSIX 标准说(我的重点)

如果从正在搜索的目录层次结构中删除或添加文件未指定是否find在搜索中包含该文件

由于未指定是否会包含新文件,也许更安全的方法是

find dir -type d -exec sh -c '
    for n in "$1"/*.txt; do
        test -f "$n" && mv "$n" "${n%.txt}_hello.txt"
    done' sh {} ';'

在这里,我们不查找文件而是查找目录,并且for内部sh脚本的循环在第一次迭代之前评估其范围一次,因此我们不会遇到相同的潜在问题。

GNUfind手册没有明确说明这一点,OpenBSDfind手册也没有明确说明。

答案1

可以find找到在遍历目录时创建的文件吗?

简而言之:是的,但这取决于实施。最好编写条件,以便忽略已处理的文件。

如前所述,POSIX 不保证任何一种方式,就像它也保证不保证底层readdir()系统调用:

如果在最近一次调用opendir()或后从目录中删除或添加文件rewinddir(),则后续调用是否readdir()返回该文件的条目是未指定的。


find在我的 Debian(GNU find,Debian 软件包版本4.6.0+git+20161106-2)上进行了测试。strace显示它在执行任何操作之前读取了完整目录。

多浏览一下源代码,看起来 GNU find 使用 gnulib 的一部分来读取目录,这在gnulib/lib/fts.cgl/lib/fts.cfind压缩包中):

/* If possible (see max_entries, below), read no more than this many directory
   entries at a time.  Without this limit (i.e., when using non-NULL
   fts_compar), processing a directory with 4,000,000 entries requires ~1GiB
   of memory, and handling 64M entries would require 16GiB of memory.  */
#ifndef FTS_MAX_READDIR_ENTRIES
# define FTS_MAX_READDIR_ENTRIES 100000
#endif

我将限制更改为 100,然后执行了

mkdir test; cd test; touch {0000..2999}.foo
find . -type f -exec sh -c 'mv "$1" "${1%.foo}.barbarbarbarbarbarbarbar"' sh {} \; -print

导致了像这个文件这样搞笑的结果,它被重命名了五次:

1046. 巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴巴

显然,需要一个非常大的目录(超过 100 000 个条目)才能在 GNU find 的默认构建上触发该效果,但没有缓存的简单 readdir+process 循环将更容易受到攻击。

理论上,如果操作系统总是按照返回文件的顺序最后添加重命名的文件readdir(),那么像这样的简单实现甚至可能会陷入无限循环。

在Linux上,readdir()C库中是通过getdents()系统调用实现的,它已经一次性返回多个目录项。这意味着稍后的调用readdir()可能会返回已删除的文件,但对于非常小的目录,您将有效地获得起始状态的快照。其他系统我不知道。

在上面的测试中,我故意重命名为更长的文件名:以防止文件名被就地覆盖。无论如何,对相同长度重命名的相同测试也进行了两次和三次重命名。当然,这是否重要以及如何重要取决于文件系统的内部结构。

find考虑到所有这些,通过表达来避免整个问题可能是明智的不是匹配已处理的文件。也就是说,添加-name "*.foo"到我的示例或! -name "*_hello.txt"问题中的命令中。

相关内容