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

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

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

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

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

all_lines except (pattern_matching_lines  U (B lines_before) U (A lines_after))

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

aaa
bbb
ccc
hhh
iii

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


请注意,当我尝试时:

grep -v 'fff'  -A1 -B2 file.txt

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

aaa
bbb
ccc
ddd
eee
fff
--
--
fff
ggg
hhh
iii

答案1

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

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

grep这也应该适用于传递给via的模式文件,-f例如:

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

我认为,如果它将任何三个或更多连续行号折叠到范围中,以便使用 例如2,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-

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-

答案2

在大多数情况下,不这样做可能会更好,但以防万一文件是真的大,你无法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'

这是所谓的一个例子滑动窗口在输入上。它的工作原理是建立一个展望$B在尝试打印任何内容之前,先缓冲-count 行。

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

所以这是工作流程:

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

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

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

^1\n2\n3$

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

^2\n3$

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

^2\n3\n4$

因此,当我们5在输入中找到第一次出现时,模式空间实际上如下所示:

^3\n4\n5$

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

^5$

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

这是一个运行示例:

A=8 B=7 match='[24689]0'
seq 100 |
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'

打印:

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

答案3

如果您不介意使用vim

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

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

答案4

您可以通过使用临时文件获得足够好的结果:

my_file=file.txt #or =$1 if in a script

#create a file with all the lines to discard, numbered
grep -n -B1 -A5 TBD "$my_file" |cut -d\  -f1|tr -d ':-'|sort > /tmp/___"$my_file"_unpair

#number all the lines
nl -nln "$my_file"|cut -d\  -f1|tr -d ':-'|sort >  /tmp/___"$my_file"_all

#join the two, creating a file with the numbers of all the lines to keep
#i.e. of those _not_ found in the "unpair" file
join -v2  /tmp/___"$my_file"_unpair /tmp/___"$my_file"_all|sort -n > /tmp/___"$my_file"_lines_to_keep

#eventually use these line numbers to extract lines from the original file
nl -nln $my_file|join - /tmp/___"$my_file"_lines_to_keep |cut -d\  -f2- > "$my_file"_clean

结果是够好了因为您可能会在此过程中丢失一些缩进,但如果它是 xml 或缩进不敏感的文件,那么它应该不是问题。由于此脚本使用 RAM 驱动器,因此写入和读取这些临时文件与在内存中工作一样快。

相关内容