用sed替换跨行内容

sed是*nix下方便的行编辑工具,经常用来替换文件的内容,sed一般都是处理单行的,但通过它的一些内建功能,也能实现跨行替换(即要替换的内容有多行内容)。

解决方法主要来自网上搜到的[一篇文章](http://www.mail-archive.com/debian-chinese-gb@lists.debian.org/msg14118.html),但文中的大侠并没有解释得特别清楚,我对照着其他两个更晦涩的例子([一](http://www.panix.com/~elflord/unix/sed.html)、[二](http://bbs.chinaunix.net/viewthread.php?tid=279427)),结合man搞懂了之后,记录于此。

假设我们的目标文件test内容是这样的:

file content
  aabbcc<<<comment part 1
  comment part 2>>>
  ddeeff

现在需要把[[[…]]]这一段替换为“COMMENT”(为了说明的必要,没有用容易和正则相混淆的字符比如//*{}[]等来举例子),那么sed语法应当是:

:begin
/<<</,/>>>/ {
    />>>/! {
        $! {
            N;
             b begin
        }
    }
    s/<<<.*>>>/COMMENT/;
}

上述语句存储在test.sed中,那么执行的方式和结果就是:

$ sed -f test.sed test
        file content
          aabbccCOMMENT
          ddeeff

把正则直接写到命令里面也可以,用“;”来分隔命令即可:

$ sed -e ":begin; /<<</,/>>>/ { />>>/! { $! { N; b begin }; }; s/<<<.*>>>/COMMENT/; };" test
        file content
          aabbccCOMMENT
          ddeeff

注意右花括号之后也要加上分号“;”,如果再加上-i参数就可以直接把改动写到原文件中去了。

怎么样?看懂了么?我来详细说明吧,看那个多行命令的test.sed文件的内容:

  • 首先花括号{}代表命令块的开始,类似c的语法,后面就不再说了。

  • :begin,这是一个标号,man中叫做label,也就是跳转标记,供b和t命令用,本例中使用了b命令。

  • /<<</,/>>>/,这是一个地址范围(Addresses),后面{}中的命令只对地址范围之间的内容使用。其中逗号前面的部分是开始地址,逗号后面是结束地址,都是正则表达式。由于sed是“流”式“行”处理,所以结束地址是可以省略的,即如果地址的结束范围不存在,那么将一直处理到文件结尾。本例中使用这个地址范围主要是缩小处理的数据量,因为虽然后面用N命令把对一行的处理扩展为了多行,但如果从文件开头一直N扩展到<<<出现为止,buffer中要处理的字符串可能会很长,影响效率。所以去掉这个处理范围也是能够得到正确结果的,比如:

    $ sed -e ":begin; { />>>/! { $! { N; b begin }; }; s/<<<.*>>>/COMMENT/; };" test
    or
    $ sed -e "{:begin;  />>>/! { $! { N; b begin }; }; s/<<<.*>>>/COMMENT/; };" test
    
  • />>>/!>>>是要替换内容的结束标记,带上!就是说当一行处理完毕之后,如果没有发现结束标记。。。

  • $!$在正则中表示字符串结尾,在sed中代表文件的最后一行,本句和上一句结合起来的意思就是:如果在本行没有发现结束标记,并且当前扫描过的行并不是文件的最后一行。

  • N;,把下一行的内容追加(append)到缓冲区(pattern)之后,在我们的例子中,在处理aabbcc<<<comment part 1这一行的内容时,就会执行到这里,然后把下一行的内容comment part 2>>>一起放入缓冲区,相当于“合并”成了一行(sed的缓冲区中默认都只会包含一行的内容)。

  • b begin,由于仍然没有找到结束标记<<<(注意上一条说的缓冲区还没有被处理),所以在这里跳回到标号begin,重新开始命令。如果开始和结束标记之间间隔了多行,那么就会有多次跳转发生。

  • s/<<<.*>>>/COMMENT/;,终于,/>>>/!不再匹配成功,也就是我们已经找到了结束标记,那么用s命令来进行替换。如果开始和结束标记在一行的话,就会越过上面那些复杂的处理,直接执行到这里了。

介绍完毕,收工。

Update @ 2007-12-14

在和bxy讨论的过程中,又发现sed的另外一种用途,从html或xml中按照tag对应关系,筛选打印出指定的tag内容,使用了正则中的p命令,好像默认就没有“不能处理多行内容”以及“贪婪性”的问题,很好用,很强大:

    $ sed -n -e '/<title>/p' -e '/<text /,/<\/text>/p' from.xml

注意//不在同一行的时候才好用,不然会匹配到下一个实例出现的位置作为结束边界。

11 thoughts on “用sed替换跨行内容”

  1. 我这两天也在看sed http://www.grymoire.com/Unix/Sed.html 特别喜欢 你介绍的 -i 直接写回文件我觉得最有用。其他没看动

    我问一下 用sed能完写成这个么

    1.txt的内容

    pattern1 pattern2

    我要把1.txt改成

    pattern1 pattern2 pattern1_extra

    就是寻找了一个内容把它改变一下写到后面。 我不知道这个人物是不是适合用sed完成。

    1. 博主介绍的不错 看了其它人的方法,感觉下面的这个方法可能最简单,没有测试。 sed ‘s/\(pattern1\) \(pattern2\)/\1 \2 \1_extra/’ input_file

  2. 应该是能用sed实现的,把pattern2作为结束标记来套例子应该可行。 另外你给的那个地址里面,sed介绍的更详细和全面,好好研究吧。

  3. 强的,sed 跨行替换一直不会,这个超实用 我以前一直用的土方法就是先把那几行找出来,一块儿删光,在重新添加插入,步骤多不说,遇到长行的话特麻烦

    To LS:sed 肯定能做,只是想起来麻烦。不如用变量省脑子:A_str=egrep pattern1 1.txt && echo $A_str”_extra”>>1.txt && unset A_str

  4. 哎呀,wp把反单引号过掉了(上面变色的部分)。。。shell变量赋值需要用反单引号把命令引起来

  5. @Blake 不是WP作的,是markdown插件,反单引号在markdown中的作用是<code>,我给你更正如下: A_str=`egrep pattern1 1.txt` && echo $A_str”_extra”>>1.txt && unset A_str

Leave a Reply

Your email address will not be published. Required fields are marked *