sed是简单小巧但功能非常强大的工具。在上一篇博客中自己注解了sed的info文档中那些比较复杂的脚本示例,最近工作中使用到sed,复习了一下,再做一篇总结放在这里:
- sed有两块缓冲区:pattern space和hold space。pattern space中存放待处理的目标文本,hold space供程序员自己使用。
- sed处理流程:
- 清空pattern space。(如果这是对该输入流的第一次读取,则也将hold space初始化为空)
- 从输入流中读取一行,去除末尾的换行符,将其放入pattern space。
- 执行sed脚本。sed脚本由一系列命令组成。因为sed是按行处理,所以sed命令的含义主要是“对于什么样的行,则进行什么样的处理”。例如,“把第3行删除”,或者“如果这一行包含this,则把其中的this替换成that”。其中“包含什么样的行”由address指定,“执行什么样的处理”由sed命令序列表达。
- 脚本执行完毕,如果输入流中还有内容,转至1,开始下一轮处理。
- 所以,学习sed,就是学习如何“指定需要处理的行”(address),以及学习我们能使用sed“进行什么样的操作”(command)。
- 注意的是,sed都是按行处理,即使指定的是一个address range,比如sed -e '3,7s/this/that/g',s命令会被执行5次(从第3行到第7行每行执行一次)而不是只执行一次就将第3到第7行的所有this换成that。可以使用sed的N命令或者其他命令读入多行文本到pattern space。
- sed使用/M或者/m来切换到"multiple line mode"(即多行处理模式)。所谓“多行处理模式”,是指在这种模式下,元字符^不再表示字符串起始位置,而是表示换行符后面的空字符串,元字符$也不再表示字符串末尾位置,而是表示换行符前面的空字符串。自我感觉这种模式没有任何作用,只会把问题弄得混乱,因为完全可以使用\n来匹配换行符,将其作为普通字符用在正则表达式中。
- sed中的调试非常简便,可以使用上一篇博客中的那个python脚本来调试sed,也可以使用sed中的l命令或者p命令来输出pattern space中的内容进行调试。在以下的“去除多余空行“的方法二中使用l命令进行调试。
练习几个例子,以便将来温习:准备以下文件:
hbwang@jcwkyl:/home/hbwang$cat test abc de f 1 2 345 en d int main () { printf("hello\n"); return 0; } last empty
- 去掉所有换行符:
- 方法一:vim打开文件,执行:1,$s/\n//即可。这里使用sed的方法:读入一行,删除换行符,再读入下一行,直到整个文件处理结束。实现如下:
命令解释:-e参数用来添加执行命令。":a"用来设置一个名字为a的label以供后来的goto使用(sed中有两个跳转命令:b和t。b为无条件跳转,t为当上一个s命令替换成功时跳转)。"N;s/\n//;ba"用分号隔开了三条命令。在执行这些命令之前,pattern space中存放着一个输入行(其末尾的换行符已经去掉),N命令在pattern space后面追加一个换行符并从输入流中读入下一行。此时pattern space中的内容就是输入流中的连续两行文本的拷贝,用s命令去掉换行符,然后再用b命令无条件跳转到label ":a"上继续处理后面的文本。执行结果如上所示,成功达到了目标。hbwang@jcwkyl:/home/hbwang$sed -e :a -e "N;s/\n//;ba" test abcde f1 2 345en dint main () { printf("hello\n"); return 0;}last empty
这里有个细节。如果复习一下开头所说的sed的处理流程(execution cycle),会发现每一轮开始sed都会自己删除pattern space的内容并从输入流中读取下一行。照刚才这种方法,pattern space中两行之间的换行符去掉了,但第二行末尾的换行符似乎没有机会被删除。然而实验结果证明这种方法是对的。不妨再验证一下,在每次替换后用l命令查看一下pattern space中的内容(l命令会以raw format来输出pattern space中的内容,其中特殊字符都会以其转义形式输出,例如换行符会被输出成为\n,字符串末尾会输出一个$来标记):hbwang@jcwkyl:/home/hbwang$sed -e :a -e "N;s/\n//;l;ba" test abcde f$ abcde f1 2 345$ abcde f1 2 345en d$ abcde f1 2 345en d$ abcde f1 2 345en d$ abcde f1 2 345en d$ abcde f1 2 345en d$ abcde f1 2 345en dint main () {$ abcde f1 2 345en dint main () { printf("hello\\n");$ abcde f1 2 345en dint main () { printf("hello\\n"); return \ 0;$ abcde f1 2 345en dint main () { printf("hello\\n"); return \ 0;}$ abcde f1 2 345en dint main () { printf("hello\\n"); return \ 0;}$ abcde f1 2 345en dint main () { printf("hello\\n"); return \ 0;}$ abcde f1 2 345en dint main () { printf("hello\\n"); return \ 0;}last empty$ abcde f1 2 345en dint main () { printf("hello\n"); return 0;}last empty
那如果要实现刚才设想的情况,只删除第奇数个换行符而保留第偶数个换行符,只需要让sed去除一个换行符后直接开始下一轮执行即可,用d命令可以做到:hbwang@jcwkyl:/home/hbwang$sed -e :a -e "N;s/\n//;p;d" test abcde f 1 2 345en d int main () { printf("hello\n"); return 0;} last empty
- 方法二:读入整个文件,用一条命令删除所有换行符。
因为sed在每个cycle开始都会清空pattern space,所以要使用sed读入整个文件,要么在一个cycle中用一个循环把所有行都读入到pattern space,要么利用hold space不会被清空的特性,每读一行就把它添加到hold space中,整个文件读完,hold space中也就保存了整个文件的内容。sed中只支持两种跳转方式,即:- if(是目标行) { /* handle */ } else { /*非目标行 */}
- if(替换成功) { goto label; } else { /* 继续执行 */}
hbwang@jcwkyl:/home/hbwang$sed -e '$!{H;d}' -e '${H;x;s/\n//g;}' test abcde f1 2 345en dint main () { printf("hello\n"); return 0;}last empty 或者: hbwang@jcwkyl:/home/hbwang$sed -n -e '$!H' -e '${H;x;s/\n//gp}' test abcde f1 2 345en dint main () { printf("hello\n"); return 0;}last empty 或者: hbwang@jcwkyl:/home/hbwang$sed -n -e '$!H' -e '${H;g;s/\n//gp}' test abcde f1 2 345en dint main () { printf("hello\n"); return 0;}last empty
其中,"$"是最后一行地址,"$!"表示“不是最后一行”。实现1,'$!{H,d}',表示如果不是最后一行,用H命令在hold space上添加一个换行符并把pattern space中的内容append到hold space,然后删除pattern space(如果不删除,因为我们没有指定-n选项,所以pattern space的内容会在进入下一轮cycle前输出出来),'${H;x;s\n//g;}'表示如果是最后一行,用H命令把最后一行添加到hold space,用x命令交换pattern space和hold space(其目的是把hold space内容放到pattern space以便s命令处理),然后用s命令删除所有换行符。实现二,和实现一相同,不过指定了-n选项,所以需要用p命令输出自己希望输出的东西。实现三使用g命令,与x命令不同的是g命令直接用hold space的内容覆盖pattern space的内容。
- 方法一:vim打开文件,执行:1,$s/\n//即可。这里使用sed的方法:读入一行,删除换行符,再读入下一行,直到整个文件处理结束。实现如下:
- 去掉所有空行空行是只包含空白字符的行。这个处理非常简单:如果当前pattern space中只包含空白字符,删除它就可以:
hbwang@jcwkyl:/home/hbwang$sed -e "/^[ \t]*$/d" test abc de f 1 2 345 en d int main () { printf("hello\n"); return 0; } last empty
- 去掉多余的空行
- 方法一:将连续多个空行变成一个,如果只有一个空行,则不变。有一个简单思路:如果当前行是空行,则删除当前行后续的所有连续空行。换一种描述:对所有的空行,删除其后续的所有连续空行。第二种描述更容易写出sed实现,因为sed基本功能就是“对什么样的行”进行“什么样的操作”。写出来的脚本如下:
hbwang@jcwkyl:/home/hbwang$sed -e '/^[ \t]*$/{:a;N;s/^\([ \t]*\n\)\+/\1/;ta}' test abc de f 1 2 345 en d int main () { printf("hello\n"); return 0; } last empty
- 方法二:像冒泡排序算法那样,将相邻的两个空行替换成一个空行。实现:
hbwang@jcwkyl:/home/hbwang$sed -e ":a; N; s/\n\([ \t]*\n\)\1/\n\1/g; ba" test abc de f 1 2 345 en d int main () { printf("hello\n"); return 0; } last empty
hbwang@jcwkyl:/home/hbwang$sed -n -e ":a ; N; l 200; ba" test abc\nde f$ <===执行到N时,pattern space中是abd$,执行命令N后,pattern space中是abd\nde f$ abc\nde f\n1 2 345$ abc\nde f\n1 2 345\nen d$ abc\nde f\n1 2 345\nen d\n$ abc\nde f\n1 2 345\nen d\n\n$ abc\nde f\n1 2 345\nen d\n\n\n$ abc\nde f\n1 2 345\nen d\n\n\n\n$ abc\nde f\n1 2 345\nen d\n\n\n\n\nint main () {$ abc\nde f\n1 2 345\nen d\n\n\n\n\nint main () {\n printf("hello\\n");$ abc\nde f\n1 2 345\nen d\n\n\n\n\nint main () {\n printf("hello\\n");\n return 0;$ abc\nde f\n1 2 345\nen d\n\n\n\n\nint main () {\n printf("hello\\n");\n return 0;\n}$ abc\nde f\n1 2 345\nen d\n\n\n\n\nint main () {\n printf("hello\\n");\n return 0;\n}\n$ abc\nde f\n1 2 345\nen d\n\n\n\n\nint main () {\n printf("hello\\n");\n return 0;\n}\n\n$ abc\nde f\n1 2 345\nen d\n\n\n\n\nint main () {\n printf("hello\\n");\n return 0;\n}\n\n\nlast empty$
- 方法三:和方法一类似,比方法一更简单的实现:
hbwang@jcwkyl:/home/hbwang$sed -e "/^[[:blank:]]*$/{h; s/.*//; :a; N; s/^\n[[:blank:]]*$//; ta; s/^\n//; H; x;}" test abc de f 1 2 345 en d int main () { printf("hello\n"); return 0; } last empty
- 方法一:将连续多个空行变成一个,如果只有一个空行,则不变。有一个简单思路:如果当前行是空行,则删除当前行后续的所有连续空行。换一种描述:对所有的空行,删除其后续的所有连续空行。第二种描述更容易写出sed实现,因为sed基本功能就是“对什么样的行”进行“什么样的操作”。写出来的脚本如下: