最近对sed产生了浓厚的兴趣,如同一位作者所说:绝大部分人只会使用s命令,很多人也只有在替换的时候才想到sed。
直到头几天,一个群里有人提问,如何用sed显示一个文件的末尾3行,一个人给出了下面这个代码:
sed -n -e ':a;$!{N;4,${s/^[^\n]*\n//};${p;q}};b a'
是不是感觉看这个代码跟看天书一样?反正我看了半天不解,就存下来了。后来仔细看了一些sed的说明文档,终于略懂皮毛。
上面这个命令是这样的:
:a; #建立一个名叫a的label
$!{.....} # $符号表示最后一行,!表示不执行后面的命令,所以这个是除了最后一行都执行括号内的命令。
N; # 将下一行读入缓冲区
4,${......} # 对第四行至末位行执行括号内的命令
s/^[^\n]*\n// # 这个命令是最常见的s替换命令,是一个正则,含义是将一行(包含最后回车符)替换为空
${p;q} # 对最后一行执行花括号的内容,p命令是打印缓冲区内容,q命令是退出sed
b a # b命令是break,a是前面的那个label,跳转到label处
所以综合起来,这个命令就是说,执行一个循环,循环中除了最后一行,其余的行都是将下一行内容添加到缓冲区,并且从第四行开始到最后,每次把缓冲区最前面的一行删掉,到达最后一行时,打印并退出循环。
所以说,sed并不难,关键在于它的命令都是单个字符,零丁一看,很晦涩。其实删除缓冲区还有更方便的命令D,所以可以改成:
sed -n -e ':a;${p;q};N;4,$D;b a'
这样是不是更简短一些呢?
后来我又想到了一个需求,就是在做ACM题的时候,经常重复造轮子,有时候觉得一段代码写过,但是不记得放到哪了,或者懒得找了,就自己又写一遍。如果写成公共组件,每次只要include一个头文件该多好啊,但是提交的时候,又要替换成实际代码,很麻烦,我试过“gcc -E”选项,生成的预处理文件比较大,因为他把系统头文件也展开了,并且输出格式也不友好,一般OJ不认的。所以我用C语言写了一个展开的工具,只展开#include "xxx"这样的,而不展开#include <xxx>这样的。
再后来小小董@aikilis提醒我,可能sed也可以做这件事,我就埋头钻研,终于被我弄成功了,输出结果和我C语言的版本一样,但是只用了两行,很激动!
最开始比较困扰我的是怎么在sed内部打开一个新的文件,用r命令限制很多,至少我目前遇到的,它后面只能接一个固定参数,不能是一个可变的,而且不能用分号结束r命令,它会统一认为是文件名,只能换行。后来我发现s命令有一个e选项,可以调用shell执行目前pattern space的文本内容,所以事情就变得简单了。
sed -e 's/^#include[ \t]\+"\(.*\)"/cat \1/e'
就是遇到开头是#include "haha.h"这样的字符串,将文本内容替换为cat haha.h,然后调用shell执行它。其中'\1'是正则匹配里面第一个圆括号的内容。
如果仅仅是这样,那么当haha.h里面有#include "hehe.h"的时候,是无法继续展开的,所以我们要用到递归,可是递归的话,直接在这一行里面写,那这行真的是无限长了,所以考虑用sed的另一个特性-f参数,可以将命令写在一个文件里,直接执行文件里的命令。这个命令文件就是一个函数封装,可以递归调用,不妨起名叫expand.sed,内容是:
s%^#include[ \t]\+"\(.*\)"%cat \1 | sed -f "/path/to/expand.sed";%e
注意因为路径里含有/符号,我就用%作为s命令的分隔符了,这个和前面那个差不多,只不过cat后面加了一个管道,继续调用sed命令进行处理,而且使用的命令就是这个expand.sed本身。
然后在外面封装一个脚本调用就OK了。
$ cat /path/to/myexpand
#! /bin/sh -
sed -f /path/to/expand.sed $1
注意我的脚本叫myexpand,这是因为expand是shell的内建命令,所以你的程序千万不要叫expand,否则不会调用到的。
update 2014-07-21
那个取最后三行的代码,还可以再短
sed -n -e ':a;$p;$q;N;4,$D;b a'
expand里面用到了cat,如果头文件不在当前路径,会产生问题,需要用一个脚本替换命令cat,这个脚本实现的功能就是在环境变量INCLUDE中所包含的路径中去查找头文件,当然,需要实现将你常用的头文件路径放到环境变量INCLUDE中。
find `echo $INCLUDE |tr ':' ' '` -name $1 |head -1