对于Bash中的vs查找


28

遍历文件时,有两种方法:

  1. 使用for-loop:

    for f in *; do
        echo "$f"
    done
    
  2. 用途find

    find * -prune | while read f; do 
        echo "$f"
    done
    

假设这两个循环将找到相同的文件列表,那么这两个选项在性能和处理方面有何不同?


1
为什么?find不会打开找到的文件。我唯一看到的关于大量文件的问题是ARG_MAX
2013年

1
请参阅答案和注释,告诉您read f在读取文件名时会破坏文件名(例如,带有前导空格的名称)。find * -prune似乎也很简单地说“ ls -1是”?
伊恩·艾伦

4
不要假设这两个循环会找到相同的文件集;在大多数情况下,他们不会。另外,应该find .不是find *
Alexis

1
@terdon是的,解析ls -l是一个坏主意。但是解析ls -11不是l)并不比解析更糟糕find * -prune。两者都在名称中带有换行符的文件上失败。
伊恩·艾伦

5
我怀疑我们每个人花了更多的时间在阅读这个问题和答案上,而不是整个脚本生命周期内的性能差异。
mpez0 2013年

Answers:


9

1。

第一个:

for f in *; do
  echo "$f"
done

失败名为文件-n-e并像变种-nene,并与一些bash的部署,包含反斜杠的文件名。

第二:

find * -prune | while read f; do 
  echo "$f"
done

失败的更多的情况下(文件名为!-H-name(,文件名是开始或结束用空格或包含换行符...)

它是可扩展的外壳,除了打印作为参数接收到的文件外*find什么也不做。您也可以使用内置的printf '%s\n'替代方法来printf避免过多的args潜在错误。

2。

的扩展*已排序,如果不需要排序,则可以使其速度更快。在zsh

for f (*(oN)) printf '%s\n' $f

或者简单地:

printf '%s\n' *(oN)

bash据我所知,没有任何等效项,因此您需要诉诸find

3。

find . ! -name . -prune ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

(以上使用GNU / BSD -print0非标准扩展名)。

那仍然涉及产生一个find命令并使用一个慢while read循环,因此for除非文件列表很大,否则它可能比使用循环慢。

4。

另外,与shell通配符扩展相反,find它将lstat对每个文件进行系统调用,因此,非排序不太可能对此进行补偿。

使用GNU / BSD find时,可以通过使用它们的-maxdepth扩展名来避免这种情况,这将触发优化,从而节省了lstat

find . -maxdepth 1 ! -name '.*' -print0 |
  while IFS= read -rd '' f; do
    printf '%s\n' "$f"
  done

因为find找到后即开始输出文件名(stdio输出缓冲除外),所以在循环中进行的操作比较耗时并且文件名列表比stdio缓冲区更多(4,这可能会更快) / 8 kB)。在这种情况下,循环中的处理将在find完成查找所有文件之前开始。在GNU和FreeBSD系统上,您可能会stdbuf导致这种情况更快发生(禁用stdio缓冲)。

5,

POSIX / standard / portable运行每个文件命令的方式find是使用-exec谓词:

find . ! -name . -prune ! -name '.*' -exec some-cmd {} ';'

在这种情况下echo,这效率不如在Shell中循环,因为Shell具有内置版本的echowhile find将需要生成一个新进程并/bin/echo在其中为每个文件执行。

如果需要运行多个命令,则可以执行以下操作:

find . ! -name . -prune ! -name '.*' -exec cmd1 {} ';' -exec cmd2 {} ';'

但是要当心,cmd2只有cmd1成功才能执行。

6。

对每个文件运行复杂命令的一种规范方法是使用以下命令调用Shell -exec ... {} +

find . ! -name . -prune ! -name '.*' -exec sh -c '
  for f do
    cmd1 "$f"
    cmd2 "$f"
  done' sh {} +

到那时,echo由于我们使用了sh内置的,因此我们又恢复了高效,并且-exec +生成的版本sh越少越好。

7。

对ext4上具有200.000个短名称文件的目录的测试中,一个文件zsh(第2段)到目前为止是最快的,其次是第一个简单for i in *循环(尽管与往常一样,bash它比其他shell慢得多)。


!在find命令中做什么?
rubo77

@ rubo77,!是否定的。! -name . -prune more...会做-prune(和more...因为-prune总是返回true)为每个文件,但.。因此,它将more...对中的所有文件执行操作.,但将排除.且不会将其归入的子目录.。因此,这是GNU的标准等效项-mindepth 1 -maxdepth 1
斯特凡Chazelas

18

我在具有2259个条目的目录上进行了尝试,并使用了time命令。

time for f in *; do echo "$f"; done(减去文件!)的输出为:

real    0m0.062s
user    0m0.036s
sys     0m0.012s

time find * -prune | while read f; do echo "$f"; done(减去文件!)的输出为:

real    0m0.131s
user    0m0.056s
sys     0m0.060s

我多次运行每个命令,以消除高速缓存未命中。这表明,将其保留bash(对于i in ...)要比使用find和传递输出(至bash)更快

为了完整起见,我从中删除了管道find,因为在您的示例中,这完全是多余的。just的输出find * -prune是:

real    0m0.053s
user    0m0.016s
sys     0m0.024s

另外,time echo *(输出不是用换行符分隔的,a):

real    0m0.009s
user    0m0.008s
sys     0m0.000s

在这一点上,我怀疑原因echo *是更快,因为它没有输出太多的换行符,因此输出没有滚动太多。让我们测试一下...

time find * -prune | while read f; do echo "$f"; done > /dev/null

产量:

real    0m0.109s
user    0m0.076s
sys     0m0.032s

time find * -prune > /dev/null产量:

real    0m0.027s
user    0m0.008s
sys     0m0.012s

time for f in *; do echo "$f"; done > /dev/null产量:

real    0m0.040s
user    0m0.036s
sys     0m0.004s

最后:time echo * > /dev/null产量:

real    0m0.011s
user    0m0.012s
sys     0m0.000s

某些变化可以由随机因素解释,但似乎很清楚:

  • 输出缓慢
  • 管道成本有点高
  • for f in *; do ...本身比慢find * -prune,但对于上述涉及管道的结构,速度更快。

此外,顺便说一句,这两种方法似乎都可以使用空格来处理名称。

编辑:

find . -maxdepth 1 > /dev/nullvs.的时间find * -prune > /dev/null

time find . -maxdepth 1 > /dev/null

real    0m0.018s
user    0m0.008s
sys     0m0.008s

find * -prune > /dev/null

real    0m0.031s
user    0m0.020s
sys     0m0.008s

因此,附加结论:

  • find * -prune慢于find . -maxdepth 1-在前一种情况下,shell处理一个glob,然后为构建一个(大)命令行find。注意:find . -prune只返回.

更多测试 time find . -maxdepth 1 -exec echo {} \; >/dev/null

real    0m3.389s
user    0m0.040s
sys     0m0.412s

结论:

  • 迄今为止最慢的方法。正如在建议使用此方法的答案的注释中所指出的那样,每个参数都产生一个shell。

哪条管道是多余的?您可以显示不使用管道的线路吗?
rubo77

2
@ rubo77 find * -prune | while read f; do echo "$f"; done具有冗余管道-管道所做的全部工作就是完全find输出自己的输出。如果没有管道,那将是简单的find * -prune 。管道仅是多余的,特别是因为管道另一侧的东西只是将stdin复制到stdout(大部分情况下)。这是昂贵的无人操作。如果要使用find的输出进行处理,而不仅仅是再次将其吐回去,那就不一样了。
Phil

可能主要的耗时是*。作为BitsOfNix说:我还是强烈建议不要使用*,并.用于find代替。
rubo77

@ rubo77似乎是这样。我想我忽略了这一点。我已经为系统添加了发现。我认为find . -prune速度更快,因为find它将逐字读取目录条目,而Shell也会这样做,可能会与glob匹配(可能针对优化*),然后为构建大型命令行find
Phil

1
find . -prune.在我的系统上打印。它几乎没有任何作用。它与find * -prune显示当前目录中的所有名称完全不同。裸露的read f文件名将带有前导空格。
伊恩·艾伦

10

尽管可以将您的发现更改为以下内容,但我肯定会使用find:

find . -maxdepth 1 -exec echo {} \;

在性能方面,find当然要快得多,具体取决于您的需求。您当前所拥有的for将仅显示当前目录中的文件/目录,而不显示目录内容。如果使用find,它还将显示子目录的内容。

我说,发现是因为有更好的你for*将不得不首先被膨胀和我怕,如果你有一个目录与一个巨大的文件的数量可能会给错误过长参数列表。一样find *

例如,在我当前使用的其中一个系统中,有几个目录包含超过200万个文件(每个文件<100k):

find *
-bash: /usr/bin/find: Argument list too long

我添加-prune了使两个示例更相似。而且我更喜欢使用while管道,因此更容易在循环中应用更多命令
rubo77


从我的POV来看,更改硬限制几乎不是适当的解决方法。特别是在谈论2+百万个文件时。在没有脱离问题的情况下,对于简单情况,作为一级目录,速度更快,但是如果更改文件/目录结构,将很难迁移。使用find时,它有大量的选择,您可以更好地做好准备。不过,我仍然强烈建议不要使用*和。寻找代替。它比*可能更难携带,因为您可能无法控制硬限制...
BitsOfNix

4
这将为每个文件生成一个回显进程(在shell循环中,它是将使用的内置回显,而不会产生额外的进程),并且将下降到目录中,因此它会很多。另请注意,它将包括点文件。
斯特凡Chazelas

没错,我添加了maxdepth 1,使其仅保持当前水平。
BitsOfNix

7
find * -prune | while read f; do 
    echo "$f"
done

是无用的find-您所说的实际上是“对于目录(*)中的每个文件,找不到任何文件。而且,出于以下几个原因,它也不安全:

  • 路径中的反斜杠在没有-r选项的情况下会被特殊对待read。这不是for循环的问题。
  • 路径中的换行符会破坏循环中所有重要的功能。这不是for循环的问题。

使用来处理任何文件名find都很困难,因此for,仅出于该原因,就应尽可能使用loop选项。另外,find通常运行外部程序(如)要比运行内部循环命令(如)要慢for


@ I0b0关于find -path'./*'-prune或find -path'./[^.]*'-prune(以避免隐藏的文件和目录)作为更好的构造-完整格式:find -path' ./*'-修剪-print0 | xargs -0 sh -c'...'?
AsymLabs 2013年

1
find' -print0xargs' 都不-0是POSIX兼容的,您不能放入任意命令sh -c ' ... '(单引号不能在单引号内转义),所以它不是那么简单。
13年

4

但是,我们是性能问题的傻瓜!此实验要求至少做出两个假设,使其并非十分有效。

答:假设他们找到相同的文件…

好的,他们首先找到相同的文件,因为它们都在同一个glob上进行迭代*。但是存在find * -prune | while read f一些缺陷,这些缺陷很可能无法找到您期望的所有文件:

  1. 不能保证POSIX find接受多个路径参数。大多数find实现都可以,但是仍然不应依赖于此。
  2. find *击中时会断裂ARG_MAXfor f in *不会,因为ARG_MAX适用于exec,而不是内置函数。
  3. while read f可以使用以空格开头和结尾的文件名中断,这将被剥离。您可以使用while read及其默认参数来克服此问题REPLY,但是对于其中包含换行符的文件名,这仍然无济于事。

B. echo。没有人会这样做只是回显文件名。如果需要,请执行以下一项操作:

printf '%s\n' *
find . -mindepth 1 -maxdepth 1 # for dotted names, too

while循环的管道在此处创建了一个隐式子外壳,该子外壳在循环结束时关闭,这对于某些人可能并不直观。

为了回答这个问题,这是我的目录中包含184个文件和目录的结果。

$ time bash -c 'for i in {0..1000}; do find * -prune | while read f; do echo "$f"; done >/dev/null; done'

real    0m7.998s
user    0m5.204s
sys 0m2.996s
$ time bash -c 'for i in {0..1000}; do for f in *; do echo "$f"; done >/dev/null; done'

real    0m2.734s
user    0m2.553s
sys 0m0.181s
$ time bash -c 'for i in {0..1000}; do printf '%s\n' * > /dev/null; done'

real    0m1.468s
user    0m1.401s
sys 0m0.067s

$ time bash -c 'for i in {0..1000}; do find . -mindepth 1 -maxdepth 1 >/dev/null; done '

real    0m1.946s
user    0m0.847s
sys 0m0.933s

我不同意该声明while循环产卵子shell -在最坏的情况下,一个新的线程:以下是尝试之前和之后显示,道歉为穷人格式$ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20811 pts/1 R+ 0:00 grep bash $ while true; do while true; do while true; do while true; do while true; do sleep 100; done; done; done; done; done ^Z [1]+ Stopped sleep 100 $ bg [1]+ sleep 100 & $ ps ax | grep bash 20784 pts/1 Ss 0:00 -bash 20924 pts/1 S+ 0:00 grep bash
菲尔-

从技术上讲,我搞错了:管道导致隐式子外壳,而不是while循环。我会编辑。
2013年

2

find *如果*生成的令牌看起来像谓词而不是路径,则将无法正常工作。

您不能使用通常的--参数来解决此问题,因为它--指示选项的结尾,而find的选项位于路径之前。

要解决此问题,您可以find ./*改用。但是,它不会产生与完全相同的字符串for x in *

请注意,find ./* -prune | while read f ..它实际上并未使用的扫描功能find./*实际上是遍历目录并生成名称的通配语法。然后,该find程序将必须至少stat对这些名称中的每个名称执行一次检查。您需要启动程序并使其访问这些文件,然后执行I / O读取其输出的开销。

很难想象,除了效率不高外,还有什么其他办法for x in ./* ...


1

对于初学者来说,for是一个内置在Bash中的shell关键字,而这find是一个单独的可执行文件。

$ type -a for
for is a shell keyword

$ type -a find
find is /usr/bin/find

for循环只能找到从globstar字符的文件时,它的扩大,它不会递归到它发现的任何目录。

另一方面,find也将提供由globstar扩展的列表,但是它将递归地查找此扩展列表下面的所有文件和目录,并将每个文件和目录通过管道传递到while循环中。

从它们不处理包含空格的路径或文件名的意义上来说,这两种方法都可能被认为是危险的。

这就是我可以想到的关于这两种方法的全部内容。


我在查找命令中添加了-prune,因此它们更加相似。
rubo77

0

如果find所返回的所有文件都可以由一个命令处理(显然不适用于上面的echo示例),则可以使用xargs:

find * |xargs some-command

0

多年来我一直在使用它:

find . -name 'filename'|xargs grep 'pattern'|more

查找某些文件(例如* .txt),该文件包含grep可以查找的模式并将其通过管道传输到更多文件中,从而不会滚动到屏幕之外。有时,我使用>>管道将结果写入另一个文件,以便以后查看。

以下是结果示例:-

./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:In-Reply-To: <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:  <A165CE5C-61C5-4794-8651-66F5678ABCBF@usit.net>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2008-August.txt:Message-ID: <448E53556A3F442ABC58203D6281923E@hypermax>
./Documents/Organ_docos/Rodgerstrio321A/rodgersmylist/2011-April.txt:URL: http://mylist.net/private/rodgersorganusers/attachments/20110420/3f
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.