如何遍历find返回的文件名?

时间:2012-03-08 02:23:35

标签: bash find

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

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

当然,我可以进一步将它们分开来获取列表,但我确信有更好的方法可以做到。

那么循环查看find命令结果的最佳方法是什么?

13 个答案:

答案 0 :(得分:298)

TL; DR:如果您只是在这里寻找最正确的答案,您可能需要我的个人偏好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

如果可以的话,它会更好地很多。白色空间安全,适用于当前目录中的文件:

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

通过启用globstar选项,您可以对此目录和所有子目录中的所有匹配文件进行选通:

# 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也可以进入每个文件的目录,并且可以使用(在为每个文件运行命令之前提示) -ok代替-exec(或-okdir代替-execdir)。

*:从技术上讲,findxargs(默认情况下)都将使用尽可能多的参数来运行命令,因为它们可以在命令行中使用,所需的次数与通过所有参数的次数相同文件。在实践中,除非你有非常多的文件,否则无关紧要,如果你超过了长度但需要在同一命令行中,你是SOL 找到一种不同的方式。

答案 1 :(得分:91)

find . -name "*.txt"|while read fname; do
  echo "$fname"
done

注意:这个方法 bmargulies显示的(第二个)方法可以安全地用于文件/文件夹名称中的空格。

为了在文件/文件夹名称中包含换行符(有点异国情调),您必须使用-exec这样的find谓词:

find . -name '*.txt' -exec echo "{}" \;

{}是找到的项目的占位符,\;用于终止-exec谓词。

为了完整起见,让我添加另一种变体 - 你必须喜欢* nix的方式来实现它们的多功能性:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

根据我的知识,这会将打印的项目与文件或文件夹名称中的任何文件系统中不允许的\0字符分开,因此应涵盖所有基础。 xargs逐个接受它们......

答案 2 :(得分:90)

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

答案 3 :(得分:12)

文件名可以包含空格甚至控制字符。空格是bash中shell扩展的(默认)分隔符,因此根本不推荐使用x=$(find . -name "*.txt")的问题。如果find获取带有空格的文件名,例如"the file.txt"如果您在循环中处理x,您将获得2个分开的字符串进行处理。您可以通过更改分隔符(bash IFS变量)来改善这一点,例如到\r\n,但文件名可以包含控制字符 - 所以这不是一个(完全)安全的方法。

从我的角度来看,有两种推荐(和安全)模式用于处理文件:

<强> 1。用于循环和放大文件名扩展:

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

<强> 2。使用find-read-while&amp;流程替代

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

<强>说明

模式1上的

    如果找不到匹配的文件,
  1. bash返回搜索模式(&#34; * .txt&#34;),如果文件不存在,则额外的行&#34;继续&#34;需要。见Bash Manual, Filename Expansion
  2. shell选项nullglob可用于避免此额外行。
  3. &#34;如果设置了failglob shell选项,但未找到匹配项,则会打印错误消息并且不执行该命令。&#34; (来自上面的Bash手册)
  4. shell选项globstar:&#34;如果设置,文件名扩展上下文中使用的模式“**”将匹配所有文件以及零个或多个目录和子目录。如果模式后跟'/',则只有目录和子目录匹配。&#34;见Bash Manual, Shopt Builtin
  5. 文件名扩展的其他选项:extglobnocaseglobdotglob&amp; shell变量GLOBIGNORE
  6. 模式2上的

    1. 文件名可以包含空格,制表符,空格,换行符,...以安全的方式处理文件名,使用find -print0:文件名打印所有控制字符和放大器;终止于NUL。另请参阅Gnu Findutils Manpage, Unsafe File Name Handlingsafe File Name Handlingunusual characters in filenames。有关此主题的详细讨论,请参阅下面的David A. Wheeler。

    2. 有一些可能的模式可以在while循环中处理查找结果。其他人(凯文,大卫W.)已经展示了如何使用管道做到这一点:

      files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
      当你尝试这段代码时,你会发现它不起作用:files_found始终是&#34; true&#34; &安培;代码将始终回显&#34;没有找到文件&#34;。原因是:管道的每个命令都在一个单独的子shell中执行,因此循环内部更改的变量(单独的子shell)不会更改主shell脚本中的变量。这就是为什么我建议使用流程替换作为更好的&#34;更有用,更通用的模式。
      请参阅I set variables in a loop that's in a pipeline. Why do they disappear...(来自Greg的Bash常见问题解答)进行详细讨论关于这个主题。

    3. 其他参考资料&amp;来源:

答案 4 :(得分:6)

# 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

答案 5 :(得分:5)

如果您希望稍后将输出用作以下内容,则可以将find输出存储在数组中:

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

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

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

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

您也可以使用:

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

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

要仅以列表形式打印find输出,您可以使用以下任一项:

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

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

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

如果您希望对文件名执行某些操作,将其存储在数组中是好的,否则无需占用该空间,您可以直接打印find的输出。

答案 6 :(得分:3)

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

答案 7 :(得分:3)

(更新为包括@Socowi的优秀速度提升)

支持它的任何$SHELL(dash / zsh / bash ...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

完成。

原始答案(更短,但更慢):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;

答案 8 :(得分:1)

您可以将find返回的文件名放入如下数组中:

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

现在,您可以循环访问数组以访问各个项目,并随意执行任何操作。

注意:这是白色空间安全。

答案 9 :(得分:1)

我喜欢使用首先分配给变量的find和IFS切换到新行,如下所示:

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

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

以防您想在同一组DATA上重复更多操作,并且发现服务器上的速度很慢(I / 0高利用率)

答案 10 :(得分:0)

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

这将列出文件并提供有关属性的详细信息。

答案 11 :(得分:0)

根据@phk的其他答案和评论,使用fd#3:
(它仍允许在循环中使用stdin)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")

答案 12 :(得分:-4)

如果使用grep而不是find,那该怎么样?

ls | grep .txt$ > out.txt

现在您可以阅读此文件,文件名采用列表形式。