提取两个模式之间的行,并包括第一个模式上面和第二个模式下面的行。

3
拥有以下文本文件,我需要提取并打印两个模式之间的字符串,并包括第一个模式上方的行和第二个模式下方的行。
asdgs sdagasdg sdagdsag
asdfgsdagg gsfagsaf 
asdfsdaf dsafsdfdsfas
asdfdasfadf
nnnn nnnnn aaaaa
line before first pattern
***** FIRST *****
dddd ffff cccc
wwww rrrrrrrr xxxx
***** SECOND *****
line after second pattern
asdfgsdagg gsfagsaf 
asdfsdaf dsafsdfdsfas
asdfdasfadf
nnnn nnnnn aaaaa

我已经找到了许多用sed和awk提取两个标签之间内容的解决方案,如下所示

sed -n '/FIRST/,/SECOND/p' FileName

但是如何包含模式前后的行?

期望输出:

line before first pattern
***** FIRST *****
dddd ffff cccc
wwww rrrrrrrr xxxx
***** SECOND *****
line after second pattern

这在 ed 中很简单,因为 ed 的寻址比 sed 更先进:printf '%s\n' "/FIRST/-;/SECOND/+p" q | ed -s FileName。如果你想保存那一部分:printf '%s\n' "/FIRST/-;/SECOND/+w NewFileName" q | ed -s FileName - gniourf_gniourf
永远不要使用范围表达式(/FIRST/,/SECOND/),因为它们使得微不足道的事情变得稍微简洁一些,但是当问题变得稍微有点有趣时,就需要完全重写和/或重复条件,正如您现在正在发现的那样。始终使用“found”标志,'/FIRST/{f=1} f; /SECOND/{f=0}(哦,顺便说一下,对于这个和涉及多行的任何其他事情,请使用awk而不是sed)。 - Ed Morton
7个回答

3

由于您要求使用 sed/awk 解决方案(而且每个人都害怕使用 ed ;-),以下是一种您可以在 awk 中实现的方法:

awk '/FIRST/{print p; f=1} {p=$0} /SECOND/{c=1} f; c--==0{f=0}' file

当匹配到第一个模式时,打印前一行p并设置打印标志f。当匹配到第二个模式时,将c设置为1。如果f为1(真),则当前行将被打印。只有在匹配到第二个模式后的下一行才会出现c--==0
另一种方法是通过两次循环文件来实现:
awk 'NR==FNR{if(/FIRST/)s=NR;else if(/SECOND/)e=NR;next}FNR>=s-1&&FNR<=e+1' file file

第一遍循环文件并记录行号,第二遍打印范围内的行。

第二种方法的优点是只需更改脚本中的数字,就可以轻松地打印范围前M行和后N行。

要使用shell变量而不是硬编码模式,可以像这样传递变量:

awk -v first="$first" -v second="$second" '...' file

那么使用$0 ~ first代替/FIRST/


"c--==0"会在c变成0之后继续递减。你确定如果文件很大它不会绕过来吗?我总是写成"c&&c--",这样当c变成0时就会停止递减。 - Ed Morton
如果SECOND出现在FIRST之前或没有FIRST,它也会表现出不良行为,并且它无法处理同一文件(或多个文件)中的多个范围。 - Ed Morton
@Ed感谢您的评论,尽管我不确定您建议将c--==0更改为什么,因为c&&c--的工作方式不同。我会更改我的答案使其更具弹性,但那样的话它基本上就是您的副本了! - Tom Fenech
一种选项是 /SECOND/{f=0;c=2} f||(c&&c--) 但不是特别直观地明白它在做什么!另外 c&&c--; f{print; if(/SECOND/){c=1;f=0}} 或者 ... - Ed Morton
@TomFenech:如果FIRST和SECOND模式是shell变量,该怎么办?我尝试使用-v开关但没有成功:awk -v firstpattern=$FIRST -v secondpattern=$SECOND '/firstpattern/{print p; f=1} {p=$0} /secondpattern/{c=1} f; c--==0{f=0}' file - John
显示剩余2条评论

2

I'd say

sed '/FIRST/ { x; G; :a n; /SECOND/! ba; n; q; }; h; d' filename

即:

/FIRST/ {        # If a line matches FIRST
  x              # swap hold buffer and pattern space,
  G              # append hold buffer to pattern space.
                 # We saved the last line before the match in the hold
                 # buffer, so the pattern space now contains the previous
                 # and the matching line.
  :a             # jump label for looping
  n              # print pattern space, fetch next line.
  /SECOND/! ba   # unless it matches SECOND, go back to :a
  n              # fetch one more line after the match
  q              # quit (printing that last line in the process)
}
h                # If we get here, it's before the block. Hold the current
                 # line for later use.
d                # don't print anything.

请注意,BSD版本的sed(如Mac OS X和*BSD中自带的)对分支命令要求较高。如果您正在这些平台上工作,请注意此点。
sed -e '/FIRST/ { x; G; :a' -e 'n; /SECOND/! ba' -e 'n; q; }; h; d' filename

应该可以正常工作。


1
无论文件中是否有多个范围,这都可以正常工作:
$ cat tst.awk
/FIRST/ { print prev; gotBeg=1 }
gotBeg {
    print
    if (gotEnd)   gotBeg=gotEnd=0
    if (/SECOND/) gotEnd=1
}
{ prev=$0 }

$ awk -f tst.awk file
line before first pattern
***** FIRST *****
dddd ffff cccc
wwww rrrrrrrr xxxx
***** SECOND *****
line after second pattern

如果您需要在“FIRST”更改之前打印超过1行,请将prev更改为数组。如果您需要在“SECOND”之后打印超过1行,请将gotEnd更改为计数。

0
sed '#n
   H;$!d
   x;s/\n/²/g
   /FIRST.*SECOND/!b
   s/.*²\([^²]*²[^²]*FIRST\)/\1/
:a
   s/\(FIRST.*SECOND[^²]*²[^²]*\)².\{1,\}/\1/
   ta
   s/²/\
/g
   p' YourFile
  • POSIX sed版本(GNU sed使用--posix
  • 如果在同一行上,也可以采用以下第二个模式,易于适应至少一个新行之间
    • #n:不打印除非表达式请求(如p
    • H;$!d:将每行附加到缓冲区,如果不是最后一行,则删除当前行并循环
    • x;s/\n/²/g:加载缓冲区并替换任何新行为另一个字符(这里我使用²),因为posix sed不允许[^\n]
    • /FIRST.*SECOND/!b:如果没有模式存在,则退出而不输出
    • s/.*²\([^²]*²[^²]*FIRST\)/\1/:删除第一个模式之前的所有内容
    • :a:用于goto的标签(稍后使用)
    • s/\(FIRST.*SECOND[^²]*²[^²]*\)².\{1,\}/\1/:删除第二个模式之后的所有内容。它采用最大字符串,因此最后出现的模式是参考
    • ta:如果最后一个s///发生,则转到标签a。它循环,直到文件中出现第一个SECOND模式(在FIRST之后)
    • s/²/\ /g:恢复新行
    • p:打印结果

0

根据Tom的评论:如果文件不大,我们可以将其存储在数组中,然后循环遍历:

awk '{a[++i]=$0} /FIRST/{s=NR} /SECOND/{e=NR} END {for(i=s-1;i<e+1;i++) print a[i]}'

你不需要存储整个文件,只需存储你感兴趣的片段。如果有多个FIRST/SECOND片段或没有FIRST的SECOND片段,那么这种方法会失败。 - Ed Morton

0

个人而言,我会使用Perl来完成。我们有“范围运算符”,可以用来检测是否在两个模式之间:

if ( m/FIRST/ .. /SECOND/ ) 

这是容易的部分。稍微有点困难的是“捕捉”前面和后面的行。因此,我设置了一个$prev_line值,这样当我第一次遇到该测试时,我知道要打印什么。我清除了$prev_line,因为当我再次打印它时,它就为空了,但也因为我可以在范围结束时发现转换。

所以大概像这样:

#!/usr/bin/perl

use strict;
use warnings;

my $prev_line = " ";
while (<DATA>) {
    if ( m/FIRST/ .. /SECOND/ ) {
        print $prev_line;
        $prev_line = '';
        print;
    }
    else {
        if ( not $prev_line ) {
            print;
        }
        $prev_line = $_;
    }
}

__DATA__ 
asdgs sdagasdg sdagdsag
asdfgsdagg gsfagsaf 
asdfsdaf dsafsdfdsfas
asdfdasfadf
nnnn nnnnn aaaaa
line before first pattern
***** FIRST *****
dddd ffff cccc
wwww rrrrrrrr xxxx
***** SECOND *****
line after second pattern
asdfgsdagg gsfagsaf 
asdfsdaf dsafsdfdsfas
asdfdasfadf
nnnn nnnnn aaaaa

0
这可能适用于您(GNU sed):
sed '/FIRST/!{h;d};H;g;:a;n;/SECOND/{n;q};$!ba' file

如果当前行不是FIRST,则将其保存在保留空间中并删除当前行。如果该行是FIRST,则将其附加到已保存的行中,然后打印两行及更多行,直到打印出SECOND时,另外一行被打印并退出脚本。

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