如何使用grep命令搜索大量文件?

11

我正在尝试在当前目录下使用grep查找4万个文件,但是我收到了这个错误提示。

for i in $(cat A01/genes.txt); do grep $i *.kaks; done > A01/A01.result.txt
-bash: /usr/bin/grep: Argument list too long

如何通常使用grep搜索数千个文件?

谢谢 Upendra


1
我认为你应该使用 find - keyser
5个回答

31

这让David很难过...

到目前为止,每个人都错了(除了anubhava)。

Shell脚本不像其他编程语言,因为很多行的解释来自于shell在命令实际执行之前对它们进行插值的能力。

我们来看一个简单的例子:

$ set -x
$ ls
+ ls
bar.txt foo.txt fubar.log
$ echo The text files are *.txt
echo The text files are *.txt
> echo The text files are bar.txt foo.txt
The text files are bar.txt foo.txt
$ set +x
$

set -x命令可以让你查看shell实际插值的通配符,并将其作为输入传递给命令。 >指向实际被命令执行的行。

你可以看到echo命令没有解释*。相反,shell获取了*并用匹配文件的名称替换它。只有在这之后,echo命令才会真正执行该命令。

当你有超过40K个文件时,并且你执行grep *命令时,在grep有机会执行之前,你已经将该*扩展为这40,000多个文件的名称,这就是错误消息/usr/bin/grep: Argument list too long的来自之处。

幸运的是,Unix有一种解决这个困境的方法:

$ find . -name "*.kaks" -type f -maxdepth 1 | xargs grep -f A01/genes.txt

find . -name "*.kaks" -type f -maxdepth 1会找到所有*.kaks文件,-depth 1 只会包括当前目录下的文件。-type f 确保您只选择文件而不是目录。

find 命令将文件名传递给 xargsxargs 将文件名追加到 grep -f A01/genes.txt 命令中。但是,xargs 有一个技巧,它知道命令行缓冲区的长度,当命令行缓冲区已满时,它会执行 grep,然后传入另一系列文件给 grep。这样,grep 可能会执行三次或十次(取决于命令行缓冲区的大小),并且所有文件都会被使用。

不幸的是,xargs 使用空格作为文件名的分隔符。如果您的文件名包含空格或制表符,那么在使用 xargs 时就会出问题。幸运的是,还有另一种解决方法:

$ find . -name "*.kaks" -type f -maxdepth 1 -print0 | xargs -0 grep -f A01/genes.txt

-print0参数会导致find输出文件名,不是用换行符,而是用NUL字符分隔。对于xargs命令,-0参数告诉xargs文件分隔符不是空格,而是NUL字符。因此,问题得以解决。

你也可以这样做:

$ find . -name "*.kaks" -type f -maxdepth 1 -exec grep -f A01/genes.txt {} \;

这将执行每个找到的文件的 grep ,而不是像 xargs 那样仅对可以放在命令行上运行的所有文件运行 grep。这样做的好处是完全避免了 shell 的干扰。但是,它可能会更或者更少有效。

有趣的是进行实验并查看哪种方法更有效。您可以使用 time 查看:

$ time find . -name "*.kaks" -type f -maxdepth 1 -exec grep -f A01/genes.txt {} \;

这将执行命令,然后告诉您执行的时间。请尝试使用-execxargs,并查看哪个更快。让我们知道您的发现。


1
向您致敬,您花费了大量时间来解释这个问题。 - anubhava
1
是的,在我还在思考的时候,你已经给出了正确的答案。就在我发布之前,我不得不对我的“每个人都错了”的说法进行快速编辑。 - David W.
@David,非常感谢您详细的解释。我正在运行这两种方法并计时,完成后会尽快更新... - upendra
1
@DavidW。如果您关心速度,那么至少在支持它的系统上,你应该使用 -exec command {} + 形式,不是吗? - John1024
@John1024 谢谢你让我知道了 find 命令中 + 的用法。我以前从没见过这种写法。是的,根据 man 手册:与 -exec 相同,但每次调用实用程序时,“{}”将被尽可能多的路径名替换。此行为类似于 xargs(1)。。这就是当你学习20年前的东西,而没有跟上变化的结果。 - David W.
显示剩余4条评论

7
您可以像这样结合findgrep
find . -maxdepth 1 -name '*.kaks' -exec grep -H -f A01/genes.txt '{}' \; > A01/A01.result.txt

1
这个答案很好地解决了两个潜在的问题:它避免了(a)在命令行上扩展cat A01/genes.txt,并且避免了(b)在命令行上扩展*.kaks - John1024

0

你可以使用 grep 的递归特性:

for i in $(cat A01/genes.txt); do 
    grep -r $i .
done > A01/A01.result.txt

如果你只想选择kaks文件:

for i in $(cat A01/genes.txt); do 
    find . -iregex '.*\.kaks$' -exec grep $i \;
done > A01/A01.result.txt

0
在你的外部循环中再嵌套一个for循环:
for f in *.kaks; do
   grep -H  $i "$f"
done

顺便问一下,您是想在每个文件中找到每个匹配项,还是仅仅知道搜索字符串是否存在于其中一次或多次?如果只需要知道字符串出现了一次或多次,那么您可以在grep命令中指定“-n 1”,它会在找到第一个匹配项后停止读取/搜索文件的其余部分,这可能会节省大量时间。

我使用了@David建议的方法,它运行良好。感谢提供的替代方案。 - upendra

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接