一、多行模式: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
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/'