The Advanced Usage of Sed

这里总结的是 sed 的“高级”用法,主要集中在 patten space 和 hold space。

注意:在进行一些文本处理前,需要注意文本行数的奇偶性,这个会影响到最终的结果。

首先描述下 sed 这个流编辑器的工作流程:

1.从标准输入/文件中读取一行
2.去除改行的换行符\n
3.将该行放入 pattern buffer(模式空间,或者 patten space)
4.根据 sed 提供的命令修改 pattern buffer
5.打印 pattern buffer 至标准输出

通常,一行被读入 patten space 并且用脚本中的每个命令(一个接一个地)应用于那一行,当到达脚本的底部时,输出这一行并且清空 patten space。然后新行被读入 patten space,如此循环至行末。总之,patten space 和 hold space 都是为了做同样一件事情:改变执行或控制的流程顺序

正如名字提示的那样,hold space 是用来存放从 pattern space 中复制/追加过来的内容。hold space 中的内容不会直接被操作。sed 提供了一套 hold/get 函数来处理上述的问题。

h 函数
h(hold)函数将 pattern space 的内容复制到 holding space 里,原来 holding space 里面的内容将被覆盖

H 函数
H 函数将 pattern space 的内容追加到 hold space 中,追加的内容跟 hold space 原来的内容以换行符相分隔

g 函数
g(get) 函数将 hold space 中的内容拷贝至 pattern space

G 函数
G 函数将 hold space 中的内容追加至 pattern space 中,追加的内容跟原来以换行符相分隔

x 函数
x 使用来交换 pattern space 和 hold space 中的内容

除了 h/H,g/G,还有几对容易混淆的:

d 函数
删除 patten space 的内容,开始新的循环

D 函数
删除 patten space 第一行的内容,开始新的循环

n 函数
将标准输入的下一行覆盖到 patten space

N 函数
将标准输入的下一行追加到 patten space

p 函数
打印出当前的 patten space

P 函数
打印出当前 patten space 中的第一行

w 函数
将 patten space 当前的内容写到文件中去

W 函数
将 patten space 中的第一行写到文件中去

$ cat linux.txt
Archlinux
CentOS
CentOS
Ubuntu

Debian
Gentoo
Gentoo
Redhat

1) 将连续的两行通过 @ 这个符号合并:

$ sed -e N -e 's/\n/ @ /' linux.txt
$ sed  '{N;s/\n/ @ /}' linux.txt    // 二者均可

上面的语句
1.sed 首先读取第一行,将其放进 pattern space,通过 N 读取下一行内容,然后将其追加到 pattern space,通过换行符来分隔。因此现在的 pattern space 看起来是这样的:firstline\nsecondline
2.接下来执行 s/\n/ @ /,也就是用 @ 替换掉 \n,然后打印出 pattern space 中的内容。
3.接着循环进行,直至结束。

2) 给每一个非空行加上行号:

$ sed '/./=' linux.txt | sed 'N;s/\n/ /'

1.第一个 sed 语句打印出行号:如果不是空行,就将原始的内容打印在行号的下面一行
2.第二个 sed 语句将原始的语句通过 N 添加到行号上去

上面的语句适用于行数为偶数时,当行数为奇数时,就会出现排版错位。解决办法就是在第二个 sed 语句中加上非空行的判断:

$ sed '/./=' linux.txt | sed '/./{N;s/\n/ /}'

3) 删除连续的两行空行:

$ sed '/^$/{N;/^\n$/d}' linux.txt

下面这个只能删除空行,但是不一定是连续的空行

$ sed '/^$/d' linux.txt

4) 删除文件的最后两行:

在执行之前,先看两个命令:

P-打印出 patten space 的第一行(包含 \n)
D-从 patten space 删除第一行,让后跳回至命令的第一句继续执行,即在不改变行号的情况下从头执行

$ sed 'N;$!P;$!D;$d' linux.txt

1.读取第一行,将其放至 patten space
2.N 命令读取接下来的一行,将其追加到 patten space 中,现在就是这样:firstline\nsecondline
3.如果没有到达最后一行($),打印出首行,接着从 patten space 中删除首行。如此循环
4.最后,当 patten space 中像 9th\n10th 这样时,就到达了最后一行($)了,执行删除命令 d

5) 打印出文件的最后两行:

$ sed '$!N;$!D' linux.txt

6) 删除重复的行:

$ sed '$!N;/^\(.*\)\n\1$/!P;D' linux.txt

$ cat linux.txt
#Linux
       ArchLinux
      CentOS
      Ubuntu

#Windows
       XP

#Database
       Mysql
       Oracle
       SQLserver

7) 在每一行下面增加一个新的空行,像下面这样:
#Linux

    ArchLinux

       CentOS

       Ubuntu

#Windows

       XP

#Database

       Mysql

       Oracle

       SQLserver

     

$ sed 'G' linux.txt

1.sed 读取第一行,将其放置到 patten space 中
2.G 命令将 hold space 的内容追加到 pattern space 中,以 \n 分隔,因此 pattern space 中就多了一空行
3.类似的,如果你想在每行下面增加两个空白行,连续两次将 hold space 追加到 pattern space 就可以了

8) 倒序打印出文件(类似于 tac),像下面这样:

       SQLserver
       Oracle
       Mysql
#Database

       XP
#Windows

       Ubuntu
       CentOS
       ArchLinux
#Linux

$ sed -n '1!G;h;$p' linux.txt

1.首先读取第一行“#Linux”,放到 patten space;执行 h,将 patten space 的内容复制到 hold space
2. 清空 patten space。读取第二行“ArchLinux”,放到 patten space 中;执行 1!G,将 hold space 中的“#Linux”追加到 patten space 中,现在 patten space 中为“ArchLinux\n#Linux”;执行 h,将 patten space 中的内容复制到 hold space 中
3. 清空 patten space。读取第三行“CentOS”,放到 patten space 中,执行 1!G,将 hold space 中的内容追加到 patten space 中,现在 patten space 中变为“CentOS\nArchLinux\n#Linux”;执行 h,将 patten space 中的内容复制到 hold space 中
4. 像上面的 2 或者 3 步那样往下执行。当读取到最后一行“SQLserver”,将其放到 patten space 中;将 hold space 中的内容追加到 patten space 中;将 patten space 中的内容复制到 hold space 中;执行 $p,也就是到了最后一行,打印出 patten space 中的内容

9) 打印出被 Mysql 匹配行的前一行,像下面这样:

#Database

$ sed -n '/Mysql/{g;1!p};h' linux.txt

1.每一轮,如果不匹配“Mysql”,将其复制到 hold space 中
2.如果该行匹配“Mysql”,从 hold space 中取出前一行并打印
3.如果第一行就匹配“Mysql”,此时从 hold space 中取出的为空,因此就不打印

10) 删除每一个自然段的最后一行,像下面这样:

#Linux
      ArchLinux
      CentOS

#Windows

#Database
       Mysql
       Oracle
       

$ sed -n -e '/^$/{x;d}' -e '/./x;p' linux.txt

1.如果不为空,交换 patten space 和 hold space,并打印 patten space;如果为空行,交换 patten space 和 hold space,并删除 patten space 中的内容
2.第一行不为空,将原先在 patten space 中的 “#Linux” 交换到 hold space 中,打印一个空行
3.第二行不为空,将原先在 patten space 中的 “ArchLinux” 跟 hold space 中的“#Linux”交换,打印出“#Linux”。如此反复
4.当到了第五行,也就是空行时,将 hold space 中的“CentOS” 和在 patten space 中的空行交换,删除 patten space 中“CentOS”

11) 在每一行的最后添加上一行中的内容,像下面这样:

#Linux
       ArchLinux#Linux
       CentOS    ArchLinux
       Ubuntu        CentOS
       Ubuntu
#Windows
       XP#Windows
       XP
#Database
       Mysql#Database
       Oracle        Mysql
       SQLserver    Oracle
       SQLserver

$ sed 'H;x;s/^\(.*\)\n\(.*\)/\2\1/' linux.txt

1.将第一行放到 hold space 中
2.当第二行到达 patten space 时,将 patten space 中的内容追加到 hold space 中
3.调换 patten 和 hold space 中的内容,此时 patten space 中是 secondline\nfirstline;hold space 只有第二行的内容
4.通过 s 替换掉换行符,将其连接起来
5.重复上述步骤至结束

12) 将每个 tag 以行的形式组织,像下面这样:
Linux         ArchLinux
Linux         CentOS
Linux         Ubuntu
Linux
Windows         XP
Windows
Database        Mysql
Database        Oracle
Database        SQLserver

$ sed '/^#/{h;d};G;s/^\(.*\)\n#\(.*\)/\2\1/' linux.txt

1.当遇到以“#”开头的行时,将其复制到 hold space 中;将 patten space 中的内容删除;开始新的一轮
2.对于不以“#”开头的行,将 hold space 中的内容追加到 patten space 中,替换到相应的字符

下面我们继续看一些我从 google 上找到的练习。

$ cat foo
111
222
333
444
555

$ sed 'x' foo

111
222
333
444
555

$ sed 'G' foo
111

222

333

444

555

由上面的可以引申下:

$ sed '/^$/d;G' foo

删除空行,开始新的一轮(不再执行 G)

注意:
如果匹配 d/D 执行结束后,sed 不会接着执行 d/D 后面的内容,而是开始接下来新的一行。

$ sed '/444/{x;p;x}' foo
在 444 前面插入一空行

$ sed '/444/{x;p}' foo
444 的地方由两个空行取代

$ sed '/444/{p;x}' foo
$ sed '/444/G' foo        //ok
444 的下面产生一个空行

$ sed '{x;p;x}' foo
在每行的上面插入一空行

$ sed '/333/{x;p;x;G}' foo
在 333 的上下各插入一空行

$ sed 'n;G' foo
在文件的偶数行下面插入空行
n:将 patten space 中的内容复制到标准输出;用输入的下一行替代 patten space。
N:是追加到 patten space。

$ sed 'n;n;G' foo
在文件的 3,6,9 … 行插入一空行

$ sed 'n;n;n;G‘ foo
在文件的 4,8,12 … 行插入一空行

$ sed 'n;d' foo
删除偶数行

$ sed '$!N;$!D' foo
打印出最后两行,同 tail -2 foo

$ sed = foo | sed 'H;s/\n/ /'
加上行号

$ sed '1!G;h;$!d' foo
$ sed -n '1!G;h;$p' foo
行反序显示

P.S:对 vi 文件反序:
:g/./m0

$ sed '1,3{H;d};7G' foo
将 1-3 的内容移至第 7 行下面

$ cat t
0.15
0.28
0.36
0.42
5.12
43.5
234.1
3.14
将其变为:
0.15 0.28 0.36 0.42
5.12 42.5 234.1 3.14

$ sed -ne 'H;1h;4{x;s/\n/\ /gp};5h;${x;s/\n/\ /gp}' t

他的详细解释以及变种请看这里

参考:
文末的没有确切的原文来源,能有链接的都已给出;由于 thegeekstuff 的有版权问题,这里不给链接了。

  • http://www.freetstar.com freetstar

    sed+awk乃神奇也

  • http://aegiryy.net aegiryy

    流编辑器威武啊⋯⋯顶一个。

  • yaode

    3) 删除连续的两行空行:

    $ sed ‘/^$/{N;/^n$/d}’ linux.txt

    下面这个只能删除空行,但是不一定是连续的空行

    上面命令是错的,应该如下:

    sed ‘/^$/{N;d}’ linux.txt