如何使用grep跨多行找到模式?

我想找到有“abc”和“efg”的文件,这两个字符串在该文件中的不同行。一个包含以下内容的文件:

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

应该匹配。

359895 次浏览

我不知道如何用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/ case等中添加一个If。

遗憾的是,你不能。来自grep文档:

grep搜索已命名的输入FILEs(或标准输入,如果没有文件被命名,或如果一个连字符减号(-)被作为文件名),以查找包含与给定PATTERN匹配的

如果可以使用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

我不确定是否可以使用grep,但sed使它非常简单:

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

Grep是这种操作的笨拙工具。

pcregrep在大多数现代Linux系统中都可以被用作

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

哪里-M--multiline允许模式匹配多行

还有一个更新的pcre2grep。两者都由PCRE项目. xml提供。

pcre2grep可用于Mac OS X,通过Mac港口作为端口pcre2的一部分:

% sudo port install pcre2

和通过家酿作为:

% brew install pcre

或者pcre2

% brew install pcre2

pcre2grep是Linux上的也可用 (Ubuntu 18.04+)

$ sudo apt install pcre2-utils # PCRE2
$ sudo apt install pcregrep    # Older PCRE
#!/bin/bash
shopt -s nullglob
for file in *
do
r=$(awk '/abc/{f=1}/efg/{g=1;exit}END{print g&&f ?1:0}' file)
if [ "$r" -eq 1 ];then
echo "Found pattern in $file"
else
echo "not found"
fi
done

虽然sed选项是最简单、最简单的,但遗憾的是,LJ的一行程序并不是最可移植的。那些受困于C Shell(而不是bash)版本的人将需要摆脱他们的刘海:

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

不幸的是,这一行在bash等中不起作用。

如果您对模式序列不感兴趣,可以使用grep。

grep -l "pattern1" filepattern*.* | xargs grep "pattern2"

例子

grep -l "vector" *.cpp | xargs grep "map"

grep -l将找到所有与第一个模式匹配的文件,xargs将为第二个模式grep。希望这能有所帮助。

awk一行程序:

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

作为Balu Mohan的答案的替代方案,可以只使用grepheadtail来强制模式的顺序:

for f in FILEGLOB; do tail $f -n +$(grep -n "pattern1" $f | head -n1 | cut -d : -f 1) 2>/dev/null | grep "pattern2" &>/dev/null && echo $f; done

不过,这个不太漂亮。格式化得更容易读:

for f in FILEGLOB; do
tail $f -n +$(grep -n "pattern1" $f | head -n1 | cut -d : -f 1) 2>/dev/null \
| grep -q "pattern2" \
&& echo $f
done

这将打印所有"pattern2"出现在"pattern1"或者两者出现在同一行之后的文件名称:

$ echo "abc
def" > a.txt
$ echo "def
abc" > b.txt
$ echo "abcdef" > c.txt; echo "defabc" > d.txt
$ for f in *.txt; do tail $f -n +$(grep -n "abc" $f | head -n1 | cut -d : -f 1) 2>/dev/null | grep -q "def" && echo $f; done
a.txt
c.txt
d.txt

解释

  • tail -n +i -打印__abc1之后的所有行,包括
  • grep -n -在匹配的行前加上行号
  • head -n1 -只打印第一行
  • cut -d : -f 1 -使用:作为分隔符打印第一个切割列
  • 2>/dev/null - silence如果$()表达式返回空,则出现的tail错误输出
  • grep -q -静默grep,如果找到匹配则立即返回,因为我们只对退出码感兴趣

如果您愿意使用上下文,这可以通过输入来实现

grep -A 500 abc test.txt | grep -B 500 efg

这将显示之间的 "abc"和"efg"的所有内容,只要它们之间的距离在500行以内。

下面是一个受这个答案启发的解决方案:

  • 如果'abc'和'efg'可以在同一行:

      grep -zl 'abc.*efg' <your list of files>
    
  • 如果'abc'和'efg'必须在不同的行:

      grep -Pzl '(?s)abc.*\n.*efg' <your list of files>
    

参数:

  • 使用perl兼容的正则表达式(PCRE)。

  • -z将输入视为一组行,每一行以0字节结束,而不是换行符。例如,grep将输入视为一行。注意,如果你不使用-l,它将显示匹配后的NUL字符,见注释。

  • -l只列出匹配的文件名。

  • (?s)激活PCRE_DOTALL,这意味着'。'查找任何字符或换行符。

sed应该足够了,就像海报LJ上面说的,

而不是!d,你可以简单地使用p打印:

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

银色的搜索器:

ag 'abc.*(\n|.)*efg' your_filename

与戒指持有者的答案相似,但用ag代替。银色搜索者的速度优势可能在这里大放异彩。

我在几天前发布了一个grep替代方案,它直接支持这一点,通过多行匹配或使用条件——希望它对搜索这里的人有用。下面是示例命令的样子:

多行:

sift -lm 'abc.*efg' testfile

条件:

sift -l 'abc' testfile --followed-by 'efg'

你也可以指定'efg'必须在一定的行数内跟在'abc'后面:

sift -l 'abc' testfile --followed-within 5:'efg'

你可以在sift-tool.org上找到更多信息。

如果你需要两个单词彼此接近,例如不超过3行,你可以这样做:

find . -exec grep -Hn -C 3 "abc" {} \; | grep -C 3 "efg"

同样的例子,但是只过滤*.txt文件:

find . -name *.txt -exec grep -Hn -C 3 "abc" {} \; | grep -C 3 "efg"

你也可以用egrep命令替换grep命令,如果你想找到正则表达式。

我非常依赖于pcregrep,但是对于更新的grep,您不需要安装它的许多特性。只需使用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。

这个也能用吗?!

perl -lpne 'print $ARGV if /abc.*?efg/s' file_list

$ARGV包含从file_list读取当前文件时的文件名 /s修饰符跨换行搜索。< / p >

这很容易做到,首先使用tr将换行符替换为其他字符:

tr '\n' '\a' | grep -o 'abc.*def' | tr '\a' '\n'
这里,我使用警报字符\a (ASCII 7)来代替换行符。 这几乎从未在你的文本中找到,并且grep可以将其与.匹配,或者专门与\a匹配

这应该可以工作:

cat FILE | egrep 'abc|efg'

如果有多个匹配项,可以使用grep -v过滤掉

文件模式*.sh对于防止目录被检查很重要。当然,一些测试也可以防止这种情况发生。

for f in *.sh
do
a=$( grep -n -m1 abc $f )
test -n "${a}" && z=$( grep -n efg $f | tail -n 1) || continue
(( ((${z/:*/}-${a/:*/})) > 0 )) && echo $f
done

grep -n -m1 abc $f

搜索最大匹配1并返回(-n)行数。 如果找到一个匹配(test -n…),找到efg的最后一个匹配(找到所有,最后一个尾部为-n 1).

z=$( grep -n efg $f | tail -n 1)

其他的继续。

由于结果是类似18:foofile.sh String alf="abc";的东西,我们需要从“:”截断到行尾。

((${z/:*/}-${a/:*/}))

如果第二个表达式的最后一个匹配超过了第一个表达式的第一个匹配,则应返回正结果。

然后我们报告文件名echo $f

我用它从一个multi fasta文件中提取一个fasta序列,使用grep的-P选项:

grep -Pzo ">tig00000034[^>]+"  file.fasta > desired_sequence.fasta
  • 基于perl的搜索
  • Z表示行以0字节结尾,而不是换行字符
  • O来捕获匹配的内容,因为grep返回整行(在本例中,因为您做了-z是整个文件)。

regexp的核心是[^>],它翻译为“not The greater than symbol”。

如果你对你要找的两个字符串'abc'和'efg'之间的距离有一些估计,你可以使用:

grep -r . -e 'abc' -A num1 -B num2 | grep 'efg'
这样,第一个grep将返回'abc' + #num1行,后面是#num2行,第二个grep将筛选所有这些以获得'efg'。 然后你就会知道它们在哪些文件中同时出现

下面是一种连续使用两个grep的方法:

egrep -o 'abc|efg' $file | grep -A1 abc | grep efg | wc -l

返回0或正整数。

egrep -o(只显示匹配,技巧:同一行上的多个匹配会产生多行输出,就好像它们在不同的行上一样)

  • grep -A1 abc(打印abc及其后面的行)

  • grep efg | wc -l(在abc之后找到的相同或后面的efg行数为0-n,结果可用于'if")

  • 如果需要模式匹配,grep可以更改为egrep等

随着几个月前发布的ugrep:

ugrep 'abc(\n|.)+?efg'

这个工具是高度优化的速度。它也是GNU/BSD/PCRE-grep兼容的。

注意,我们应该使用惰性重复+?,除非你想将所有带有efg的行匹配在一起,直到文件中的最后一个efg

在所有文件中递归搜索(在每个文件中的多行中),同时存在两个字符串(即string1和string2在不同的行中,并且都存在于同一个文件中):

grep -r -l 'string1' * > tmp; while read p; do grep -l 'string2' $p; done < tmp; rm tmp

在所有文件中递归搜索(在每个文件中的多行中),使用EITHER字符串存在(即string1和string2在不同的行中,并且在同一个文件中存在):

grep -r -l 'string1\|string2' *

你至少有几个选择

  1. DOTALL方法
  • 用(?s) DOTALL the。包含\n的字符
  • 你也可以使用一个超前(?=\n)——不会在匹配中被捕获

example-text:

true
match me


false
match me one


false
match me two


true
match me three
third line!!
{BLANK_LINE}


命令:

grep -Pozi '(?s)true.+?\n(?=\n)' example-text

-p用于perl正则表达式

-o只匹配模式,而不是整行

-z允许换行

-i不区分大小写

输出:

true
match me
true
match me three
third line!!

注:

- +? makes modifier non-greedy so matches shortest string instead of largest (prevents from returning one match containing entire text)
  1. 你可以使用老式的O.G.手动方法,使用\n

命令:

grep -Pozi 'true(.|\n)+?\n(?=\n)'

输出:

true
match me
true
match me three
third line!!

使用ripgrep可以:

$ rg --multiline 'abc(\n|.)+?efg' test.txt
3:blah abc blah
4:blah abc blah
5:blah blah..
6:blah blah..
7:blah blah..
8:blah efg blah blah

或者其他咒语。

如果你想让.作为换行符:

$ rg --multiline '(?s)abc.+?efg' test.txt
3:blah abc blah
4:blah abc blah
5:blah blah..
6:blah blah..
7:blah blah..
8:blah efg blah blah

或者与(?s)等价的是rg --multiline --multiline-dotall

为了回答最初的问题,它们必须在不同的行上:

$ rg --multiline 'abc.*[\n](\n|.)*efg' test.txt

如果你想要“不贪婪”;所以你不只是得到第一个ABC和最后一个efg(把它们分成一对):

$ rg --multiline 'abc.*[\n](\n|.)*?efg' test.txt

https://til.hashrocket.com/posts/9zneks2cbv-multiline-matches-with-ripgrep-rg