如何grep反向匹配并排除“之前”和“之后”行

Ame*_*ina 36 grep sed awk text-processing

考虑一个包含以下条目的文本文件:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii
Run Code Online (Sandbox Code Playgroud)

给定一个模式(例如fff),我想对上面的文件进行 grep 以获取输出:

all_lines except (pattern_matching_lines  U (B lines_before) U (A lines_after))
Run Code Online (Sandbox Code Playgroud)

例如,如果B = 2A = 1,带有模式 = 的输出fff应该是:

aaa
bbb
ccc
hhh
iii
Run Code Online (Sandbox Code Playgroud)

如何使用 grep 或其他命令行工具执行此操作?


请注意,当我尝试时:

grep -v 'fff'  -A1 -B2 file.txt
Run Code Online (Sandbox Code Playgroud)

我没有得到我想要的。我反而得到:

aaa
bbb
ccc
ddd
eee
fff
--
--
fff
ggg
hhh
iii
Run Code Online (Sandbox Code Playgroud)

don*_*sti 13

您可以使用gnu grepwith-A-B精确打印要排除的文件部分,但添加-n开关以打印行号,然后格式化输出并将其作为命令脚本传递sed以删除这些行:

grep -n -A1 -B2 PATTERN infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile
Run Code Online (Sandbox Code Playgroud)

这也适用于grep通过以下方式传递的模式文件-f

grep -n -A1 -B2 -f patterns infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile
Run Code Online (Sandbox Code Playgroud)

我认为这可以稍微优化,如果它将任何三个或更多连续行号折叠到范围内,以便使用 eg2,6d而不是2d;3d;4d;5d;6d...,但如果输入只有几个匹配项,则不值得这样做。


其他不保留行顺序并且很可能更慢的方法:
使用comm

comm -13 <(grep PATTERN -A1 -B2 <(nl -ba -nrz -s: infile) | sort) \
<(nl -ba -nrz -s: infile | sort) | cut -d: -f2-
Run Code Online (Sandbox Code Playgroud)

comm需要排序的输入,这意味着行顺序不会保留在最终输出中(除非您的文件已经排序),因此nl用于在排序前对行进行编号,comm -13仅打印第二个文件独有的行,然后cut删除添加的部分nl(即第一个字段和分隔符:
join

join -t: -j1 -v1 <(nl -ba -nrz -s:  infile | sort) \
<(grep PATTERN -A1 -B2 <(nl -ba -nrz -s:  infile) | sort) | cut -d: -f2-
Run Code Online (Sandbox Code Playgroud)


mik*_*erv 11

在大多数情况下,don's 可能会更好,但以防万一文件真的很大,并且您无法sed处理那么大的脚本文件(这可能发生在大约 5000 行以上的脚本),这里是普通的sed

sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'
Run Code Online (Sandbox Code Playgroud)

这是所谓的输入滑动窗口的示例。它通过建立一个前瞻缓冲区$B曾经试图打印任何东西之前-count线。

实际上,也许我应该澄清我之前的观点:此解决方案和不解决方案的主要性能限制因素将与间隔直接相关。这个解决方案会随着更大的间隔大小而变慢,而不要随着更大的间隔频率而变慢。换句话说,即使输入文件非常大,如果实际间隔出现的频率仍然非常低,那么他的解决方案可能是要走的路。但是,如果间隔大小相对易于管理,并且可能经常发生,那么这就是您应该选择的解决方案。

所以这是工作流程:

  • 如果$match在模式空间中以\newline 开头,sed则将递归D删除\n它之前的每个ewline。
    • $match之前完全清除了的模式空间 - 但为了轻松处理重叠,留下一个里程碑似乎效果更好。
    • 我还尝试s/.*\n.*\($match\)/\1/一次性完成并避开循环,但是当$A/$B很大时,Delete 循环证明要快得多。
  • 然后我们将N输入的ext 行拉入以\newline 分隔符开头,并再次尝试通过引用我们最近使用的正则表达式 w/再次D删除 a 。/\n.*$match///
  • 如果模式空间匹配,$match那么它只能$match在行的开头这样做- 所有前$B行都已被清除。
    • 所以我们开始循环$A
    • 这个循环的每次运行,我们将尝试s///&自己替换模式空间中的$Ath \newline 字符,如果成功,test 会将我们 - 以及我们的整个后$A缓冲区 - 完全从脚本中分出,以从顶部开始脚本与下一个输入行(如果有)。
    • 如果test 不成功,我们将b返回到:top 标签并递归输入另一行 - 如果$match在收集$After时发生,可能会重新开始循环。
  • 如果我们通过了一个$match函数循环,那么我们将尝试p打印$最后一行(如果是),如果!不是,则尝试s///&自己替换模式空间中的$Bth \newline 字符。
    • 我们也会t对此进行测试,如果成功,我们将分支到:Print 标签。
    • 如果不是,我们将分支回到:top 并在缓冲区中添加另一个输入行。
  • 如果我们让它:Print,我们将Print 然后Delete 到\n模式空间中的第一个ewline 并从顶部重新运行脚本并保留剩余的部分。

所以这一次,如果我们在做 A=2 B=2 match=5; seq 5 | sed...

:Print第一次迭代的模式空间如下所示:

^1\n2\n3$
Run Code Online (Sandbox Code Playgroud)

这就是如何sed收集它的前$B缓冲区。因此sed打印到它收集的输入后面的输出$B-count 行。这意味着,鉴于我们之前的示例,将rint输出,然后删除它并将其发送回脚本顶部的模式空间,如下所示:sedP1D

^2\n3$
Run Code Online (Sandbox Code Playgroud)

...并在脚本的顶部N检索 ext 输入行,因此下一次迭代如下所示:

^2\n3\n4$
Run Code Online (Sandbox Code Playgroud)

因此,当我们5在输入中找到第一次出现时,模式空间实际上是这样的:

^3\n4\n5$
Run Code Online (Sandbox Code Playgroud)

然后Delete 循环开始,当它通过时,它看起来像:

^5$
Run Code Online (Sandbox Code Playgroud)

Next 输入线被拉动时,会sed碰到 EOF 并退出。到那时,它只P打印了第 1 行和第 2 行。

这是一个示例运行:

^1\n2\n3$
Run Code Online (Sandbox Code Playgroud)

那打印:

1
2
3
4
5
6
7
8
9
10
11
12
29
30
31
32
49
50
51
52
69
70
71
72
99
100
Run Code Online (Sandbox Code Playgroud)

  • @Amelio - 这将适用于任何大小的流,并且不需要通读文件即可工作。最大的性能因素是`$A` 和/或`$B` 的大小。您将这些数字设置得越大,它就会变得越慢 - 但您可以将它们设置得相当大。 (4认同)

mur*_*uru 9

如果您不介意使用vim

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
Run Code Online (Sandbox Code Playgroud)
  • -Nes打开不兼容的静音 ex 模式。对脚本很有用。
  • +{command}告诉 vim{command}在文件上运行。
  • g/${PAT}/- 在所有匹配的行上/fff/。如果模式包含您不打算以这种方式处理的正则表达式特殊字符,这将变得棘手。
  • .-${B} - 从这一行上方的 1 行开始
  • .+${A}- 到这一行下方的 2 行(参见:he cmdline-ranges这两行)
  • d - 删除行。
  • +w !tee 然后写入标准输出。
  • +q! 退出而不保存更改。

您可以跳过变量并直接使用模式和数字。我使用它们只是为了明确目的。


hee*_*ayl 7

怎么样(使用 GNUgrepbash):

$ grep -vFf - file.txt < <(grep -B2 -A1 'fff' file.txt)
aaa
bbb
ccc
hhh
iii
Run Code Online (Sandbox Code Playgroud)

在这里,我们找到要被 丢弃的行grep -B2 -A1 'fff' file.txt,然后使用它作为输入文件来查找丢弃这些行的所需行。

  • 这将与“kos”(现已删除)的解决方案具有相同的问题,就好像输入文件中存在重复行,其中一些超出范围,而另一些则在该范围内,这会将它们全部删除。另外,在多次出现 _pattern_ 的情况下,如果输入文件中存在类似“--”的行(超出范围),这将删除它们,因为当多于一行时,分隔符“--”出现在“grep”的输出中正在匹配 _pattern_ (后者极不可能,但我想值得一提)。 (2认同)