如果一行符合“foo”,上一行符合“bar”,下一行符合“baz”,则删除该行?

4

使用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

相当丑陋。

尝试使用while循环逐行读取文件。请将该代码添加到问题中.. :) - Sundeep
1
好观点@Sundeep,已经编辑过了。它非常丑陋... - birac
5个回答

5

如果您需要一种优雅的 perl 解决方案,请参考Sundeep的答案

如果您需要类似且非常好的sed解决方案,请参考potong的第二个答案

这两种解决方案都会将文件完全读入内存并一次性处理。如果您不需要处理GB级别的文件大小,那么这是可以接受的。换句话说,这些是最佳解决方案(如果我们忽略CASE3)。

注释:这两种解决方案都无法通过CASE3(请参见下文)。CASE3是一个特殊的有争议的情况。


更新1:以下的awk解决方案是一个新脚本,可在所有情况下运行。早期的解决方案在特定情况下失败(请参见下面的CASE3)。此解决方案解决了嵌套分组(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通过在保留空间(x;s...;x)中放置单词print并向模式空间附加另一行来初始化状态(N)。
  • 第二行#2将第三行添加到模式空间中。
  • 第三行#3通过检查保留空间并删除保留空间P,打印模式空间中的第一个\n并用z清除模式空间来确定是否需要打印模式空间的第一行。
  • 第四行#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

以前被接受的答案:(更新以指出哪些情况会失败)

awkCASE3中失败

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>

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

  • !/baz/&&(c==2){print foo}:这解决了提前终止的问题。如果在有效的bar-foo组合之后没有找到baz,则打印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的字符串,因此RS表示有效的bar-foo-baz组合。找到的记录分隔符RTsub进行编辑以删除foo行,并在找到的记录之后打印。

RT(gawk扩展)匹配由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;...} 如果模式缓冲区在新行字符后包含foo,则将下一行追加到模式缓冲区中 (N)
  • /foo.*\n.*baz[^\n]*$/{s/\n.*foo.*\n/\n/} 如果模式缓冲区包含 foo后跟单个换行符并以包含baz的行结尾,则删除包含foo的行。此处搜索模式排除了类似于barfoo\nfoobaz\ncar的情况。

很好 :) 建议也为第一个答案添加解释 :) - Sundeep
2
@Sundeep 按要求添加了注释,并提供另一个 sed 解决方案。 - kvantour
bar然后跟随foo,接着是barfoo然后是baz会发生什么? - potong
@kvantour 不,sed和第一个awk解决方案都不起作用...只有gawk可以... - Sundeep
1
@Sundeep,你是正确的,确实存在问题。我已经更新了答案,提供了两个新的解决方案和更多的测试。据我所知,现在所有情况都可以正常工作。 - kvantour

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
  • 有关此正则表达式的更多详细信息,请参见《参考 - 正则表达式是什么意思?》 中的“lookarounds”部分。这里确保跨越3行的重叠匹配被处理

2
Perl 太棒了!非常不错! - kvantour
1
哇,这个 Perl 解决方案真是太优雅了!我以后一定会尝试使用 Perl 来处理这些事情。 - birac
1
@Sundeep,有一个嵌套的情况 bar \n foobar \n foobaz \n baz 似乎失败了。我期望这两行 foo 被删除。然而这是一个特殊情况。 - kvantour
@kvantour 是的,因为 bar \n foobar \n foobaz 会导致正则表达式引擎在删除 foobar 行后移动到 foobaz,所以它将无法匹配已经被删除的 bar... 不确定正则表达式是否能处理这种情况 :) - Sundeep

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

2

解决方案1: 对于相同的完全相同的文件(如您所示),如果没有进一步的条件,则以下内容可能会对您有所帮助。

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

解决方案2:使用以下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同时出现时,它将正常工作。

情况2:当字符串bar出现并且没有foo的情况下,然后字符串baz出现,它也应该正常工作。


不行,这样做不行...尝试使用一行bar,然后是不包含foo的行。 - Sundeep
不要使用getline,这里会导致麻烦... 检查一下行bar,后跟 foobarfoobaz... - Sundeep
@Sundeep,这样我们可以创建n个组合 :) 让OP说一下,然后我就可以修复它。 - RavinderSingh13
@RavinderSingh13,你的第一个答案对我很有用,但是编辑后的版本似乎没有删除第二个foo? - birac
1
@birac,请检查我的编辑过的帖子,并附上解释。 - RavinderSingh13

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

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