如何在sed、AWK或Perl中打印两个模式之间的行,包括或排除这两个模式?

98
我有一个文件,我想打印在两个给定的模式 PAT1PAT2 之间的行。
1
2
PAT1
3    - first block
4
PAT2
5
6
PAT1
7    - second block
PAT2
8
9
PAT1
10    - third block

我已经阅读了如何使用awk / sed选择在多次出现的两个标记模式之间的行,但我很想看到所有可能的组合,包括或排除该模式。

如何打印两个模式之间的所有行?


我正在发布一份规范答案的尝试,以涵盖所有情况,回答如何使用awk/sed选择两个标记模式之间可能多次出现的行。我遵循提问和回答自己的问题是可以的并将答案发布为社区Wiki,所以请随意改进它! - fedorqui
2
@ Cyrus 是的,谢谢!在发布这个问题/答案之前,我也检查了这个。重点是提供一组工具来处理它,因为在我的另一个答案中的评论数量(以及对它们的投票)让我认为一篇通用文章将对未来的读者有所帮助。 - fedorqui
请参阅http://www.thelinuxrain.com/articles/how-to-use-flags-in-awk。 - user2138595
@fedorqui,我没有收到回复,所以我决定尝试改善问题,使其在谷歌上排名更好,并澄清范围。如果您对此不满意,请随时恢复原状。 - Alex Harvey
@Alex,我不确定我的回复应该放在哪里,但无论如何,感谢您的编辑!对我来说看起来很好。感谢您抽出时间处理这个问题。 - fedorqui
9个回答

147

打印 PAT1 和 PAT2 之间的行

$ awk '/PAT1/,/PAT2/' file
PAT1
3    - first block
4
PAT2
PAT1
7    - second block
PAT2
PAT1
10    - third block

或者,使用变量:

awk '/PAT1/{flag=1} flag; /PAT2/{flag=0}' file

这是怎么工作的?

  • /PAT1//PAT2/一样,匹配包含该文本的行。
  • /PAT1/{flag=1}在找到行中的文本PAT1时设置标志flag
  • /PAT2/{flag=0}在找到行中的文本PAT2时取消标志flag
  • flag是一个具有默认操作的模式,即 print $0:如果flag等于1,则打印该行。这样,它将打印从出现PAT1的时间开始到下一个PAT2出现的所有行。这还将打印从最后一个PAT1匹配到文件结尾的所有行。

打印PAT1和PAT2之间的行-不包括PAT1和PAT2

$ awk '/PAT1/{flag=1; next} /PAT2/{flag=0} flag' file
3    - first block
4
7    - second block
10    - third block

使用next跳过包含PAT1的行,以避免打印该行。

通过重新组合块,可以删除对next的调用:awk '/PAT2/{flag=0} flag; /PAT1/{flag=1}' file

打印介于PAT1和PAT2之间的行 - 包括PAT1

$ awk '/PAT1/{flag=1} /PAT2/{flag=0} flag' file
PAT1
3    - first block
4
PAT1
7    - second block
PAT1
10    - third block

flag 放在最后,触发设置在 PAT1 或 PAT2 上的操作:在 PAT1 上打印,不在 PAT2 上打印。

打印 PAT1 和 PAT2 之间的行-包括 PAT2

$ awk 'flag; /PAT1/{flag=1} /PAT2/{flag=0}' file
3    - first block
4
PAT2
7    - second block
PAT2
10    - third block

flag 放置在开头,会触发之前设置的操作,并因此打印出结束模式而不是开始模式。

打印 PAT1 和 PAT2 之间的行 - 如果没有其他 PAT2 出现,则排除从最后一个 PAT1 到文件末尾的行

这基于 Ed Morton 的解决方案

awk 'flag{
        if (/PAT2/)
           {printf "%s", buf; flag=0; buf=""}
        else
            buf = buf $0 ORS
     }
     /PAT1/ {flag=1}' file
作为一行代码:
$ awk 'flag{ if (/PAT2/){printf "%s", buf; flag=0; buf=""} else buf = buf $0 ORS}; /PAT1/{flag=1}' file
3    - first block
4
7    - second block

# note the lack of third block, since no other PAT2 happens after it

该过程将所有选定的行保存在一个缓冲区中,该缓冲区从找到PAT1时开始填充。然后,它将继续使用以下行填充,直到找到PAT2为止。在那一点上,它会打印存储的内容并清空缓冲区。


这是最短匹配吗? - Mukul Anand
@MukulAnand 这取决于具体情况。 - fedorqui
如果只使用awk一次提取PART1和PART2之间的行,该如何实现? - Hemant
我希望有一个解决方案,其中PAT1可能在遇到PAT2之前重复出现,而我们想要的是在最靠近PAT2的PAT1之间非贪婪的内容。即PAT1(一些我们不想要的文本)PAT1(一些我们想要的文本)PAT2。 - levigroker
无法工作: awk: cmd. line:1: /<p>/{flag=1; next}/</p>/{flag=0} flag awk: cmd. line:1: ^ 未终止的正则表达式 - Andrew
显示剩余4条评论

85

那么经典的 sed 解决方案呢?

打印 PAT1 和 PAT2 之间的行 - 包括 PAT1 和 PAT2

sed -n '/PAT1/,/PAT2/p' FILE

打印位于PAT1和PAT2之间的行 - 不包括PAT1和PAT2

GNU sed
sed -n '/PAT1/,/PAT2/{/PAT1/!{/PAT2/!p}}' FILE
sed -n '/PAT1/,/PAT2/{/PAT1/!{/PAT2/!p;};}' FILE

甚至更好(感谢Sundeep):

GNU sed
sed -n '/PAT1/,/PAT2/{//!p}' FILE
sed -n '/PAT1/,/PAT2/{//!p;}' FILE

打印PAT1和PAT2之间的所有行 - 包括PAT1但不包括PAT2

以下仅包含范围开始:

GNU sed
sed -n '/PAT1/,/PAT2/{/PAT2/!p}' FILE
sed -n '/PAT1/,/PAT2/{/PAT2/!p;}' FILE

打印在PAT1和PAT2之间的行 - 包括PAT2但不包括PAT1

以下仅包含范围结束:

GNU sed
sed -n '/PAT1/,/PAT2/{/PAT1/!p}' FILE
sed -n '/PAT1/,/PAT2/{/PAT1/!p;}' FILE

1 关于BSD/Mac OS X的sed说明

像这样的命令:

sed -n '/PAT1/,/PAT2/{/PAT1/!{/PAT2/!p}}' FILE

会出现错误:

▶ sed -n '/PAT1/,/PAT2/{/PAT1/!{/PAT2/!p}}' FILE
sed: 1: "/PAT1/,/PAT2/{/PAT1/!{/ ...": extra characters at the end of p command

因此,为了包括BSD和GNU版本的一行命令,编辑了此答案。


2
嘿,经典版本甚至更短! - David C. Rankin
6
不确定其他版本是否适用,但使用GNU sed时,第一个命令可以简化为sed -n '/PAT1/,/PAT2/{//!p}' file...参考手册中的说明,空正则表达式“//”会重复上一个正则表达式匹配。 - Sundeep
1
@Sundeep 谢谢你的提示。POSIX规定:如果一个正则表达式为空(即没有指定模式),sed应该像最后一个命令中使用的最后一个RE一样行事(无论是作为地址还是替换命令的一部分)。 看起来这里唯一剩下的问题就是如何解释“最后一个RE”。BSD对此有所说明。请看这里(第23点):https://github.com/freebsd/freebsd/blob/master/usr.bin/sed/POSIX - hek2mgl
2
看起来很难找到一个不兼容的版本来证明。 :) - hek2mgl
4
@AlexHarvey,我认为你在这里所做的是一种非常好的善举,通过分享你的知识来改进其他答案。最终,这也是我发布这个问题的目的,以便我们可以拥有一个规范的(又一个:P)来源集。非常感谢! - fedorqui
显示剩余8条评论

13

使用具有PCRE功能(如果可用)的grep打印标记和标记之间的行

$ grep -Pzo "(?s)(PAT1(.*?)(PAT2|\Z))" file
PAT1
3    - first block
4
PAT2
PAT1
7    - second block
PAT2
PAT1
10    - third block
  • -P 使用perl正则表达式(PCRE),不是所有版本的grep都支持
  • -z 将输入视为一组以零字节而非换行符终止的行
  • -o 仅打印匹配项
  • (?s) DotAll,即点号也可以匹配换行符
  • (.*?) 非贪婪查找
  • \Z 仅在字符串末尾或末尾换行符之前匹配

打印标记之间的行(不包括结束标记):

$ grep -Pzo "(?s)(PAT1(.*?)(?=(\nPAT2|\Z)))" file
PAT1
3    - first block
4
PAT1
7    - second block
PAT1
10    - third block
  • (.*?)(?=(\nPAT2|\Z)) 非贪婪匹配,同时使用向前环视查找 \nPAT2\Z

打印标记之间的行但不包括标记本身::

$ grep -Pzo "(?s)((?<=PAT1\n)(.*?)(?=(\nPAT2|\Z)))" file
3    - first block
4
7    - second block
10    - third block
  • (?<=PAT1\n)表示正向后行环视,匹配PAT1\n前面的位置。

打印标记之间的行,但不包括起始标记:

$ grep -Pzo "(?s)((?<=PAT1\n)(.*?)(PAT2|\Z))" file
3    - first block
4
PAT2
7    - second block
PAT2
10    - third block

喜欢这个是因为它可以与正则表达式模式一起使用。而选择的解决方案则不行。 - Phil

9

为了完整起见,这里提供一种Perl解决方案:

打印PAT1和PAT2之间的行 - 包括PAT1和PAT2

perl -ne '/PAT1/../PAT2/ and print' FILE

或者:

perl -ne 'print if /PAT1/../PAT2/' FILE

打印PAT1和PAT2之间的行 - 不包括PAT1和PAT2

perl -ne '/PAT1/../PAT2/ and !/PAT1/ and !/PAT2/ and print' FILE

或者:

perl -ne 'if (/PAT1/../PAT2/) {print unless /PAT1/ or /PAT2/}' FILE 

打印PAT1和PAT2之间的行 - 仅排除PAT1

perl -ne '/PAT1/../PAT2/ and !/PAT1/ and print' FILE

打印PAT1和PAT2之间的行-仅排除PAT2

perl -ne '/PAT1/../PAT2/ and !/PAT2/ and print' FILE

另请参见:

  • perldoc perlop 的范围运算符部分了解更多关于 /PAT1/../PAT2/ 语法的内容:

范围运算符

……在标量上下文中,“..” 返回一个布尔值。该运算符是 像翻转闸门一样的双稳态,并模拟 sed、awk 和各种编辑器的行范围(逗号) 运算符。

  • 有关 -n 选项,请参阅 perldoc perlrun,它使 Perl 的行为类似于 sed -n

  • 有关提取一系列行的详细讨论,请参见 Perl Cookbook, 6.8


8

这里有另一种方法

包含两种模式(默认)

$ awk '/PAT1/,/PAT2/' file
PAT1
3    - first block
4
PAT2
PAT1
7    - second block
PAT2
PAT1
10    - third block

掩盖两个模式

$ awk '/PAT1/,/PAT2/{if(/PAT2|PAT1/) next; print}' file
3    - first block
4
7    - second block
10    - third block

掩码起始模式

$ awk '/PAT1/,/PAT2/{if(/PAT1/) next; print}' file
3    - first block
4
PAT2
7    - second block
PAT2
10    - third block

掩码结束模式

$ awk '/PAT1/,/PAT2/{if(/PAT2/) next; print}' file
PAT1
3    - first block
4
PAT1
7    - second block
PAT1
10    - third block

7

或者:

sed '/START/,/END/!d;//d'

这将删除除了START和END之间(包括START和END)的所有行,然后//d会删除START和END行,因为//会让sed使用上一个模式。

5

这里是对上面两个答案(awk和sed)的补充说明。我需要在大量文件上运行它,因此性能非常重要。我将这两个答案进行了10000次的负载测试:

sedTester.sh

for i in `seq 10000`;do sed -n '/PAT1/,/PAT2/{/PAT1/!{/PAT2/!p;};}' patternTester >> sedTesterOutput; done

awkTester.sh

 for i in `seq 10000`;do awk '/PAT1/{flag=1; next} /PAT2/{flag=0} flag' patternTester >> awkTesterOutput; done

以下是结果:
zsh sedTester.sh  11.89s user 39.63s system 81% cpu 1:02.96 total
zsh awkTester.sh  38.73s user 60.64s system 79% cpu 2:04.83 total

相比于awk解决方案,sed解决方案似乎快了一倍(Mac OS)。


4
您可以通过使用 -n抑制模式空间的正常打印,并对 sed 进行任何您想要的操作。例如,要在结果中 包含 模式,可以执行以下操作:
$ sed -n '/PAT1/,/PAT2/p' filename
PAT1
3    - first block
4
PAT2
PAT1
7    - second block
PAT2
PAT1
10    - third block

为了排除模式并仅打印它们之间的内容:
$ sed -n '/PAT1/,/PAT2/{/PAT1/{n};/PAT2/{d};p}' filename
3    - first block
4
7    - second block
10    - third block

这是如何实现的:

  • sed -n '/PAT1/,/PAT2/ - 查找在 PAT1PAT2 之间的范围并抑制打印;

  • /PAT1/{n}; - 如果匹配了 PAT1,则移动到下一行(n);

  • /PAT2/{d}; - 如果匹配了 PAT2,则删除该行;

  • p - 打印所有在 /PAT1/,/PAT2/ 范围内且未被跳过或删除的行。


谢谢这些有趣的一行代码和它们的分解!我必须承认,我仍然更喜欢awk,因为它对我来说更清晰 :) - fedorqui
我刚刚整理完这个,结果发现 hek2mgl 有一种更简短的方法——看看他的经典 sed 解决方案。 - David C. Rankin

3
这可能适用于您(GNU sed),前提是PAT1PAT2在不同的行上:
sed -n '/PAT1/{:a;N;/PAT2/!ba;p}' file

使用-n选项关闭隐式打印并像grep一样操作。

注意:所有使用范围习惯用法,即/PAT1 /,/ PAT2 /命令的解决方案都会遇到相同的边缘情况,其中PAT1存在但PAT2不存在,因此将从PAT1打印到文件末尾。

为了完整起见:

# PAT1 to PAT2 without PAT1
sed -n '/PAT1/{:a;N;/PAT2/!ba;s/^[^\n]*\n//p}' file 

# PAT1 to PAT2 without PAT2
sed -n '/PAT1/{:a;N;/PAT2/!ba;s/\n[^\n]*$//p}' file 

# PAT1 to PAT2 without PAT1 and PAT2   
sed -n '/PAT1/{:a;N;/PAT2/!ba;/\n.*\n/!d;s/^[^\n]*\n\|\n[^\n]*$/gp}' file

注意:在最后一个解决方案中,PAT1PAT2可能在连续的行上,因此可能会出现进一步的边缘情况。在我看来,两者都被删除,什么也不打印。

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