问题
for f in $(find .)
结合了两个不兼容的事物。
find
打印以换行符分隔的文件路径列表。当您$(find .)
在列表上下文中未加引号时调用的split + glob运算符将其拆分为字符$IFS
(默认情况下包括换行符,还包括空格和制表符(以及中的NUL zsh
)),并对每个结果单词执行遍历(除中的zsh
)(甚至在ksh93或pdksh衍生物中大括号扩展!)。
即使您做到了:
IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion in pdksh
# but not ksh93)
for f in $(find .) # invoke split+glob
这仍然是错误的,因为换行符与文件路径中的任何字符一样有效。的输出find -print
根本无法可靠地进行后处理(除非使用一些复杂的技巧,如此处所示)。
这也意味着外壳程序需要存储完整的输出find
,然后将其拆分+ glob(这意味着将输出再次存储在内存中),然后再开始循环遍历文件。
请注意,find . | xargs cmd
存在类似的问题(存在空格,换行符,单引号,双引号和反斜杠(并且在某些xarg
实现中,字节不构成有效字符的一部分)是一个问题)
更正确的选择
只有这样,才能使用for
上的输出回路find
是使用zsh
支持IFS=$'\0'
和:
IFS=$'\0'
for f in $(find . -print0)
(替换-print0
用-exec printf '%s\0' {} +
的find
是不支持非标准(但相当普遍时下)实现-print0
)。
在这里,正确且可移植的方法是使用-exec
:
find . -exec something with {} \;
或者,如果something
可以接受多个参数:
find . -exec something with {} +
如果确实需要外壳处理该文件列表:
find . -exec sh -c '
for file do
something < "$file"
done' find-sh {} +
(请注意,它可能启动不止一个sh
)。
在某些系统上,您可以使用:
find . -print0 | xargs -r0 something with
虽然具有少许优于标准语法和装置something
的stdin
要么是管或/dev/null
。
您可能要使用的一个原因可能是使用-P
GNU选项xargs
进行并行处理。该stdin
问题也可以通过GNU来解决xargs
,该-a
选项带有支持进程替换的shell:
xargs -r0n 20 -P 4 -a <(find . -print0) something
例如,最多运行4个并发调用,something
每个调用都带有20个文件参数。
使用zsh
或bash
,另一种循环输出的方法find -print0
是:
while IFS= read -rd '' file <&3; do
something "$file" 3<&-
done 3< <(find . -print0)
read -d ''
读取NUL分隔记录,而不是换行分隔记录。
bash-4.4
及以上版本还可以将find -print0
数组返回的文件存储为:
readarray -td '' files < <(find . -print0)
该zsh
当量(其中有保留的优点find
的退出状态):
files=(${(0)"$(find . -print0)"})
使用zsh
,您可以将大多数find
表达式转换为递归glob和glob限定符的组合。例如,循环find . -name '*.txt' -type f -mtime -1
将是:
for file (./**/*.txt(ND.m-1)) cmd $file
要么
for file (**/*.txt(ND.m-1)) cmd -- $file
(注意的需要的--
,与**/*
,文件路径不与开始./
,所以可以与启动-
例如)。
ksh93
并bash
最终增加了对**/
(尽管没有更多的递归形式的递归glob)支持,但仍然没有glob限定符,这使此处的使用**
非常有限。还要注意,bash
降级目录树时,4.3之前的版本会遵循符号链接。
像循环一样$(find .)
,这也意味着将整个文件列表存储在内存1中。在某些情况下,当您不希望对文件执行的操作对文件的查找有影响时(例如在添加更多可能最终被自己发现的文件时),这可能是理想的。
其他可靠性/安全性考虑
比赛条件
现在,如果我们谈论可靠性,我们必须提到时间find
/ zsh
找到文件并检查文件是否符合标准以及使用时间之间的竞争条件(TOCTOU race)。
即使降落在目录树上,也必须确保不遵循符号链接,并且在没有TOCTOU竞争的情况下这样做。find
(GNU find
至少)不会通过使用开放目录,openat()
用正确的O_NOFOLLOW
标志(如果支持),并保持文件描述符打开每个目录,zsh
/ bash
/ ksh
不这样做。因此,面对攻击者能够在正确的时间用符号链接替换目录的情况,您最终可能会得出错误的目录。
即使find
不恰当地降低目录,以-exec cmd {} \;
更应如此有-exec cmd {} +
,一旦cmd
被执行,例如作为cmd ./foo/bar
或cmd ./foo/bar ./foo/bar/baz
由时间cmd
利用的./foo/bar
,属性bar
可能不再符合条件匹配的find
,但更糟的是,./foo
可能已经替换为到其他位置的符号链接(并且-exec {} +
在find
等待有足够文件调用的地方,竞赛窗口变得更大了cmd
)。
一些find
实现具有(尚未标准化的)-execdir
谓词来缓解第二个问题。
拥有:
find . -execdir cmd -- {} \;
find
chdir()
在运行之前进入文件的父目录cmd
。而不是调用cmd -- ./foo/bar
,而是调用cmd -- ./bar
(cmd -- bar
在某些实现中,因此是--
),因此./foo
避免了更改为符号链接的问题。这样可以使用rm
更安全的命令(它仍然可以删除其他文件,但不能删除其他目录中的文件),但是除非这些文件被设计为不遵循符号链接,否则它们不能修改这些文件的命令。
-execdir cmd -- {} +
有时也可以使用,但是在一些实现中包括GNU的一些版本find
,它等同于-execdir cmd -- {} \;
。
-execdir
还具有解决与目录树太深相关的一些问题的好处。
在:
find . -exec cmd {} \;
给定路径的大小cmd
将随着文件所在目录的深度而增加。如果该大小变得更大PATH_MAX
(类似于Linux上的4k),则cmd
在该路径上执行的任何系统调用都会失败并显示ENAMETOOLONG
错误。
使用时-execdir
,仅文件名(可能带有./
)被传递到cmd
。在大多数文件系统上,文件名本身的限制(NAME_MAX
)比更低得多PATH_MAX
,因此ENAMETOOLONG
不太可能遇到该错误。
字节与字符
另外,在考虑安全性时find
,通常在处理文件名时通常会忽略一个事实,即在大多数类Unix系统上,文件名是字节序列(文件路径中除0以外的任何字节值,在大多数系统上,(基于ASCII的字符,我们现在将忽略基于EBCDIC的罕见字符)(0x2f是路径分隔符)。
由应用程序决定是否将这些字节视为文本。它们通常这样做,但是通常从字节到字符的转换是根据用户的语言环境(基于环境)完成的。
这意味着给定的文件名可能会根据区域设置具有不同的文本表示形式。例如,字节序列63 f4 74 e9 2e 74 78 74
将côté.txt
用于在字符集为ISO-8859-1 cєtщ.txt
的语言环境中以及在字符集为IS0-8859-5的语言环境中解释该文件名的应用程序。
更差。在字符集为UTF-8(当今的规范)的语言环境中,根本无法将63 f4 74 e9 2e 74 78 74映射到字符!
find
是一个这样的应用程序,它将文件名视为其-name
/ -path
谓词的文本(以及更多类似-iname
或-regex
带有某些实现的文本)。
这意味着例如具有多个find
实现(包括GNU find
)。
find . -name '*.txt'
63 f4 74 e9 2e 74 78 74
在UTF-8语言环境中调用时,将无法在上方找到我们的文件,因为*
(匹配0个或更多字符,而不是字节)不能匹配那些非字符。
LC_ALL=C find...
可以解决该问题,因为C语言环境意味着每个字符一个字节,并且(通常)保证所有字节值都映射到一个字符(尽管某些字节值可能是未定义的)。
现在,当涉及从外壳循环这些文件名时,该字节与字符之间的关系也会成为问题。在这方面,我们通常会看到4种主要的外壳类型:
仍不支持多字节的类似dash
。对于他们来说,一个字节映射到一个字符。例如,在UTF-8中côté
为4个字符,但为6个字节。在以UTF-8为字符集的语言环境中,
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
find
将成功找到名称由UTF-8编码的4个字符组成的文件,但dash
报告的长度在4到24之间。
yash
:相反。它只处理字符。它需要的所有输入都在内部翻译为字符。它构成了最一致的shell,但也意味着它不能应付任意字节序列(那些不转换为有效字符的序列)。即使在C语言环境中,它也无法处理0x7f以上的字节值。
find . -exec yash -c 'echo "$1"' sh {} \;
例如,在UTF-8语言环境中使用ISO-8859-1失败côté.txt
。
那些类似的bash
或zsh
已逐步添加多字节支持的地方。这些将退回到考虑不能像字符一样映射到字符的字节。他们仍然在这里和那里仍然有一些错误,特别是对于不常见的多字节字符集,例如GBK或BIG5-HKSCS(它们非常讨厌,因为它们的许多多字节字符包含0-127范围内的字节(例如ASCII字符) )。
类似于sh
FreeBSD的(至少11个)或mksh -o utf8-mode
支持多字节但仅适用于UTF-8的那些。
笔记
1为了完整起见,我们可以提到一种zsh
使用递归glob遍历文件而不将整个列表存储在内存中的骇人方法:
process() {
something with $REPLY
false
}
: **/*(ND.m-1+process)
+cmd
是一个glob限定符,它cmd
使用中的当前文件路径调用(通常是一个函数)$REPLY
。该函数返回true或false以决定是否应选择文件(并且还可以修改$REPLY
或返回$reply
数组中的多个文件)。在这里,我们在该函数中进行处理并返回false,因此不会选择该文件。