x=$(find . -name "*.txt")
echo $x

如果我在Bash shell中运行上面的代码段,我得到的是一个包含几个由空白分隔的文件名的字符串,而不是一个列表。

当然,我可以进一步用空白分隔它们以得到一个列表,但我相信有更好的方法来做到这一点。

那么,循环查找命令结果的最佳方法是什么呢?


当前回答

# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one

其他回答

如果你可以假设文件名不包含换行符,你可以使用以下命令将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

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就会找到不同的方法。

你可以存储你的查找输出在数组中,如果你希望以后使用输出:

array=($(find . -name "*.txt"))

现在要打印new line中的每个元素,可以使用for循环迭代数组的所有元素,也可以使用printf语句。

for i in ${array[@]};do echo $i; done

or

printf '%s\n' "${array[@]}"

你还可以使用:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

这将以换行符打印每个文件名

若要仅以列表形式打印查找输出,可以使用以下方法之一:

find . -name "*.txt" -print 2>/dev/null

or

find . -name "*.txt" -print | grep -v 'Permission denied'

这将删除错误消息,并仅在新行中输出文件名。

如果您希望对文件名做一些事情,将其存储在数组中是很好的,否则不需要占用该空间,您可以直接从find输出。

无论你做什么,都不要使用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作为分隔符。

你可以把find返回的文件名放入这样一个数组:

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

现在,您可以循环遍历数组以访问单个项,并对它们做任何您想做的事情。

注意:它是空白安全的。