2009-12-31

sed单行脚本学习笔记

Sed单行脚本学习笔记

redraiment, 2009-12-31

回家真好





  前段时间忙着找工作、项目结题、写报告……反正是总有做不完的事情,哈哈。好在暂时告一段落了,应老妈强烈要求回家休息几天。这次回家除了这身衣服,只带了一本《sed与awk》,我觉得这种小册子最适合茶余饭后休闲之用。如果你也有兴趣学 sed ,推荐你一起看《sed与awk》(可以在谷歌图书在线阅读英文版:D)。
  花了两天时间,看完了前面 sed 的部分。要掌握一个工具就要熟悉它的规则,man 等参考手册向我们介绍这些规则,教程则演示如何使用这些规则,但要将这些规则运用自如,还需要去理解别人的代码并尝试自己解决问题。在 SourceForge 上有份经典的文档:《SED单行脚本快速参考》(单行脚本要求命令行长度小于65个字符),由 Eric Pement 整理,Joe Hong 翻译,通篇阅读后获益良多,故撰此文和大家分享。

精彩脚本摘录

# 在每一行后面增加一空行
sed G
  在参考手册中,命令G的作用是“将换行符后的保持空间内容追加到模式空间”。就像前文提到的,看过教程后只是熟悉了规则,还不能将规则运用自如,我自己写的代码是:sed 's/$/\n/',就是因为我还不熟悉每个命令会对模式空间产生什么影响。所以看到这段参考代码时感觉眼前一亮:“原来还可以这样写!”
# 显示文件中的最后10行 (模拟“tail”)
sed -e :a -e '$q;N;11,$D;ba'
  假设文件有 N 行(N 大于10),显示最后10行也就意味着删除前的 N-10 行。在多行模式中,命令“D”可以删除模式空间中第一行;命令“N”可以将下一行追加到模式空间中,建立多行模式。因此问题转化为:“1)将整个文件的内容放入一个模式空间中;2)删除前 N-10 行。”其中问题1)通过控制语句“b”来解决:sed ':a; N; ba';至于问题二,模式“1,$”代表文本中的所有行,因此紧跟着的命令被执行N次,同理,模式“11,$”匹配后面的 N-10 行,因此“11,$D”一个执行了 N-10 次。
  其实,在 GNU sed 中,命令“$q”是可以删掉的,因为在最后一行执行命令“N”就会因出错而自动退出。
  另外,在 info 手册中也有一个解决方法:sed '1h;2,10{H;g};$q;1,9d;N;D'。他的思路差不多,只不过是将中间文本保持在“保持空间”而不是“模式空间”,因此无需通过控制语句来制造循环。但它繁琐一些,需要在两处指定地址范围。
# 显示文件中的最后2行(模拟“tail -2”命令)
sed '$!N;$!D'
  这条脚本也很精彩,命令“$!N”只能执行到倒数第二行,除了倒数第二行,命令“$!D”都能被执行,因此仅剩下最后两行未被删除。我的解决方法要麻烦一些:sed -n 'N;$p;D',命令“$p”只能执行在倒数第二行执行,并且输出最后两行。当文件只有一行时,两段脚本都没有输出。

我的解决方法

# 显示文件中的倒数第二行
sed -e '$!{h;d;}' -e x              # 当文件中只有一行时,输入空行
sed -e '1{$q;}' -e '$!{h;d;}' -e x  # 当文件中只有一行时,显示该行
sed -e '1{$d;}' -e '$!{h;d;}' -e x  # 当文件中只有一行时,不输出

# 我的解决方法
sed 'x;$!d'                         # 当文件中只有一行时,输入空行
sed '1h;1!x;$!d'                    # 当文件中只有一行时,显示该行
sed -n 'N;$P;D'                     # 当文件中只有一行时,不输出
  在解决这个问题上,参看代码显得有些繁琐。只有将每行都交换“模式空间”和“保持空间”的内容(命令“x”),并将除最后一行外所有内容删除(命令“$!d”),就能获得倒数第二行,因为“保持空间”初始化时为空,因此当文件中只有一行时输入空行;为了在文件中只有一行时能显示该行,要对第一行特殊照顾:覆盖保持空间;第三条命令你很熟悉,咋一看以为是上面“tail -2”的解决方法,它们很像,差别仅仅是“tail -2”中“p”是小写,此处是大写。
# 删除文件中的重复行,不管有无相邻。注意hold space所能支持的缓存
# 大小,或者使用GNU sed。
sed -n 'G; s/\n/&&/; /^\([ -~]*\n\).*\n\1/d; s/\n//; h; P'

# 上面的代码在 GNU sed v4.1.5 中不能正常工作,但在 v4.0.7 中却可以执行。
# 修改后
sed -n 'G; s/\n/&&/; /^\([^\n]*\n\).*\n\1/d; s/\n//; h; P'
  参考代码中运用了一个小技巧:用模式“[ -~]”来匹配所有可打印字符。可打印字符的ASCII范围是0x20-0x7F,而0x20和0x7F分别是空格和波浪线。但这个技巧不能在 GNU sed v4.1.5 中使用(但在 v4.0.7 中却可以使用)。为了使代码通用,需要改为“[^\n]”。
# 只保留多个相邻空行的第一行。并且删除文件顶部和尾部的空行。
# (模拟“cat -s”)
sed '/./,/^$/!d'        #方法1,删除文件顶部的空行,允许尾部保留一空行
sed '/^$/N;/\n$/D'      #方法2,允许顶部保留一空行,尾部不留空行
  在我的环境里测试,方法2尾部同样保留一个空行。

我的单行脚本

  我看的兴起,也设计了一个单行脚本。问题来源于设计宏替换器,比如C语言中有如下定义:
#define PRINT printf
PRINT("printf with PRINT");
  此时使用 s/PRINT/printf/g 就会把字符串中的“PRINT”也替换掉。因此需要使用一下脚本:
# 只替换不在字符串内的模式
sed -r 's/^|"[^"]*"/&\n/g; :a; s/(\n[^"]*)foo/\1bar/; ta; s/\n//g'

# 只替换字符串中的模式
sed -r 's/^|"[^"]*"/\n&/g; :a; s/(\n"[^"]*)1/\1x/; ta; s/\n//g'
  替换不在字符串内模式的原理是:
  1. 先在起始位置和字符串的第二个引号后面添加换行符(s/^|"[^"]*"/&\n/g);
  2. 替换所有将换行符和第一个引号之间的模式,这些字符都不在字符串里面(s/(\n[^"]*)foo/\1bar/);
  3. 迭代执行第二部,直到替换所有模式(ta);
  4. 删除所有换行符(s/\n//g)。
  其中换行符是作分隔符用,你也可以使用其他的、不在该行中的字符。替换字符串内模式的原理基本相同,只是分隔符放到字符串第一个引号的前面,并替换以引号开头的模式。下面是测试结果(将字符‘1’替换为‘x’):
$ cat string
123"123"123
111"44a"jjl
dad"111"ddd
"111"44"5555"
"111""""333"
1122
"1111"2211"1111"
4455
"9988"
"1155"
$ sed -r 's/^|"[^"]*"/&\n/g;:a;s/(\n[^"]*)1/\1x/;ta;s/\n//g' string
x23"123"x23
xxx"44a"jjl
dad"111"ddd
"111"44"5555"
"111""""333"
xx22
"1111"22xx"1111"
4455
"9988"
"1155"
$ sed -r 's/^|"[^"]*"/\n&/g;:a;s/(\n"[^"]*)1/\1x/;ta;s/\n//g' string
123"x23"123
111"44a"jjl
dad"xxx"ddd
"xxx"44"5555"
"xxx""""333"
1122
"xxxx"2211"xxxx"
4455
"9988"
"xx55"

  我的环境是 Debian Lenny + GNU sed 4.1.5。Windows版本可以到 GNUWin32 下载最新的 Sed for Windows,也可以发邮件向我索取 GNU sed.exe v4.0.7。GNU sed 自 v3.02.80 起可以使用转义字符'\t'来代替制表符,其他大部分他版本还不能识别'\t'的简写方式。下面摘录《sed与awk》中很好玩的一段话:
  像许多程序一样,sed脚本通常以开始都很小,并且写和读都很简单。在测试脚本时,可能会发现不适用于一般规则的特殊情况。为了解决这些问题,可以给脚本增加行,生成更长、更复杂并且更完整的脚本。虽然花费在细化脚本上的时间抵消了不用手动编辑而节省下来的时间,但至少在这段时间内,你的头脑被自己的这个似乎熟悉的想法占据:“看!计算机完成的。”
——《sed与awk》 P119

2009-12-20

戏说C语言变量

戏说C语言变量

redraiment, 2009-12-18

好玩的问题





  今早帮老师去答疑,一位同学跑来问:“使用 printf 输出 %d、%c 时,后面传的参数都是变量的值,为什么 %s 看起来和它们不一样,要传一个地址?”我说:“小伙子很有前途,一般人不问这样的问题,哈哈!”
  这个问题类似 Java 中基础类型传递值、对象传递引用,这么设计是为了提高效率。对于还没学完C语言的初学者来说,如果我给他扯一堆“底层设计”或“效率”等显然不合适,还极有可能掉进“值传递还是址传递”等文字游戏漩涡中,估计到了最后也只能在他听得晕头转向时搪塞一句“当初就是这么设计的”。为了尽快得给他满意的答复,我只要想办法让 %s “看起来”和其他标记一样就行了,于是写了如下的代码:
#include <stdlib.h>
#include <stdio.h>

typedef char STRING[80];

int main ( void )
{
    STRING s = "redraiment";
    int i = 1;

    printf("%s, %d\n", s, i);

    return EXIT_SUCCESS;
}
  然后我告诉他:“因为C语言不够抽象,让你知道了太多的底层实现细节,比如你知道字符串在内存中是以字符数组的形式保存的。现在我用 typedef 定义了一个字符串类型,把这些细节屏蔽掉。通过 STRING s; 来定义一个名字是 s 的字符串类型变量,这样就和用 double d; int i; 等方式定义变量一样,你无需了解它们在内存中如何实现。此时,对于 printf 来说,%s、%d 后面跟着的 s, i 都是一回事了,它们都是变量的名字,里面保存着不同类型的数据。”很幸运,前面的话解决了他的疑惑,让我剩下不少口水:P。

指针和数组的定义是个BUG

  我初学C语言时,也有过类似的困惑:指针和数组别样的定义方式让我以为它们有别于普通类型。所有普通类型、自定义的结构体类型的定义方式都是 TYPE name,数据类型后面紧跟着变量名。因此,就理论上来说,你定义一个指针 int* pi,其中表示指针类型的“*”应该从属于 int。但很遗憾,实际上它从属于变量名 pi,证据就是 int* pi, i; 中,变量 i 的类型是 int,而不是一个指针。
  数组同样存在这样的问题:“[]”从属于变量名。解决的办法就是将指针或数组自定义成新类型,比如:
typedef int* PINT;
PINT p1, p2;
  此时 p1 和 p2 都是指针。切忌不能用宏来解决,它依然会存在上面的问题。

神奇的声明

  每次提起变量声明时,我都会想起《C专家编程》里“骑士和公主”的故事。这是一个很绕嘴的故事(国外诗歌的名字读不习惯),我姑且用毛主席的《沁园春·雪》打个类似的比方。“沁园春”是词牌名,它“(仄)仄平平,(仄)仄平平,仄仄仄平(韵)……”;只要按照这种词牌格式写的词,都被称作“沁园春”;这首词本身的名字称作“雪”;“雪”这首词的内容是“北国风光,千里冰封,万里雪飘……”。把前面关键词(加粗)部分列于表中:

被称作
词的名字沁园春“(仄)仄平平……”
“北国风光 ……”
  也许你还不是很了解,我们再把文章开头的问题也罗列成表格:

被称作
变量的类型STRINGchar [80]
变量s"redraiment"
  现在清晰了,你明白我们的毛主席也是一个编程高手,他创建了“沁园春 雪, 国情, 长沙;”等多首词。