如何使用grep在多行中查找模式?

276

我想查找那些文件中按顺序包含字符串"abc"和"efg",并且这两个字符串在文件的不同行中出现。例如:一个包含以下内容的文件:

blah blah..
blah blah..
blah abc blah
blah blah..
blah blah..
blah blah..
blah efg blah blah
blah blah..
blah blah..

应该被匹配。


4
可能是如何在文件中搜索多行模式?的重复问题。 - Ciro Santilli OurBigBook.com
1
:) 细想一下,我们的世界随着时间的推移不断变化。因此,可能会有比这更好的线程在某个时候出现。 - ring bearer
30个回答

278

使用grep进行此操作有点棘手。

pcregrep是现代Linux系统中普遍存在的工具,可用作替代。

pcregrep -M  'abc.*(\n|.)*efg' test.txt

使用-M--multiline选项可以使正则表达式匹配多行文本。

还有一个更新的pcre2grep,它们都是由PCRE项目提供的。

通过Mac Ports中的pcre2端口,可以在Mac OS X上获得pcre2grep:

% sudo port install pcre2 

通过 Homebrew 安装:

% brew install pcre

或者针对pcre2

% brew install pcre2

pcre2grep也可在Linux(Ubuntu 18.04+)上获取

$ sudo apt install pcre2-utils # PCRE2
$ sudo apt install pcregrep    # Older PCRE

11
@StevenLu -M, --multiline - 允许模式匹配多行文本。 - ring bearer
7
请注意,".(\n|.)"与"(\n|.)*"等价,且后者更短。此外,在我的系统上,当我运行较长的版本时会出现“pcre_exec()错误-8”的错误。因此,请尝试使用"abc(\n|.)*efg"! - daveagp
7
在这种情况下,您需要使表达式变成非贪婪的。例如:'abc.*(\n|.)*?efg' - ring bearer
5
你可以省略第一个 .* -> 'abc(\n|.)*?efg',使正则表达式更短(并且更严谨)。 - Michi
8
pcregrep 确实可以让事情变得更容易,但 grep 也可以使用。例如,请参见 https://dev59.com/mnA65IYBdhLWcg3wogEb#7167115 - Michael Mior
显示剩余11条评论

142
这里有一个受到这个答案启发的解决方案:
  • 如果'abc'和'efg'可以在同一行上:

      grep -zl 'abc.*efg' <你的文件列表>
    
  • 如果'abc'和'efg'必须在不同的行上:

      grep -Pzl '(?s)abc.*\n.*efg' <你的文件列表>
    

参数:

  • -P 使用Perl兼容的正则表达式(PCRE)

  • -z 将输入视为一组以零字节而不是换行符终止的行。即grep将输入视为一行。请注意,如果您不使用-l,它将显示匹配项后跟一个NUL字符,请参阅注释

  • -l 仅列出匹配的文件名

  • (?s) 激活PCRE_DOTALL,这意味着'.'匹配任何字符或换行符


@syntaxerror 不,我认为这只是小写的 l。据我所知,没有 -1 的选项。 - Sparhawk
看来你是对的,也许我在测试时打错了字。无论如何,为引入假线路向你道歉。 - syntaxerror
9
这很棒。我只有一个问题。如果-z选项指定grep将换行符视为“零字节字符”,那么为什么我们需要在正则表达式中使用(?s)?如果它已经不是换行符,那么.应该能够直接匹配它吧? - Durga Swaroop
1
-z(又称--null-data)和(?s)是使用标准grep匹配多行所需的完美选项。苹果电脑用户,请留下评论,告知您的系统是否支持-z或--null-data选项! - Zeke Fast
9
-z 绝对不可在 MacOS 上使用。 - Dylan Nicholson
显示剩余4条评论

127

我不确定是否可以使用grep实现,但是sed非常容易:

sed -e '/abc/,/efg/!d' [file-with-content]

4
这段话的意思是:这个功能不是用于查找文件,而是从单个文件中返回匹配的部分。 - shiggity
15
@Lj. 请问你能解释一下这个命令吗?我熟悉 sed,但从未见过这样的表达式。 - Anthony
3
@Anthony,这在sed的man页面中有记录,在地址一节下面。重要的是要意识到/abc/和/efg/是一个地址。 - Squidly
66
如果这个回答有更多的解释,那么它将会更加有帮助,如果是这样的话,我会再次点赞。我了解一些sed,但不足以在试验半个小时之后使用此答案生成有意义的退出代码。提示:在StackOverflow上,“RTFM”很少会得到赞同,就像你之前的评论所显示的那样。 - Michael Scheper
38
举例快速解释:sed '1,5d' :删除第1到第5行。sed '1,5!d' :删除不在第1到第5行之间的行(即保留这些行)。 然后,您可以使用 /pattern/ 搜索一行,而不是使用数字。请参阅下面更简单的示例:sed -n '/abc/,/efg/p'中的 p 是打印的意思,-n 标志不显示所有行。 - phil_w
显示剩余6条评论

37

就像LJ在上面所说的,sed应该足以满足需求,

不需要使用!d,你可以直接使用p来打印:

sed -n '/abc/,/efg/p' file

27

我曾经非常依赖pcregrep,但是在新版grep中,你无需安装pcregrep就可以使用它的许多功能。只需使用grep -P即可。

对于OP问题的示例,我认为以下选项很好地解决了问题,第二个选项最符合我的理解:

grep -Pzo "abc(.|\n)*efg" /tmp/tes*
grep -Pzl "abc(.|\n)*efg" /tmp/tes*

我将文本复制到 /tmp/test1,并删除了 'g',保存为 /tmp/test2。以下是输出,第一个显示匹配的字符串,第二个仅显示文件名(典型的 -o 是显示匹配,典型的 -l 是仅显示文件名)。请注意,'z' 对于多行是必需的,而 '(.|\n)' 表示匹配除换行符之外的任何内容或换行符 - 即任何内容:

user@host:~$ grep -Pzo "abc(.|\n)*efg" /tmp/tes*
/tmp/test1:abc blah
blah blah..
blah blah..
blah blah..
blah efg
user@host:~$ grep -Pzl "abc(.|\n)*efg" /tmp/tes*
/tmp/test1

要确定您的版本是否足够新,请运行 man grep 并查看是否在顶部附近出现了类似以下内容:

   -P, --perl-regexp
          Interpret  PATTERN  as a Perl regular expression (PCRE, see
          below).  This is highly experimental and grep -P may warn of
          unimplemented features.

这是来自GNU grep 2.10。


19

首先使用tr将换行符替换为其他字符,即可轻松完成此操作:

tr '\n' '\a' | grep -o 'abc.*def' | tr '\a' '\n'
在这里,我使用警报字符\a(ASCII 7)代替换行符。 这在您的文本中几乎不会出现,grep可以用.匹配它,也可以用\a特别匹配它。

2
这是我的方法,但我使用了\0,因此需要使用grep -a并匹配\x00...你帮助我简化了!现在echo $log | tr '\n' '\0' | grep -aoE "Error: .*?\x00Installing .*? has failed\!" | tr '\0' '\n'变成了echo $log | tr '\n' '\a' | grep -oE "Error: .*?\aInstalling .*? has failed\!" | tr '\a' '\n' - Charlie Gorichanaz
1
Use grep -o . - kyb

9

awk一行命令:

awk '/abc/,/efg/' [file-with-content]

6
如果文件中不存在结束模式,或者最后一个结束模式丢失,那么该脚本将愉快地从“abc”打印到文件末尾。您可以修复它,但这将显着增加脚本的复杂性。 - tripleee
如何从输出中排除/efg/ - kyb

8
如果您愿意使用上下文,可以通过输入以下内容实现:
grep -A 500 abc test.txt | grep -B 500 efg

这将显示在距离不超过500行的范围内的“abc”和“efg”之间的所有内容,只要它们之间有内容。


6
如果您能使用Perl,那么您可以轻松地完成这个任务。
perl -ne 'if (/abc/) { $abc = 1; next }; print "Found in $ARGV\n" if ($abc && /efg/); }' yourfilename.txt

您也可以使用单个正则表达式来完成此操作,但这涉及将整个文件内容合并为单个字符串,对于大型文件可能会占用过多内存。为了完整起见,以下是该方法:
perl -e '@lines = <>; $content = join("", @lines); print "Found in $ARGV\n" if ($content =~ /abc.*efg/s);' yourfilename.txt

发现第二个答案非常有用,可以提取具有几行匹配的整个多行块 - 必须使用非贪婪匹配(.*?)以获得最小匹配。 - RichVel

5

我不知道如何使用grep来做到这一点,但是我会使用awk来做类似的事情:

awk '/abc/{ln1=NR} /efg/{ln2=NR} END{if(ln1 && ln2 && ln1 < ln2){print "found"}else{print "not found"}}' foo

不过你需要小心如何操作。你想让正则表达式匹配子字符串还是整个单词?根据情况添加\w标记。此外,虽然这个例子严格符合你的要求,但当abc在efg之后再次出现时,它并不能完全起作用。如果你想处理这个问题,在/abc/的情况下适当添加if语句。


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