如果行匹配“foo”,则删除行,上面的行匹配“bar”,下面的行匹配“baz”?

时间:2018-03-05 14:37:39

标签: awk sed

使用sed和/或awk,我希望能够删除一行,只要它包含字符串“foo”并且前后的行分别包含字符串“bar”和“baz”。

所以对于这个输入:

blah
blah
foo
blah
bar
foo
baz
blah

我们会删除第二个foo,但没有别的,留下:

blah
blah
foo
blah
bar
baz
blah

我尝试使用while循环逐行读取文件,但这很慢,我无法弄清楚如何匹配上一行和下一行。

编辑 - 根据评论中的要求,这是我的while循环的当前状态。目前只匹配前一行(从前一个循环存储为$ linepre)。

linepre=0 
while read line
do 
   if [ $line != foo ] && [ $linepre != bar ]
   then 
       echo $line
   fi
linepre=$line
done < foobarbaz.txt

非常难看。

5 个答案:

答案 0 :(得分:5)

要获得优雅的 perl 解决方案,请参阅Sundeep's answer

对于类似且非常好的 sed 解决方案,请参阅potong's second answer

两种解决方案都将文件完全读入内存并一次处理。如果您不需要处理GB文件大小,这很好。换句话说,这些是最佳解决方案(如果我们忽略CASE3)。

评论: 两个解决方案都失败CASE3(见下文)。 CASE3是一个特别值得商榷的案例。

更新1:以下awk解决方案是一个适用于所有情况的新脚本。在特定情况下,此答案被接受的早期解决方案失败了。所提出的解决方案解决了嵌套分组(下面的CASE3):

awk 'BEGIN{p=1;l1=l2=""}
     (NR>2) && p {print l1}
     { p=!(l1~/bar/&&l2~/foo/&&/baz/);
       l1=l2;l2=$0
     }
     END{if (l1!="" && p) print l1
         if (l2!=""     ) print l2}' <file>

要解决此问题,我们会不断缓存存储在l1l2$0中的3条线。每次处理新行时,我们确定是否应在下一个周期中打印l1并交换缓冲行。打印仅从NR=3开始。要打印的条件是,如果l1包含barl2包含foo$0包含baz,那么我们不会在下一个打印周期。

更新2:可以获得基于相同原则的sed解决方案。 sed有两个回忆。 模式空间是您执行所有操作的地方,保留空间是长期记忆。我们的想法是将单词print放在保留空间中,但我们只能通过交换周围的空格(使用x)来实现这一点

 sed '1{x;s/^.*$/print/;x;N};                           #1
      N;                                                #2
      x;/print/{z;x;P;x};x;                             #3
      /bar.*\n.*foo.*\n.*baz/!{x;s/^.*$/print/;x};      #4
      $s/\(bar.*\)\n.*foo.*\n\(.*baz\)/\1\n\2/;         #5
      D' <file>                                         #6
  • #1通过将单词print放入保留空间(x;s...;x)并将另一行附加到模式空间(N
  • #2将第三行添加到模式空间
  • #3通过检查保留空间确定是否需要打印模式空间的第一行,并删除保留空间P打印到模式空间中的第一个\nz zaps模式空间
  • #4确定我们是否应该在下一个周期中打印。检查实际模式是否匹配,如果没有将单词print放入保留空间
  • #5,是文件结束条件
  • #6删除模式空间中的第一个\n,然后返回#1而不读取新行。

退出时,将再次打印图案空间。

评论:如果您想查看模式空间和保留空间的外观,可以在每行后添加以下代码:s/^/P:/;l;s/^P://;x;s/^/H:/;l;s/^H://;x。此行将在前面分别打印P: H:个空格。

使用过的测试文件:

# bar-foo-baz test file
# An asterisk indicates the foo
# lines that should be removed
<CASE0 :: default case>
bar
foo (*)
baz
<CASE1 :: reset cycle on second line>
bar
foobar
foo (*)
baz
<CASE2 :: start cycle at end of previous cycle>
bar
foo (*)
bazbar
foo (*)
baz
<CASE3 :: nested cases>
bar
foobar (*)
foobaz (*)
baz
<CASE4 :: end-of-file case>
bar
foo

以前接受的答案:(已更新,表明哪些案例失败)

awk失败CASE3

awk '!/baz/&&(c==2){print foo}
     /bar/         {c=1;print;next}
     /foo/ &&(c==1){c++;foo=$0;next}
                   {c=0;print}
     END{if(c==2){print foo}}' <file>

此解决方案默认打印所有行,除非该行包含foo,该行位于包含bar的行之后。上面的逻辑决定我们是否应该打印行foo

  • !/baz/&&(c==2){print foo}:这解决了提前终止问题。如果在有效的baz组合后找不到bar-foo,则会打印foo行。

  • /bar/{c=1;print;next}:这会初始化新周期的开始。如果找到bar,请将c设置为1,打印该行并移至下一行。始终会打印bar行。此行可解析CASE1CASE2

  • /foo/&&(c==1){c++;foo=$0;next}:这会检查bar-foo组合。它存储foo行并移至下一行。

  • {c=0;print},如果我们达到这一点,就意味着我们找不到bar行或bar-foo组合。只需在默认情况下打印该行,并将计数器重置为零。

  • END{if(c==2){print foo}}此声明仅解决CASE4

gawk失败CASE3

awk 'BEGIN{ORS="";RS="bar[^\n]*\n[^\n]*foo[^\n]*\n[^\n]*baz"}
     {sub(/\n[^\n]*foo[^\n]*\n/,"\n",RT); print $0 RT}' <file>

RS设置为bar[^\n]*\n[^\n]*foo[^\n]*\n[^\n]*baz,即我们感兴趣的模式。此处,[^\n]*\n[^\n]*表示包含单个\n的字符串,因此{{1} }}表示有效的RS组合。使用bar-foo-baz编辑找到的记录分隔符RT以删除sub行,并在找到的记录后打印。

  

foo(gawk扩展名)与表示的文字相匹配的输入文字   RT,记录分隔符。每次读取记录时都会设置它。

RS失败sed

CASE1, CASE2, CASE3, CASE4
  • sed -n '/bar/{N;/\n.*foo/{N;/foo.*\n.*baz[^\n]*$/{s/\n.*foo.*\n/\n/}}};p' <file> 如果该行包含/bar/{N;...},则将下一行追加到模式缓冲区bar
  • N如果模式缓冲区在换行符后面有/\n.*foo/{N;...},请将下一行追加到模式缓冲区({{1} })
  • foo如果模式缓冲区包含N后跟一个换行符,并以包含/foo.*\n.*baz[^\n]*$/{s/\n.*foo.*\n/\n/}的行结束,请删除包含foo的行}。此处的搜索模式将案例排除为baz

答案 1 :(得分:3)

针对更多异国情调的修改样本:

$ cat ip.txt 
blah
bar
blah
foo
blah
bar
foo
baz
blah
bar
foobar
foo
baz
asdf

如果perl没问题且输入文件小到足以满足内存要求

$ perl -0777 -pe 's/bar.*\n\K.*foo.*\n(?=.*baz)//g' ip.txt
blah
bar
blah
foo
blah
bar
baz
blah
bar
foobar
baz
asdf
  • -0777扼杀整个输入文件
  • bar.*\n\K检查上一行是否包含bar
  • .*foo.*\n当前行包含foo
  • (?=.*baz)下一行包含baz
  • 有关此正则表达式的详细信息,请参阅Reference - What does this regex mean?中的 lookarounds 部分。在这里,他们确保三条线的重叠匹配得到了照顾

答案 2 :(得分:3)

这可能适合你(GNU sed):

sed ':a;/bar/!b;n;/foo/!ba;N;s/^.*\n\(.*baz\)/\1/;t;P;D' file

如果当前行不包含bar,则打印它并开始新的循环。否则,打印包含bar的行,并将下一行读入模式空间。如果该行不包含foo,请返回并检查它是否包含bar。否则,将下一行追加到当前行(包含foo)并检查附加行是否包含baz。如果确实删除了包含foo的第一行,然后打印包含baz的行并开始新的循环。否则,附加行不包含baz,因此请打印包含foo的行并将其删除,然后检查附加行是否包含bar

另一种方法是将整个文件放入内存:

sed -zr 's/(bar[^\n]*)\n[^\n]*foo[^\n]*(\n[^\n]*baz)/\1\2/g' file

答案 3 :(得分:2)

解决方案1: 对于同一个相同的文件(您显示的),没有任何其他条件可能会对您有所帮助。

awk '/^bar/ && getline var ~ /^foo/ && getline var1 ~ /^baz/{print "bar" ORS "baz";next} 1'  Input_file

解决方案第二: 关注awk可能对您有帮助。

awk '/bar/{val=FNR} /^foo/ && ++val==FNR{value=$0;getline;if($0 ~ /^baz/){print value ORS $0;val="";next} else {print value}} 1'    Input_file

上述第二个代码的不同排列和组合检查:

情境1: 当字符串bar字符串foo和字符串baz出现时,它会正常工作。

情况第二: 当字符串bar出现,然后字符串baz没有foo时,它也应该有效。< / p>

答案 4 :(得分:0)

第一个变种 - 使用sed

sed -r ':l; N; $!bl; s/(^|\nbar\n)foo\n(baz$|\n)/\1\2/g' input.txt

或相同,但更短,可能更快,使用-z选项:

sed -zr 's/(^|\nbar\n)foo\n(baz\n|$)/\1\2/g' input.txt

-z =按NUL字符分隔行。此选项可用于将所有文本放入内存(如果文本没有NUL字符)。

第二个变体 - 使用grep和sed

grep --color=always -Pz '\^|\nbar\n\Kfoo\n(?=baz\n)' input.txt | sed '/31m/d'

两种变体都会在处理之前将所有文本放入内存中,因此对于大文件而言,它们并非最佳。

<强>输入

blah
blah
foo
blah
bar
foo
baz
blah

<强>输出

blah
blah
foo
blah
bar
baz
blah