一、多行模式:N;P;D

  上一篇sed使用基础入门中介绍的基础命令都是面向单行的,一般情况下,这种处理并没有什么问题,但是当匹配的内容是错开在两行时就会有问题,最明显的例子就是某些英文单词会被分成两行。

幸运地是,sed允许将多行内容读取到模式空间,这样你就可以匹配跨越多行的内容。

   本篇笔记主要介绍这些命令,它们能够创建多行模式空间并且处理之

其中,N/D/P这三个多行命令分别对应于小写的n/d/p命令,后者我们在上一篇已经介绍。它们的功能是类似的,区别在于命令影响的内容不同

   例如:D命令与d命令同样是删除模式空间的内容,只不过d命令会删除模式空间中所有的内容,而D命令仅会删除多行模式空间中的第一行。

   

1、N  读下一行

  我们学过n,将下一行的内容提前读入,并且将之前读入的行(在模式空间中的行,没有被编辑命令编辑)输出到屏幕,然后后续的命令会应用到新读入的行上。

   而N命令将下一行的内容读取到当前模式空间,但是和n不一样的地方是:N命令并没有直接输出当前模式空间中的行,而是把下一行追加到当前模式空间,两行之间用换行符\n连接

例:要把test中“ssbb”替换成“你好”,我们发现ssbb跨越了2行,用sed之前的命令好像没有办法实现,现在用N命令试试

[root@localhost tmp]# cat test
aaa aaaaaa aaaa ss
bb ccccc cccccc ccc
ds sdfas  centos rhess
bbntoo cobbler mint 
[root@localhost tmp]# sed 'N;s/ss\nbb/nihao/' test
aaa aaaaaa aaaa nihao ccccc cccccc ccc
ds sdfas  centos rhenihaontoo cobbler mint 
[root@localhost tmp]# sed '1N;s/ss\nbb/nihao/' test
aaa aaaaaa aaaa nihao ccccc cccccc ccc
ds sdfas  centos rhess
bbntoo cobbler mint

   可以看出N将匹配到的行的下行通过\n追加在匹配到的行的后面变成了一行,再使用s替换就可以了

不过这个例子有两个局限:

   我们知道ssbb分割的位置;

   执行替换命令后,前后两行拼接在一起,导致这行过长;

第二点,可以这样解决:

[root@localhost tmp]# sed 'N;s/ss\nbb/nihao\n/' test 
aaa aaaaaa aaaa nihao
 ccccc cccccc ccc
ds sdfas  centos rhenihao
ntoo cobbler mint 
[root@localhost tmp]# sed 'N;s/ss\nbb/\nnihao/' test  
aaa aaaaaa aaaa 
nihao ccccc cccccc ccc
ds sdfas  centos rhe
nihaontoo cobbler mint


2、D  删除行

  该命令删除多行模式空间中的首行,把连接符\n也删了,而它对应的小d命令删除模式空间的所有内容。D不会影响到读入新行,相反它会回到最初的编辑命令,重复应用在模式空间剩余的内容上。

后面半句开始比较难以理解,用一个例子来解释下。现在我们有一个文本文件,内容如下所示,行之间有空行:

[root@Note3 src]# cat N.txt
This line is followed by 1 blank line.

This line is followed by 2 blank lines.


This line is followed by 3 blank lines.



This line is followed by 4 blank lines.




This is the end.

现在我们要删除多余的空行,将多个空行缩减成一个空行,怎么实现?

是不是只有用刚学的N命令好处理

[root@Note3 src]# sed '/^$/{N;/^\n$/d}' N.txt
This line is followed by 1 blank line.

This line is followed by 2 blank lines.
This line is followed by 3 blank lines.

This line is followed by 4 blank lines.
This is the end.

   但是有没有发现连在一起的空行总数是偶数就都被删了,而奇数的相连空行已经被合并成一行

造成这样的原因需要重新看下上面的命令,当匹配一个空行,是将下一行也读取到模式空间,然后若下一行也是空行,则模式空间中的内容应该是\n,因此匹配^\n$,从而执行d命令会将模式空间中的内容清空,结果就是相连的两个空行都被删除。这样就可以理解为什么相连奇数个空行的情况下是正常的,而偶数个数就有问题了。

这种情况下,我们就应该用D命令来处理,这样做就得到预期的结果了:

[root@localhost tmp]# sed '/^$/{N;/^\n$/D}' test1 
This line is followed by 1 blank line.

This line is followed by 2 blank lines.

This line is followed by 3 blank lines.

This line is followed by 4 blank lines.

This is the end.

   D命令只会删除模式空间的第一行,而且删除后会重新在模式空间的内容上执行编辑命令,类似形成一个循环,前提是相连的都是空行。当匹配一个空行时,N读取下一行内容,此时匹配^\n$导致模式空间中的第一行被删除。现在模式空间中的内容是空的,重新执行编辑命令,此时匹配/^$/。继续读取下一行,当下一行依然为空行时,重复之前的动作,否则输出当前模式空间的内容。造成的结果是连续多个空行,只有最后一个空行是保留输出的,其余的都被删除了。这样的结果才是我们最初希望得到的。


3、P 打印行

  P命令与p命令一样是打印模式空间的内容,不同的是前者仅打印模式空间的第一行内容,而后者是打印所有的内容。因为编辑命令全部执行完之后,sed默认会输出模式空间的内容,所以一般情况下,p和P命令都是与-n选项一起使用的。但是有一种情况是例外的,即编辑命令的执行流程被改变的情况,例如N,D等。很多情况下,P命令都是用在N命令之后,D命令之前的。这三个命令合起来,可以形成输入输出的循环,并且每次只打印一行:读入一行后,N继续读下一行,P命令打印第一行,D命令删除第一行,执行流程回到最开始重复该过程:

[root@Node5 src]# echo -e "line1\nline2\nline3"
line1
line2
line3
[root@Node5 src]# echo -e "line1\nline2\nline3"|sed 'N;P;D'
line1
line2
line3

  不过多行命令用起来要格外小心,你要用自己的大脑去演算一遍执行的过程,要不然很容易出错

比如:

[root@Node5 src]# echo -e "line1\nline2\nline3" | sed -n 'N;1P'
[root@Node5 src]# echo -e "line1\nline2\nline3" | sed -n 'N;P'
line1
[root@Node5 src]# echo -e "line1\nline2\nline3" | sed -n 'N;2P'
line1

   你可能期望打印第一行的内容,事实上并没有输出。原因是当N继续读入第二行后,当前行号已经是2了,行号只是sed在内部维护的一个计数变量而已,每当读入新的一行,行号就加1:

[root@localhost tmp]# echo -e "line1\nline2\nline3" | sed '$!N;='  
                   # $!N表示如果是最后一行则不执行N命令了
2
line1
line2
3
line3
[root@localhost tmp]# echo -e "line1\nline2\nline3" | sed -n '$!N;='
2
3


二、保持空间

1、保持空间简介

  上面N;P;D三个命令,它们可以形成多行的模式空间,在一点程度上弥补了单行模式空间的不足,我们用sed编辑文本的能力又进了一步。模式空间是sed内部维护的一个缓存空间,它存放着读入的一行或者多行内容。但是模式空间的一个限制是无法保存模式空间中被处理的行,因此sed又引入了另外一个缓存空间——保持空间(Hold Space)。


保持空间:

  保持空间用于保存模式空间的内容,模式空间的内容可以复制到保持空间,同样地保持空间的内容可以复制回模式空间。sed提供了几组命令用来完成复制的工作,其它命令无法匹配也不能修改保持空间的内容。


操作保持空间的命令如下所示:

  名称          命令                     命令说明

保存(Hold)         h/H          将模式空间的内容覆盖或者追加到保持空间

取回(Get)         g/G          将保持空间的内容覆盖或者追加到模式空间

交换(Exchange)      x           交换模式空间和保持空间的内容


   这几组命令提供了保存、取回以及交换三个动作,交换命令比较容易理解,保存命令和取回命令都有大写和小写两种形式,这两种形式的区别是小写的是将会覆盖目的空间的内容,而大写的是将内容追加到目的空间,追加的内容和原有的内容是以\n 连接


2、基本使用

我们随便试试这几个命令,假设有如下测试文本:

[root@Node5 src]# cat test2
1
2
11
22
111
222

首先,仅使用h/H或者g/G命令:

[root@Node5 src]# sed 'h' test2     # 将模式空间的内容覆盖到保持空间,打印模式空间的内容
1
2
11
22
111
222
[root@Node5 src]# sed 'H' test2    # 将模式空间的内容覆盖到保持空间,打印模式空间的内容
1
2
11
22
111
222

[root@Node5 src]# sed 'g' test2   # 将保持空间的内容覆盖到模式空间,打印模式空间的内容







[root@Node5 src]# sed 'G' test2  # 将保持空间的内容追加到模式空间,打印模式空间的内容
1

2

11

22

111

222

[root@Node5 src]#

  前者返回的结果正常,因为复制到保持空间的内容并没有取回;后者每一行的后面都多了一个空行,原因是每行都会从保持空间取回一行,追加(大写的G)到模式空间的内容之后,以\n分隔。

这个很好理解吧,理解了就没什么问题了。


使用x命令交换空间:

[root@Node5 src]# sed 'x' test2

1
2
11
22
111
[root@Node5 src]#

命令执行后,发现前面多了一个空行并且最后一行不见了。

想sed命令用的好,需要脑补命令执行过程:

* 当读入第一行的时候,模式空间中的内容是第一行的内容,而保持空间是空的,这个时候交换两个空间,导致模式空间为空,保持空间为第一行的内容,因此输出为空行;

* 当读入下一行之后,模式空间为第2行的内容,保持空间为第一行的内容,交换后输出第1行的内容;

* 依次读入每一行,输出上一行的内容;

* 直到最后一行被读入到模式空间,交换后输出倒数第二行的内容,而最后一行的内容并没有输出,此时命令执行结束。


3、深入使用

  看了上面的内容是不是觉得sed的高级用法也不过如此?

  上面的例子简单地介绍了保持空间命令的基本使用方法,这些命令单个使用可能效果不大,但是组合起来的效果是非常好的。


第一个例子:使用逗号拼接行

[root@localhost src]# sed 'H;$!d;x;s/^\n//;s/\n/,/g' test2
1,2,11,22,111,222

是不是瞬间高上大了,装x利器啊!

其实理解了上面说对保持空间的说明,这里就很容易理解了,上面的命令执行过程是这样的:

   使用H将每一行都追加到保持空间,这里利用d命令打断常规的命令执行流程,让sed继续读入新的一行,直接到将最后一行都放到保持空间。这个时候使用x命令将保持空间的内容交换到模式空间,模式空间的内容现在是这样的“\n1\n2\n11\n22\n111\n222”替换的步骤分成两个,首先去掉首个回车符,然后把剩余的回车符替换成逗号。


其它例子:

sed 'G'                      在文件中的每行后方添加空白行;

sed '$!d'                     保留最后一行;

sed '/^$/d;G'                  保证指定的文件每一行后方有且只有一个空白行;

sed 'n;d'                     保留奇数行;

sed -n 'p;n'                   保留奇数行

sed -n 'n;p'                   保留偶数行

sed 'N;s/\(.*\)\n\(.*\)/\2\n\1/'      奇数行和偶数行调换

sed -n 'h;n;G;p'                奇数行和偶数行调换

sed -n '1!G;h;$p'                倒行输出原文内容

[root@Node5 src]# sed -n '1!G;h;$p' test2
222
111
22
11
2
1

读取第一行:

   1!G:不符合地址定界不操作;

    h:把模式空间的行原文num1复制并覆盖到保持空间;

    $p:不符合地址定界,不操作;

读取第二行:

   1!G:将取保持空间的行原文num1通过\n与读取进来的num2连接;

    h:把模式空间的行(num1+\n+num2)复制并覆盖到保持空间;

   $p:不符合地址定界,不操作;最后模式空间只有原文num2+\n+num1,

读取最后一行:

   1!G:模式空间变成num3+\n+num2+\n+\num1;

    h:保持空间变成num3+\n+\num2+\num1;

    $p:打印模式空间的num3+\n+num2+\num1;

      最后只有打印这个操作,所以输出num3+\n+num2+\n+num1


sed '$!N;$!D'               删除了第一行


读取第一行:!N:num1+\n+num2;!$D:num2;最后输出模式空间的num2

读取第三行:$!N:不符合地址定界不操作,num2;$!D:不符合地址定界,不操作,num2;最后输出num2


4、流程控制

  一般情况下,sed是将编辑命令从上到下依次应用到读入的行上,但是像d/n/D/N命令都能够在一定程度上改变默认的执行流程,甚至利用N/D/P三个命令可以形成一个强大的循环处理流程。除此之外,其实sed还提供了分支命令(b)和测试(test)两个命令来控制流程,这两个命令可以跳转到指定的标签(label)位置继续执行命令。

标签是以冒号开头的标记,如下例中的:top标签:

:top

command1

command2/pattern/b top

command3

   当执行到/pattern/b top时,如果匹配pattern,则跳转到:top标签所在的位置,继续执行下一个命令command1,如果没有指定标签,则将控制转移到脚本的结尾处,这是一个默认的行为,有时候如果用得好也是非常有用的,例如:

/pattern/b

command 1

command 2

command 3

  当执行到/pattern/b时,如果匹配pattern,则跳转到最后。这种情况下匹配pattern的行可以避开执行后续的命令,被排除在外。


下一个例子中,我们利用分支命令的跳转效果达到类似if语句的效果:

command1/pattern/b end

command2:end

command3

  当执行到/pattern/b end时,如果匹配pattern,则跳转到:end标签所在的位置,跳过command2而不执行。

  进一步地,利用两个分支命令可以达到if..else的分支效果:

command1/pattern/b dothree

command2

b:dothree

command3

  这个例子中,当执行到/pattern/b dothree时,若匹配pattern则中转到:dothree标

签,此时执行command3;若不匹配,则执行command2,并且跳转到最后。


例子:

[root@localhost tmp]# echo "a a a a a "|sed ' /[a-z]/ s//A/;/[a-zA-Z]/ s//b/'  
A b a a a 
[root@localhost tmp]# echo "a a a a a "|sed ' /[a-z]/s//A/;b;/[a-z]/s//b/' 
A a a a a 
[root@localhost tmp]# echo "a a a a a "|sed ':top /[a-z]/s//A/;/[a-z]/b;/[a-z]/s//b/'
A a a a a 
[root@localhost tmp]# echo "a a a a a "|sed ':top /[a-z]/s//A/;/[a-z]/b top;/[a-z]/s//b/'
A A A A A 
[root@localhost tmp]# echo "a a a a a "|sed ' /[a-z]/s//A/;b top;/[a-
z]/s//b/;:top;p' 
A a a a a 
A a a a a

  上面的例子都是用到了分支命令,b分支命令的跳转是无条件的。而与之相对的是t测试命令,测试命令的跳转是有条件的,当且仅当当前行发生成功的替换时才跳转。

[root@localhost tmp]# echo "a a a a a "|sed ' /[a-z]/s//A/;t;/[a-z]/s//b/'
A a a a a 
[root@localhost tmp]# echo "a a a a a "|sed ' /[a-z]/s//A/;/[0-9]/t;/[a-z]/s//b/'     
A b a a a