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

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

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

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


当前回答

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

# 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

另一种选择是不使用bash,而是调用Python来完成繁重的工作。我反复使用这个方法是因为bash解决方案作为我的另一个答案太慢了。

使用这个解决方案,我们从内联Python脚本构建一个bash文件数组:

#!/bin/bash
set -eu -o pipefail

dsep=":"  # directory_separator
base_directory=/tmp

all_files=()
all_files_string="$(python3 -c '#!/usr/bin/env python3
import os
import sys

dsep="'"$dsep"'"
base_directory="'"$base_directory"'"

def log(*args, **kwargs):
    print(*args, file=sys.stderr, **kwargs)

def check_invalid_characther(file_path):
    for thing in ("\\", "\n"):
        if thing in file_path:
            raise RuntimeError(f"It is not allowed {thing} on \"{file_path}\"!")
def absolute_path_to_relative(base_directory, file_path):
    relative_path = os.path.commonprefix( [ base_directory, file_path ] )
    relative_path = os.path.normpath( file_path.replace( relative_path, "" ) )

    # if you use Windows Python, it accepts / instead of \\
    # if you have \ on your files names, rename them or comment this
    relative_path = relative_path.replace("\\", "/")
    if relative_path.startswith( "/" ):
        relative_path = relative_path[1:]
    return relative_path

for directory, directories, files in os.walk(base_directory):
    for file in files:
        local_file_path = os.path.join(directory, file)
        local_file_name = absolute_path_to_relative(base_directory, local_file_path)

        log(f"local_file_name {local_file_name}.")
        check_invalid_characther(local_file_name)
        print(f"{base_directory}{dsep}{local_file_name}")
' | dos2unix)";
if [[ -n "$all_files_string" ]];
then
    readarray -t temp <<< "$all_files_string";
    all_files+=("${temp[@]}");
fi;

for item in "${all_files[@]}";
do
    OLD_IFS="$IFS"; IFS="$dsep";
    read -r base_directory local_file_name <<< "$item"; IFS="$OLD_IFS";

    printf 'item "%s", base_directory "%s", local_file_name "%s".\n' \
            "$item" \
            "$base_directory" \
            "$local_file_name";
done;

相关:

操作系统。不用隐藏文件夹行走 如何做一个递归子文件夹搜索和返回文件在一个列表? 如何在Bash中将字符串分割成数组?

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

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

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

注意:它是空白安全的。