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

Hai*_*ang 189 bash find

x=$(find . -name "*.txt")
echo $x
Run Code Online (Sandbox Code Playgroud)

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

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

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

Kev*_*vin 329

TL; DR:如果你只是在这里找到最正确的答案,你可能想要我个人的偏好,find . -name '*.txt' -exec process {} \;(见本文的底部).如果你有时间,请仔细阅读其余内容,看看几种不同的方式以及大多数问题.


完整答案:

最好的方法取决于你想做什么,但这里有一些选择.只要子树中的文件或文件夹名称中没有空格,您就可以循环遍历文件:

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done
Run Code Online (Sandbox Code Playgroud)

在边缘更好,切出临时变量x:

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done
Run Code Online (Sandbox Code Playgroud)

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

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done
Run Code Online (Sandbox Code Playgroud)

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

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done
Run Code Online (Sandbox Code Playgroud)

在某些情况下,例如,如果文件名已经在文件中,您可能需要使用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
Run Code Online (Sandbox Code Playgroud)

readfind通过适当设置分隔符可以安全地使用:

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process $line
    done
Run Code Online (Sandbox Code Playgroud)

对于更复杂的搜索,您可能希望使用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
Run Code Online (Sandbox Code Playgroud)

find也可以使用-execdir而不是在运行命令之前cd进每个文件的目录-exec,并且可以使用-ok而不是-exec(或-okdir代替-execdir)使其成为交互式(在为每个文件运行命令之前提示).

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

  • @phk `-d ''` 比 `-d $'\0'` 好。后者不仅更长,而且还表明您可以传递包含空字节的参数,但您不能。第一个空字节标记字符串的结尾。在 bash 中,`$'a\0bc'` 与 `a` 相同,而 `$'\0'` 与 `$'\0abc'` 相同,或者只是空字符串 `''`。`help read` 指出“* delim 的第一个字符用于终止输入*”,因此使用 `''` 作为分隔符有点小题大做。空字符串中的第一个字符是空字节,*总是* 标记字符串的结尾(即使您没有明确写下)。 (4认同)
  • 值得注意的是,在使用`done <filename`并且下一个使用管道的情况下,stdin不能再使用了(→循环中没有更多的交互式东西),但是在需要它的情况下可以使用` 3 <`而不是`<`并将`<&3`或`-u3`添加到`read`部分,基本上使用单独的文件描述符.另外,我认为`read -d''与`read -d $'\ 0'`相同,但我现在找不到任何官方文档. (3认同)
  • 对于 *.txt 中的 i;如果没有文件匹配,do 不起作用。需要一个额外的测试,例如 [[ -e $i ]] (2认同)
  • 我迷失了这一部分:`-exec process {} \;`我的猜测是另外一个问题 - 这意味着什么,我该如何操纵呢?哪个是好的Q/A或doc.在上面? (2认同)
  • @AlexHall 您可以随时查看手册页(`man find`)。在这种情况下,`-exec` 告诉`find` 执行以下命令,以`;`(或`+`)终止,其中`{}` 将被它正在处理的文件的名称替换(或者,如果使用`+`,则所有达到该条件的文件)。 (2认同)

0xC*_*22L 97

find . -name "*.txt"|while read fname; do
  echo "$fname"
done
Run Code Online (Sandbox Code Playgroud)

注意:此方法 bmargulies显示的(第二)方法可以安全地与文件/文件夹名称中的空格一起使用.

为了使文件/文件夹名称中包含换行符 - 有点奇特 - ,您将不得不求助于这样的-exec谓词find:

find . -name '*.txt' -exec echo "{}" \;
Run Code Online (Sandbox Code Playgroud)

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

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

find . -name '*.txt' -print0|xargs -0 -n 1 echo
Run Code Online (Sandbox Code Playgroud)

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

  • 可能值得指出的是,`find -print0`和`xargs -0`都是GNU扩展而不是可移植(POSIX)参数.但是,在那些拥有它们的系统上非常有用! (5认同)
  • 如果文件名中的换行符失败. (3认同)
  • @user unknown:你说得对,我根本没有考虑过这种情况,而且我认为这是非常奇特的。但我相应地调整了我的答案。 (3认同)
  • 另一个问题是,它“看起来”就像循环体在同一个 shell 中执行,但事实并非如此,因此例如 `exit` 不会按预期工作,循环体中设置的变量也不会执行。循环后可用。 (3认同)
  • 也就是说,在决定是否担心带有文字换行符的文件名时,请记住,攻击者创建 * 故意 * 难以删除的文件或将不需要的参数注入运行的命令的名称并非闻所未闻更高权限的用户。例如,考虑比 `$'/tmp/evil $\n/etc/passwd'` 会导致你的代码不仅跳过迭代 `'/tmp/evil '`,而且还会 *add* `/etc /passwd` 到您迭代的内容列表。 (2认同)

Dav*_* W. 97

你做什么,不要使用for循环:

# Don't do this
for file in $(find . -name "*.txt")
do
    …code using "$file"
done
Run Code Online (Sandbox Code Playgroud)

三个原因:

  • 要使for循环开始,find必须运行完成.
  • 如果文件名中包含任何空格(包括空格,制表符或换行符),则将其视为两个单独的名称.
  • 虽然现在不太可能,但您可以超出命令行缓冲区.想象一下,如果你的命令行缓冲区保持32KB,你的for循环返回40KB的文本.最后8KB将从你的for循环中删除,你永远不会知道它.

始终使用while read构造:

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    …code using "$file"
done
Run Code Online (Sandbox Code Playgroud)

循环将在find命令执行时执行.此外,即使返回带有空格的文件名,此命令也会起作用.并且,您不会溢出命令行缓冲区.

-print0将使用NULL作为文件分隔符,而不是换行和-d $'\0'边阅读将使用NULL作为分隔符.

  • 你应该使用`-r`选项来`读取`:`-r raw input - 禁止在读数据中解释反斜杠转义和行继续 (5认同)
  • 如果你可以使用-exec它会更好,但有时你确实需要给shell返回的名称.例如,如果要删除文件扩展名. (4认同)
  • 它不适用于文件名中的换行符.请改用find的`-exec`. (3认同)
  • 注意:这会将您的作用域放入子shell 中,并且您不会获得所有变量。 (3认同)
  • @userunknown - 你是对的。`-exec` 是最安全的,因为它根本不使用 shell。但是,文件名中的 NL 非常少见。文件名中的空格很常见。重点是不要使用许多海报推荐的`for`循环。 (2认同)
  • 实际上,这*将*适用于包含新行的文件名。这就是 `-print0` 的全部目的。 (2认同)
  • 另请参阅http://wiki.bash-hackers.org/commands/builtin/read#read_without_-r (2认同)

Mic*_*rux 13

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

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

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
Run Code Online (Sandbox Code Playgroud)

2.使用find-read-while和process替换

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

备注

在模式1:

  1. 如果没有找到匹配的文件,bash返回搜索模式("*.txt") - 因此需要额外的行"如果文件不存在则继续".请参阅Bash手册,文件名扩展
  2. shell选项nullglob可用于避免这个额外的行.
  3. "如果failglob设置了shell选项,但未找到匹配项,则会打印一条错误消息,并且不会执行该命令." (来自上面的Bash手册)
  4. shell选项globstar:"如果设置,文件名扩展上下文中使用的模式'**'将匹配所有文件和零个或多个目录和子目录.如果模式后跟'/',则只有目录和子目录匹配." 参见Bash手册,Shopt Builtin
  5. 其他选项文件名扩展:extglob,nocaseglob,dotglob及壳可变GLOBIGNORE

在模式2:

  1. 文件名可包含空格,制表符,空格,新行,...以安全的方式来处理文件名,find-print0使用:文件名是印有所有的控制字符和与NUL终止.另请参阅Gnu Findutils手册页,不安全文件名处理, 安全文件名处理,文件名中的异常字符.有关此主题的详细讨论,请参阅下面的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总是"真实"并且代码将始终回显"找不到文件".原因是:管道的每个命令都在一个单独的子shell中执行,因此循环内部更改的变量(单独的子shell)不会更改主shell脚本中的变量.这就是为什么我建议使用进程替换作为"更好",更有用,更通用的模式.
    请参阅我在管道中的循环中设置变量.为什么它们会消失......(来自Greg的Bash常见问题解答)有关该主题的详细讨论.

其他参考资料和来源:


Cel*_*luk 7

我想使用这段代码(在之后通过管道传输命令while done):

while read fname; do
  echo "$fname"
done <<< "$(find . -name "*.txt")"
Run Code Online (Sandbox Code Playgroud)

比这个答案更好,因为while循环是根据here在子shell中执行的,如果您使用这个答案while,并且如果您想修改循环内的变量,则在循环后看不到变量更改。


bma*_*ies 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
Run Code Online (Sandbox Code Playgroud)

  • `for $ in $(find ...)`将为任何带有空格的文件名中断.与`find ... |相同 xargs`除非你使用`-print0`和`-0` (3认同)

Sep*_*rvi 6

如果您可以假设文件名不包含换行符,则可以find使用以下命令将 的输出读入 Bash 数组:

readarray -t x < <(find . -name '*.txt')
Run Code Online (Sandbox Code Playgroud)

笔记:

  • -t导致readarray剥离换行符。
  • 如果readarray在管道中,它将不起作用,因此过程替换。
  • readarray 从 Bash 4 开始可用。

Bash 4.4 及更高版本还支持-d用于指定分隔符的参数。使用空字符而不是换行符来分隔文件名也适用于文件名包含换行符的罕见情况:

readarray -d '' x < <(find . -name '*.txt' -print0)
Run Code Online (Sandbox Code Playgroud)

readarray也可以像mapfile使用相同的选项一样调用。

参考:https : //mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream


use*_*825 6

(已更新,包括@Socowi出色的速度改进)

与任何$SHELL支持它的(破折号/ zsh / bash ...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +
Run Code Online (Sandbox Code Playgroud)

做完了


原始答案(较短但较慢):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;
Run Code Online (Sandbox Code Playgroud)

  • 这段代码中有一个错误。循环缺少第一个结果。这是因为 `$@` 省略了它,因为它通常是脚本的名称。我们只需在“'”和“{}”之间添加“dummy”,这样它就可以代替脚本名称,确保循环处理所有匹配项。 (4认同)
  • 你可以使用 `+` 来将尽可能多的文件传递给单个 `exec`,而不是 `\;`。然后在 shell 脚本中使用 `"$@"` 来处理所有这些参数。 (2认同)

归档时间:

查看次数:

188969 次

最近记录:

6 年,9 月 前