x=$(find . -name "*.txt")
echo $x
如果我在Bash shell中运行上面的代码段,我得到的是一个包含几个由空白分隔的文件名的字符串,而不是一个列表。
当然,我可以进一步用空白分隔它们以得到一个列表,但我相信有更好的方法来做到这一点。
那么,循环查找命令结果的最佳方法是什么呢?
x=$(find . -name "*.txt")
echo $x
如果我在Bash shell中运行上面的代码段,我得到的是一个包含几个由空白分隔的文件名的字符串,而不是一个列表。
当然,我可以进一步用空白分隔它们以得到一个列表,但我相信有更好的方法来做到这一点。
那么,循环查找命令结果的最佳方法是什么呢?
当前回答
无论你做什么,都不要使用for循环:
# Don't do this
for file in $(find . -name "*.txt")
do
…code using "$file"
done
三个原因:
为了使For循环开始,find必须运行到完成。 如果一个文件名中有任何空格(包括空格、制表符或换行符),它将被视为两个独立的名称。 虽然现在不太可能,但您可以溢出命令行缓冲区。想象一下,如果您的命令行缓冲区有32KB,而for循环返回40KB的文本。最后的8KB将从for循环中删除,而您永远不会知道。
总是使用while read结构:
find . -name "*.txt" -print0 | while read -d $'\0' file
do
…code using "$file"
done
循环将在执行find命令时执行。另外,即使返回的文件名中有空格,该命令也可以工作。而且,不会溢出命令行缓冲区。
print0将使用NULL作为文件分隔符而不是换行符,而-d $'\0'将在读取时使用NULL作为分隔符。
其他回答
如果使用grep而不是find呢?
ls | grep .txt$ > out.txt
现在您可以读取这个文件,文件名以列表的形式出现。
根据@phk的其他回答和评论,使用fd #3: (仍然允许在循环中使用stdin)
while IFS= read -r f <&3; do
echo "$f"
done 3< <(find . -iname "*filename*")
无论你做什么,都不要使用for循环:
# Don't do this
for file in $(find . -name "*.txt")
do
…code using "$file"
done
三个原因:
为了使For循环开始,find必须运行到完成。 如果一个文件名中有任何空格(包括空格、制表符或换行符),它将被视为两个独立的名称。 虽然现在不太可能,但您可以溢出命令行缓冲区。想象一下,如果您的命令行缓冲区有32KB,而for循环返回40KB的文本。最后的8KB将从for循环中删除,而您永远不会知道。
总是使用while read结构:
find . -name "*.txt" -print0 | while read -d $'\0' file
do
…code using "$file"
done
循环将在执行find命令时执行。另外,即使返回的文件名中有空格,该命令也可以工作。而且,不会溢出命令行缓冲区。
print0将使用NULL作为文件分隔符而不是换行符,而-d $'\0'将在读取时使用NULL作为分隔符。
TL;DR:如果你只是想知道最正确的答案,你可能想知道我的个人偏好(见本文底部):
# execute `process` once for each file
find . -name '*.txt' -exec process {} \;
如果有时间,请通读其余部分,了解几种不同的方法以及其中大多数方法的问题。
完整的答案是:
最好的方法取决于你想做什么,但这里有一些选择。只要子树中没有文件名中有空格的文件或文件夹,你就可以遍历这些文件:
for i in $x; do # Not recommended, will break on whitespace
process "$i"
done
稍微好一点,去掉临时变量x:
for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
process "$i"
done
当你可以的时候,最好是glob。空白安全,对于当前目录中的文件:
for i in *.txt; do # Whitespace-safe but not recursive.
process "$i"
done
通过启用globstar选项,你可以glob所有匹配的文件在这个目录和所有子目录:
# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
process "$i"
done
在某些情况下,例如,如果文件名已经在文件中,你可能需要使用read:
# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
process "$line"
done < filename
通过适当设置分隔符,Read可以安全地与find结合使用:
find . -name '*.txt' -print0 |
while IFS= read -r -d '' line; do
process "$line"
done
对于更复杂的搜索,你可能会使用find,或者带-exec选项,或者带-print0 | xargs -0:
# execute `process` once for each file
find . -name \*.txt -exec process {} \;
# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +
# using xargs*
find . -name \*.txt -print0 | xargs -0 process
# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument
Find还可以在运行命令之前使用-execdir而不是-exec来CD到每个文件的目录,并且可以使用-ok而不是-exec(或-okdir而不是-execdir)来进行交互(在为每个文件运行命令之前提示)。
*:从技术上讲,find和xargs(默认情况下)都会在命令行中使用尽可能多的参数运行命令,次数与遍历所有文件所需的次数相同。在实践中,除非您有非常多的文件,否则这并不重要,如果您超过了长度,但需要在同一个命令行上全部使用它们,那么SOL就会找到不同的方法。
如果你可以假设文件名不包含换行符,你可以使用以下命令将find的输出读入Bash数组:
readarray -t x < <(find . -name '*.txt')
注意:
-t导致readarray删除换行符。 如果readarray在管道中,它将不起作用,因此需要进行进程替换。 readarray从Bash 4开始就可用了。
Bash 4.4及更高版本还支持-d参数来指定分隔符。使用空字符而不是换行符来分隔文件名也适用于文件名包含换行符的罕见情况:
readarray -d '' x < <(find . -name '*.txt' -print0)
Readarray也可以作为具有相同选项的mapfile调用。
参考:https://mywiki.wooledge.org/BashFAQ/005 # Loading_lines_from_a_file_or_stream