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


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

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

当然,我可以用空格进一步分隔它们以获得列表,但是我敢肯定有更好的方法可以做到这一点。

那么遍历find命令结果的最佳方法是什么?


3
遍历文件名的最佳方法很大程度上取决于您实际要使用的文件,但是除非您可以保证没有文件名中包含空格,否则这不是一个好方法。那么,您想在文件循环中做什么?
凯文(Kevin)

1
关于赏金:这里的主要思想是得到一个涵盖所有可能情况的规范答案(带有新行的文件名,有问题的字符...)。这个想法是然后使用这些文件名来做一些事情(调用另一个命令,执行一些重命名...)。谢谢!
fedorqui'SO停止伤害

不要忘记文件或文件夹的名称可以包含“ .txt”,后跟空格和另一个字符串,例如“ something.txt something”或“ something.txt”
Yahya Yahyaoui

使用数组,而不是var x=( $(find . -name "*.txt") ); echo "${x[@]}"然后可以遍历for item in "${x[@]}"; { echo "$item"; }
Ivan

Answers:


389

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

如果可以的话,最好是glob。空格安全,用于当前目录中的文件:

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

*:从技术上讲,findxargs(默认情况下)都将使用在命令行上可以容纳的尽可能多的参数来运行命令,并且该命令要遍历所有文件所需的次数也要多次。在实践中,除非您拥有大量文件,否则没有关系,并且如果您超出了文件长度,但都需要它们在同一命令行上,则SOL会找到不同的方式。


4
值得一提的是,在与案件done < filename和下面的一个与标准输入,不能使用任何更多的管道(→内循环没有更多的互动的东西),但在需要的地方的情况下可以使用3<的替代<和补充 <&3-u3以该read部分,基本上使用单独的文件描述符。另外,我相信与之read -d ''相同,read -d $'\0'但目前无法找到任何官方文档。
phk

1
为我在* .txt中;如果没有文件匹配,则do不起作用。需要一个Xtra测试,例如[[-e $ i]]
Michael Brux '16

2
我迷失了这一部分:-exec process {} \;我的猜测是那是另一个问题-这意味着什么,我该如何处理?好的Q / A或文档在哪里。在上面?
亚历克斯·霍尔

1
@AlexHall您可以随时查看手册页(man find)。在这种情况下,-exec告诉find执行以;(或+)终止的以下命令,其中{}将替换为正在处理的文件的名称(或者,如果+使用了该条件的所有文件)。
凯文

3
@phk -d ''比更好-d $'\0'。后者不仅更长,而且还建议您可以传递包含空字节的参数,但不能。第一个空字节标记字符串的结尾。在bash $'a\0bc'中,它与空字符串相同a并且$'\0'相同,$'\0abc'或者只是空字符串''help read指出“ delim的第一个字符用于终止输入 ”,因此''用作定界符有点麻烦。空字符串中的第一个字符是空字节,始终标记字符串的结尾(即使您未明确写下来)。
索科维

114

无论您做什么,都不要使用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
它不适用于文件名中的换行符。使用查找的-exec代替。
用户未知

2
@userunknown-您是正确的。-exec最安全,因为它根本不使用外壳。但是,文件名中的NL非常少见。文件名中的空格很常见。要点是不要使用for许多海报推荐的循环。
David W.

1
@userunknown-在这里。我已经解决了这个问题,现在它将处理带有换行符,制表符和任何其他空白的文件。该帖子的全部目的是告诉OP不要使用,for file $(find)因为与此相关的问题。
David W.

4
如果可以使用-exec,那就更好了,但是有时候您确实需要将名称返回给shell。例如,如果要删除文件扩展名。
Ben Reser 2014年

5
您应该使用以下-r选项read-r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Daira Hopwood 2015年

102
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接他们一个接一个然后...


3
如果文件名中的换行符失败。
用户未知

2
@user unknown:您是对的,这是我根本没有考虑过的情况,我认为这很奇怪。但是我相应地调整了答案。
0xC0000022L 2012年

5
可能值得指出的是find -print0xargs -0它们都是GNU扩展而不是可移植(POSIX)参数。但是,在具有它们的那些系统上非常有用!
Toby Speight

1
对于包含反斜杠的文件名(read -r将解决)或以空格结尾的文件名(将解决),此操作也将失败IFS= read。因此,BashFAQ#1建议while IFS= read -r filename; do ...
Charles Duffy

1
与此相关的另一个问题是,循环的主体看起来像在同一个外壳中执行,但事实并非如此,例如,exit它将无法按预期工作,并且循环主体中设置的变量在循环之后将不可用。
EM0

17

文件名可以包含空格,甚至可以包含控制字符。在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.使用查找时和过程替换

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将返回搜索模式(“ * .txt”)-因此需要额外的行“如果文件不存在,则继续”。参见Bash手册,文件名扩展
  2. shell选项nullglob可用于避免出现此多余的行。
  3. “如果 failglob设置 shell选项,但没有找到匹配项,则会显示一条错误消息,并且不执行该命令。” (摘自上述Bash手册)
  4. shell选项globstar:“如果设置,则在文件名扩展上下文中使用的模式'**'将匹配所有文件以及零个或多个目录和子目录。如果该模式后接'/',则仅目录和子目录匹配。” 看到 Bash手册,内置Shopt
  5. 其他选项文件名扩展:extglobnocaseglobdotglob及壳可变GLOBIGNORE

在模式2上:

  1. 文件名可以包含空格,制表符,空格,换行符等,以便以安全的方式处理文件名,find-print0使用:使用文件名打印所有控制字符并以NUL终止。另请参见Gnu Findutils手册页,不安全的文件名处理安全的文件名处理文件名中的异常字符。有关此主题的详细讨论,请参见下面的David A. Wheeler。

  2. 有一些可能的模式可以在while循环中处理查找结果。其他人(kevin,David 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始终为“ true”,并且代码将始终回显“未找到文件”。原因是:管道的每个命令都在单独的子Shell中执行,因此循环内的更改变量(单独的子Shell)不会更改主Shell脚本中的变量。这就是为什么我建议使用流程替换作为“更好”,更有用,更通用的模式。
    请参阅我在管道循环中设置变量。它们为何消失...(来自Greg的Bash常见问题解答),以获取有关此主题的详细讨论。

其他参考资料和来源:


8

(已更新,包括@Socowi的卓越速度改进)

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

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

做完了


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

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

1
像糖蜜一样慢(因为它会为每个文件启动一个外壳),但这确实起作用。+1
dawg

1
取而代之的是,\;您可以+用来将尽可能多的文件传递给单个文件exec。然后"$@"在shell脚本中使用以处理所有这些参数。
Socowi,22:36

3
此代码中有一个错误。循环缺少第一个结果。这是因为$@忽略了它,因为它通常是脚本的名称。我们只需要添加dummy之间',并{}因此它可以利用脚本名称的地方,保证所有的比赛都是由循环处理。
BCartolo

如果我需要新创建的shell之外的其他变量怎么办?
Jodo

OTHERVAR=foo find . -na.....应该允许您$OTHERVAR从新创建的Shell中进行访问。
569825

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

3
for x in $(find ...)对于包含空格的任何文件名都会中断。find ... | xargs除非您使用-print0和,否则与相同-0
glenn jackman 2012年

1
使用find . -name "*.txt -exec process_one {} ";"代替。为什么我们应该使用xargs来收集结果?
用户未知,

@userunknown好吧,这一切都取决于什么process_one。如果它是实际命令的占位符,请确保它可以工作(如果您修正了错字并在之后添加了右引号"*.txt)。但是,如果process_one是用户定义的函数,则您的代码将无法工作。
toxalot

@toxalot:是的,但是在脚本中编写要调用的函数并不是问题。
用户未知

4

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


1
数组循环失败,文件名中带有空格。
EM0

您应该删除此答案。它不适用于文件名或目录名称中的空格。
jww

4

如果可以假设文件名不包含换行符,则可以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


这是最好的答案!适用于:*文件名中的空格*没有匹配的文件* exit遍历结果时
EM0

不过,并非所有可能的文件名适用-为此,您应该使用readarray -d '' x < <(find . -name '*.txt' -print0)
Charles Duffy

3

我喜欢使用首先分配给变量的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高利用率)


2

您可以将这样返回的文件名find放入一个数组中:

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

现在,您可以遍历数组以访问单个项目并对其进行任何操作。

注意:这是空白空间。


1
在bash 4.4或更高版本中,您可以使用单个命令来代替循环:mapfile -t -d '' array < <(find ...)IFS不需要设置mapfile
索科维

1

基于其他答案和@phk的注释,使用fd#3 :(
仍然允许在循环内使用stdin)

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

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

-1

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

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


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.