Sed是什么

《sed and awk》一书中(1.2 A Stream Editor)是这样解释的:

Sed is a "non-interactive" stream-oriented editor. It is stream-oriented because, like many UNIX
programs, input flows through the program and is directed to standard output.

Sed本质上是一个编辑器,但是它是非交互式的,这点与VIM不同;同时它又是面向字符流的,输入的字符流经过Sed的处理后输出。这两个特性使得Sed成为命令行下面非常有用的一个处理工具。

如果对sed的历史有兴趣,可以看书中2.1节"Awk, by Sed and Grep, out of Ed",这里就不多介绍。

基本概念

sed命令的语法如下所示:

sed [options] script filename

同大多数Linux命令一样,sed也是从stdin中读取输入,并且将输出写到stdout,但是当filename被指定时,则会从指定的文件中获取输入,输出可以重定向到文件中,但是需要注意的是,该文件绝对不能与输入的文件相同。

options是指sed的命令行参数,这一块并不是重点,参数也不多。

script是指需要对输入执行的一个或者多个操作指令(instruction),sed会依次读取输入文件的每一行到缓存中并应用script中指定的操作指令,因此而带来的变化并不会影响最初的文件(注:如果使用sed时指定-i参数则会影响最初的文件)。如果操作指令很多,为了不影响可读性,可以将其写到文件中,并通过-f参数指定scriptfile:

sed -f scriptfile filename

这里有一个建议,在命令行中指定的操作指令最好用单引号引起来,这样可以避免shell对特殊字符的处理(如空格、$等,关于这一点可以参考简洁的Bash编程技巧续篇 - 9. 引号之间的区别)。这个建议同样适用grep/awk等命令,当然如果有时候确实不适合使用单引号时,记得对特殊字符转义。

无论是将操作指令通过命令行指定,还是写入到文件中作为一个sed脚本,必须包含至少一个指令,否则用sed就没有意义了。一般会同时指定多个操作指令,这时候指令之间的顺序就显得非常重要。而你的脑海中必须有这么一个概念,即每个指令应用后,当前输入的行会变成什么样子。要做到这一点首先必须要了解sed的工作原理,要做到“知其然,且知其所以然”。

每条操作指令由pattern和procedure两部分组成,pattern一般是用'/'分隔的正则表达式(在sed中也有可能是行号,具体参见Sed命令地址匹配问题总结),而procedure则是一连串编辑命令(action)。

sed的处理流程,简化后是这样的:

  1. 读入新的一行内容到缓存空间;
  2. 从指定的操作指令中取出第一条指令,判断是否匹配pattern;
  3. 如果不匹配,则忽略后续的编辑命令,回到第2步继续取出下一条指令;
  4. 如果匹配,则针对缓存的行执行后续的编辑命令;完成后,回到第2步继续取出下一条指令;
  5. 当所有指令都应用之后,输出缓存行的内容;回到第1步继续读入下一行内容;
  6. 当所有行都处理完之后,结束;

具体流程见下图:
simple sed process flow chart

简单例子

概念如果脱离实际的例子就会显得非常枯燥无趣,这本书在这一点上处理得很好,都是配上实际的例子来讲解的。不过有点遗憾的是,书中的例子大多是与troff macro有关的,我们很难有条件来实际测试例子,在笔记中我会尽量使用更加浅显易懂的例子来说明。

下面是摘自书中的一个例子,假设有个文件list:

John Daggett, 341 King Road, Plymouth MA
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury MA
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston MA

这个例子中用到的知识点都会在后文中介绍,你可以先知道有这个东西就行。

假如,现在打算将MA替换成Massachusetts,可以使用替换命令s:

$ sed -e 's/MA/Massachusetts/' list
John Daggett, 341 King Road, Plymouth Massachusetts
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury Massachusetts
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston Massachusetts

这里面的-e选项是可选的,这个参数只是在命令行中同时指定多个操作指令时才需要用到,如:

$ sed -e 's/ MA/, Massachusetts/' -e 's/ PA/, Pennsylvania/' list 
John Daggett, 341 King Road, Plymouth, Massachusetts
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls, Pennsylvania
Eric Adams, 20 Post Road, Sudbury, Massachusetts
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston, Massachusetts

即使在多个操作指令的情况下,-e参数也不是必需的,我一般不会加-e参数,比如上面的例子可以换成下面的写法:

$ sed 's/ MA/, Massachusetts/;s/ PA/, Pennsylvania/' list 

操作指令之间可以用逗号分隔,这点和shell命令可以用逗号分隔是一样的。

这里提前说一点使用s替换命令需要小心的地方,不要忘记结尾的斜杠,如果你遇到下面的错误说明你犯了这个错误:

$ sed -e 's/ MA/, Massachusetts' list
sed: -e expression #1, char 21: unterminated `s' command

假如这时,我只想输出替换命令影响的行,而不想输出文件的所有内容,需要怎么办?这个时候可以利用替换命令s的p选项,它会将替换后的内容打印到标准输出。但是仅仅这样是不够的,否则输出的结果并非我们所想要的:

$ sed -e 's/ MA/, Massachusetts/p' list
John Daggett, 341 King Road, Plymouth, Massachusetts
John Daggett, 341 King Road, Plymouth, Massachusetts
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury, Massachusetts
Eric Adams, 20 Post Road, Sudbury, Massachusetts
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston, Massachusetts
Sal Carpenter, 73 6th Street, Boston, Massachusetts

从上面可以看出,文件的所有行都被打印到标准输出,并且被替换命令修改的行输出了两遍。造成这个问题的原因是,sed命令应用完所有指定之后默认会将当前行打印输出,所以就有了上面的结果。解决方法是在使用sed命令是指定-n参数,该参数会抑制sed默认的输出:

$ sed -n 's/ MA/, Massachusetts/p' list
John Daggett, 341 King Road, Plymouth, Massachusetts
Eric Adams, 20 Post Road, Sudbury, Massachusetts
Sal Carpenter, 73 6th Street, Boston, Massachusetts

正则表达式

书中的第3章的内容都是介绍正则表达式,这是因为sed的很多地方都需要用到正则表达式,比如操作指令的pattern部分以及替换命令中用到的匹配部分。关于正则这部分可以看Linux/Unix工具与正则表达式的POSIX规范,文章中已经讲得非常清楚了,我自已在忘记一些内容的时候也是去查阅这篇文章。

 

sed并非是将一个编辑命令分别应用到每一行,然后再取下一个编辑命令。恰恰相反,sed是以行的方式来处理的。另外一方面,每一行都是被读入到一块缓存空间,该空间名为模式空间(pattern space),这是一个很重要的概念,在后文中会多次被提及。因此sed操作的都是最初行的拷贝,同时后续的编辑命令都是应用到前面的命令编辑后输出的结果,所以编辑命令之间的顺序就显得格外重要。

简单例子

让我们来看一个非常简单的例子,将一段文本中的pig替换成cow,并且将cow替换成horse:

$ sed 's/pig/cow/;s/cow/hores/' input

初看起来好像没有问题,但是实际上是错误的,原因是第一个替换命令将所有的pig都替换成cow,紧接着的替换命令是基于前一个结果处理的,将所有的cow都替换成horse,造成的结果是全部的pig/cow都被替换成了horse,这样违背了我们的初衷。

在这种情况下,只需要调换下两个编辑命令的顺序:

$ sed 's/cow/hores/;s/pig/cow/' input

模式空间的转换

sed只会缓存一行的内容在模式空间,这样的好处是sed可以处理大文件而不会有任何问题,不像一些编辑器因为要一次性载入文件的一大块内容到缓存中而导致内存不足。下面用一个简单的例子来讲解模式空间的转换过程,如下图所示:

模式空间的转换

现在要把一段文本中的Unix System与UNIX System都要统一替换成The UNIX Operating System,因此我们用两句替换命令来完成这个目的:

s/Unix /UNIX /
s/UNIX System/UNIX Operating System/

对应上图,过程如下:

  1. 首先一行内容The Unix System被读入模式空间;
  2. 应用第一条替换命令将Unix替换成UNIX;
  3. 现在模式空间的内容变成The UNIX System;
  4. 应用第二条替换命令将UNIX System替换成UNIX Operating System;
  5. 现在模式空间的内容变成The UNIX Operating System;
  6. 所有编辑命令执行完毕,默认输出模式空间中的行;

地址匹配

地址匹配在sed中是非常重要的一块内容,因为它限制sed的编辑命令到底应用在哪些行上。默认情况下,sed是全局匹配的,即对所有输入行都应用指定的编辑命令,这是因为sed依次读入每一行,每一行都会成为当前行并被处理,所以s/CA/California/g会将所有输入行的CA替换成California。这一点跟vi/vim是不一样的,众所周知,vim的替换命令默认是替换当前行的内容,除非你指定%s才会作全局替换。

可以通过指定地址来限制sed的处理范围,例如只想将替换包含Sebastopol的行:

Sebastopol/s/CA/California/

/Sebastopol/是一个正则表达式匹配包含Sebastopol的行,因此像行“San Francisco, CA”则不会被替换。

sed命令中可以包含0个、1个或者2个地址(地址对),地址可以为正则表达式(如/Sebastopol/),行号或者特殊的行符号(如$表示最后一行):

  • 如果没有指定地址,默认将编辑命令应用到所有行;
  • 如果指定一个地址,只将编辑命令应用到匹配该地址的行;
  • 如果指定一个地址对(addr1,addr2),则将编辑命令应用到地址对中的所有行(包括起始和结束);
  • 如果地址后面有一个感叹号(!),则将编辑命令应用到不匹配该地址的所有行;

为了方便理解上述内容,我们以删除命令(d)为例,默认不指定地址将会删除所有行:

$ sed 'd' list 
$ 

指定地址则删除匹配的行,如删除第一行:

$ sed '1d' list 
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury MA
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston MA

或者删除最后一行,$符号在这里表示最后一行,这点要下正则表达式中的含义区别开来:

$ sed '$d' list 
John Daggett, 341 King Road, Plymouth MA
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury MA
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA

这里通过指定行号删除,行号是sed命令内部维护的一个计数变量,该变量只有一个,并且在多个文件输入的情况下也不会被重置。

前面都是以行号来指定地址,也可以通过正则表达式来指定地址,如删除包含MA的行:

$ sed '/MA/d' list 
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA

通过指定地址对可以删除该范围内的所有行,例如删除第3行到最后一行:

$ sed '2,$d' list 
John Daggett, 341 King Road, Plymouth MA

使用正则匹配,删除从包含Alice的行开始到包含Hubert的行结束的所有行:

$ sed '/Alice/,/Hubert/d' list 
John Daggett, 341 King Road, Plymouth MA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston MA

当然,行号和地址对是可以混用的:

$ sed '2,/Hubert/d' list 
John Daggett, 341 King Road, Plymouth MA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston MA

如果在地址后面指定感叹号(!),则会将命令应用到不匹配该地址的行:

$ sed '2,/Hubert/!d' list 
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury MA
Hubert Sims, 328A Brook Road, Roanoke VA

以上介绍的都是最基本的地址匹配形式,GNU Sed(也是我们用的sed版本)基于此添加了几个扩展的形式,具体可以看man手册,或者可以看我之前写的Sed命令地址匹配问题总结一文。

上面说的内容都是对匹配的地址执行单个命令,如果要执行多个编辑命令要怎么办?sed中可以用{}来组合命令,就好比编程语言中的语句块,例如:

$ sed -n '1,4{s/ MA/, Massachusetts/;s/ PA/, Pennsylvania/;p}' list 
John Daggett, 341 King Road, Plymouth, Massachusetts
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls, Pennsylvania

 

在开始之前,首先回顾上一篇的重点内容:地址匹配。上一篇中介绍过,地址可以指定0个,1个或者2个。地址的形式可以为斜杠分隔的正则表达式(例如/test/),行号(例如3,5)或者特殊符号(例如$)。如果没有指定地址,说明sed应用的编辑命令是全局的;如果是1个地址,编辑命令只是应用到匹配的那一行;如果是一对地址,编辑命令则应用到该地址对匹配的行范围。关于地址匹配的内容具体可以看Sed命令地址匹配问题总结

书中说,对于sed编辑命令的语法有两种约定,分别是

[address]command               # 第一种
[line-address]command          # 第二种

第一种[address]是指可以为任意地址包括地址对,第二种[line-address]是指只能为单个地址。文中指出,例如i(后方插入命令)、a(前方追加命令)以及=(打印行号命令)等都属于第二种命令,但是实际上我在ArchLinux上测试,指定地址对也没有报错。不过为了兼容性,建议按作者所说的做。

以下是要介绍的全部基础命令:

名称命令语法说明
替换 s [address]s/pattern/replacement/flags 替换匹配的内容
删除 d [address]d 删除匹配的行
插入 i [line-address]i\

text
在匹配行的前方插入文本
追加 a [line-address]a\

text
在匹配行的后方插入文本
行替换 c [address]c\

text
将匹配的行替换成文本text
打印行 p [address]p 打印在模式空间中的行
打印行号 = [address]= 打印当前行行号
打印行 l [address]l 打印在模式空间中的行,同时显示控制字符
转换字符 y [address]y/SET1/SET2/ 将SET1中出现的字符替换成SET2中对应位置的字符
读取下一行 n [address]n 将下一行的内容读取到模式空间
读文件 r [line-address]r file 将指定的文件读取到匹配行之后
写文件 w [address]w file 将匹配地址的所有行输出到指定的文件中
退出 q [line-address]q 读取到匹配的行之后即退出

替换命令: s

替换命令的语法是:

[address]s/pattern/replacement/flags

其中[address]是指地址,pattern是替换命令的匹配表达式,replacement则是对应的替换内容,flags是指替换的标志位,它可以包含以下一个或者多个值:

  • n: 一个数字(取值范围1-512),表明仅替换前n个被pattern匹配的内容;
  • g: 表示全局替换,替换所有被pattern匹配的内容;
  • p: 仅当行被pattern匹配时,打印模式空间的内容;
  • w file:仅当行被pattern匹配时,将模式空间的内容输出到文件file中;

如果flags为空,则默认替换第一次匹配:

$ echo "column1 column2 column3 column4" | sed 's/ /;/'
column1;column2 column3 column4

如果flags中包含g,则表示全局匹配:

$ echo "column1 column2 column3 column4" | sed 's/ /;/g'
column1;column2;column3;column4

如果flags中明确指定替换第n次的匹配,例如n=2:

$ echo "column1 column2 column3 column4" | sed 's/ /;/2'
column1 column2;column3 column4

当替换命令的pattern与地址部分是一样的时候,比如/regexp/s/regexp/replacement/可以省略替换命令中的pattern部分,这在单个编辑命令的情况下没多大用处,但是在组合命令的场景下还是能省不少功夫的,例如下面是摘自文中的一个段落:

The substitute command is applied to the lines matching the address. If no
address is specified, it is applied to all lines that match the pattern, a
regular expression. If a regular expression is supplied as an address, and no
pattern is specified, the substitute command matches what is matched by the
address.  This can be useful when the substitute command is one of multiple
commands applied at the same address. For an example, see the section "Checking
Out Reference Pages" later in this chapter.

现在要在substitute command后面增加("s"),同时在被修改的行前面增加+号,以下是使用的sed命令:

$ sed '/substitute command/{s//&("s")/;s/^/+ /}' paragraph.txt 

这里我们用到了组合命令,并且地址匹配的部分和第一个替换命令的匹配部分是一样的,所以后者我们省略了,在replacement部分用到了&这个元字符,它代表之前匹配的内容,这点我们在后面介绍。执行后的结果为:

+ The substitute command("s") is applied to the lines matching the address. If no
address is specified, it is applied to all lines that match the pattern, a
regular expression. If a regular expression is supplied as an address, and no
+ pattern is specified, the substitute command("s") matches what is matched by the
+ address.  This can be useful when the substitute command("s") is one of multiple
commands applied at the same address. For an example, see the section "Checking
Out Reference Pages" later in this chapter.

替换命令的一个技巧是中间的分隔符是可以更改的(这个技巧我们在简洁的Bash编程技巧续篇 - 5. 你知道sed的这个特性吗?中也曾经介绍过),这个技巧在有些地方非常有用,比如路径替换,下面是采用默认的分隔符和使用感叹号作为分隔符的对比:

$ find /usr/local -maxdepth 2 -type d | sed 's/\/usr\/local\/man/\/usr\/share\/man/'
$ find /usr/local -maxdepth 2 -type d | sed 's!/usr/local/man!/usr/share/man!'

替换命令中还有一个很重要的部分——replacement(替换内容),即将匹配的部分替换成replacement。在replacemnt部分中也有几个特殊的元字符,它们分别是:

  • &: 被pattern匹配的内容;
  • \num: 被pattern匹配的第num个分组(正则表达式中的概念,..括起来的部分称为分组;
  • \: 转义符号,用来转义&,\, 回车等符号

&元字符我们已经在上面的例子中介绍过,这里举个例子介绍下\num的用法。在Linux系统中,默认都开了几个tty可以让你切换(ctrl+alt+1, ctrl+alt+2 ...),例如CentOS 6.0+的系统默认是6个:

[root@localhost ~]# grep -B 1 ACTIVE_CONSOLES /etc/sysconfig/init 
# What ttys should gettys be started on?
ACTIVE_CONSOLES=/dev/tty[1-6]

可以通过修改上面的配置来减少开机启动的时候创建的tty个数,比如我们只想要2个:

[root@localhost ~]# sed -r -i 's!(ACTIVE_CONSOLES=/dev/tty\[1-)6]!\12]!' /etc/sysconfig/init 
ACTIVE_CONSOLES=/dev/tty[1-2]

其中-i参数表示直接修改原文件,-r参数是指使用扩展的正则表达式(ERE),扩展的正则表达式中分组的括号不需要用反斜杠转义"(ACTIVE_CONSOLES=/dev/tty\[1-)",这里[是有特殊含义的(表示字符组),所以需要转义。在替换的内容中使用\1来引用这个匹配的分组内容,1代表分组的编号,表示第一个分组。

删除命令: d

删除命令的语法是:

[address]d

删除命令可以用于删除多行内容,例如1,3d会删除1到3行。删除命令会将模式空间中的内容全部删除,并且导致后续命令不会执行并且读入新行,因为当前模式空间的内容已经为空。我们可以试试:

$ sed '2,${d;=}' list
John Daggett, 341 King Road, Plymouth MA

以上命令尝试在删除第2行到最后一行之后,打印当前行号(=),但是事实上并没有执行该命令。

插入行/追加行/替换行命令: i/a/c

这三个命令的语法如下所示:

# Append 追加
[line-address]a\
text
# Insert 插入
line-address]i\
text
# Change 行替换 
[address]c\
text

以上三个命令,行替换命令(c)允许地址为多个地址,其余两个都只允许单个地址(注:在ArchLinux上测试表明,追加和插入命令都允许多个地址,sed版本为GNU sed version 4.2.1

追加命令是指在匹配的行后面插入文本text;相反地,插入命令是指匹配的行前面插入文本text;最后,行替换命令会将匹配的行替换成文本text。文本text并没有被添加到模式空间,而是直接输出到屏幕,因此后续的命令也不会应用到添加的文本上。注意,即使使用-n参数也无法抑制添加的文本的输出。

我们用实际的例子来简单介绍下这三个命令的用法,依然用最初的文本list:

$ cat list
John Daggett, 341 King Road, Plymouth MA
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury MA
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston MA

现在,我们要在第2行后面添加'------':

$ sed '2a\
--------------------------------------
' list
John Daggett, 341 King Road, Plymouth MA
Alice Ford, 22 East Broadway, Richmond VA
--------------------------------------
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury MA
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston MA

或者可以在第3行之前插入:

$ sed '3i\
--------------------------------------
' list
John Daggett, 341 King Road, Plymouth MA
Alice Ford, 22 East Broadway, Richmond VA
--------------------------------------
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
Terry Kalkas, 402 Lans Road, Beaver Falls PA
Eric Adams, 20 Post Road, Sudbury MA
Hubert Sims, 328A Brook Road, Roanoke VA
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
Sal Carpenter, 73 6th Street, Boston MA

我们来测试下文本是否确实没有添加到模式空间,因为模式空间中的内容默认是会打印到屏幕的:

$ sed -n '2a\
--------------------------------------
' list
--------------------------------------

通过-n参数来抑制输出后发现插入的内容依然被输出,所以可以判定插入的内容没有被添加到模式空间。

使用行替换命令将第2行到最后一行的内容全部替换成'----':

$ sed '2,$c\
--------------------------------------
' list
John Daggett, 341 King Road, Plymouth MA
--------------------------------------

打印命令: p/l/=

这里纯粹的打印命令应该是指p,但是因为后两者(l和=)和p差不多,并且相对都比较简单,所以这里放到一起介绍。

这三个命令的语法是:

[address]p
[address]=
[address]l

p命令用于打印模式空间的内容,例如打印list文件的第一行:

$ sed -n '1p' list
John Daggett, 341 King Road, Plymouth MA

l命令类似p命令,不过会显示控制字符,这个命令和vim的list命令相似,例如:

$ echo "column1 column2 column3^M" | sed -n 'l'
column1\tcolumn2\tcolumn3\r$

=命令显示当前行行号,例如:

$ sed '=' list
1
John Daggett, 341 King Road, Plymouth MA
2
Alice Ford, 22 East Broadway, Richmond VA
3
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK
4
Terry Kalkas, 402 Lans Road, Beaver Falls PA
5
Eric Adams, 20 Post Road, Sudbury MA
6
Hubert Sims, 328A Brook Road, Roanoke VA
7
Amy Wilde, 334 Bayshore Pkwy, Mountain View CA
8
Sal Carpenter, 73 6th Street, Boston MA

转换命令: y

转换命令的语法是:

[address]y/SET1/SET2/

它的作用是在匹配的行上,将SET1中出现的字符替换成SET2中对应位置的字符,例如1,3y/abc/xyz/会将1到3行中出现的a替换成x,b替换成y,c替换成z。是不是觉得这个功能很熟悉,其实这一点和tr命令是一样的。可以通过y命令将小写字符替换成大写字符,不过命令比较长:

$ echo "hello, world" | sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/'
HELLO, WORLD

使用tr命令来转换成大写:

$ echo "hello, world" | tr a-z A-Z                                                    
HELLO, WORLD

取下一行命令: n

取下一行命令的语法为:

[address]n

n命令为将下一行的内容提前读入,并且将之前读入的行(在模式空间中的行)输出到屏幕,然后后续的命令会应用到新读入的行上。因此n命令也会同d命令一样改变sed的控制流程。

书中给出了一个例子来介绍n的用法,假设有这么一个文本:

$ cat text
.H1 "On Egypt"

Napoleon, pointing to the Pyramids, said to his troops:
"Soldiers, forty centuries have their eyes upon you."

现在要将.H1后面的空行删除:

$ sed '/.H1/{n;/^$/d}' text
.H1 "On Egypt"
Napoleon, pointing to the Pyramids, said to his troops:
"Soldiers, forty centuries have their eyes upon you."

读写文件命令: r/w

读写文件命令的语法是:

[line-address]r file
[address]w file

读命令将指定的文件读取到匹配行之后,并且输出到屏幕,这点类似追加命令(a)。我们以书中的例子来讲解读文件命令。假设有一个文件text:

$ cat text
For service, contact any of the following companies:
[Company-list]
Thank you.

同时我们有一个包含公司名称列表的文件company.list:

$ cat company.list 
Allied
Mayflower
United

现在我们要将company.list的内容读取到[Company-list]之后:

$ sed '/^\[Company-list]/r company.list' text
For service, contact any of the following companies:
[Company-list]
Allied
Mayflower
United
Thank you.

更好的处理应当是用公司名称命令替换[Company-list],因此我们还需要删除[Company-list]这一行:

$ sed '/^\[Company-list]/r company.list;/^\[Company-list]/d;' text
For service, contact any of the following companies:
[Company-list]
Thank you.

但是结果我们非但没有删除[Company-list],而且company.list的内容也不见了?这是怎么回事呢?

下面是我猜测的过程,读文件的命令会将r空格后面的所有内容都当成文件名,即company.list;/^\[Company-list]/d;,然后读取命令的时候发现该文件不存在,但是sed命令读取不存在的文件是不会报错的。所以什么事都没干成。

我们用-e选项将两个命令分开就正常了:

$ sed -e '/^\[Company-list]/r company.list' -e '/^\[Company-list]/d;' text
For service, contact any of the following companies:
Allied
Mayflower
United
Thank you.

写命令将匹配地址的所有行输出到指定的文件中。假设有一个文件内容如下,前半部分是人名,后半部分是区域名称:

$ cat text 
Adams, Henrietta Northeast
Banks, Freda South
Dennis, Jim Midwest
Garvey, Bill Northeast
Jeffries, Jane West
Madison, Sylvia Midwest
Sommes, Tom South

现在我们要将不同区域的人名字写到不同的文件中:

$ sed '/Northeast$/w region.northeast
> /South$/w region.south
> /Midwest$/w region.midwest
> /West$/w region.west' text
Adams, Henrietta Northeast
Banks, Freda South
Dennis, Jim Midwest
Garvey, Bill Northeast
Jeffries, Jane West
Madison, Sylvia Midwest
Sommes, Tom South

查看输出的文件:

$ cat region.
region.midwest    region.northeast  region.south      region.west 
$ cat region.midwest     
Dennis, Jim Midwest
Madison, Sylvia Midwest

我们也可以试试将所有的w命令放到一起用分号分隔,发现会出下面的错误,它把w空格后面的部分当作文件名,这样就验证了上面的猜测:

$ sed '/Northeast$/w region.northeast;/south$/w region.south;' text
sed: couldn't open file region.northeast;/south$/w region.south;: No such file or directory

退出命令: q

退出命令的语法是

[line-address]q

当sed读取到匹配的行之后即退出,不会再读入新的行,并且将当前模式空间的内容输出到屏幕。例如打印前3行内容:

$ sed '3q' list
John Daggett, 341 King Road, Plymouth MA
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK

打印前3行也可以用p命令:

$ sed -n '3p' list
John Daggett, 341 King Road, Plymouth MA
Alice Ford, 22 East Broadway, Richmond VA
Orville Thomas, 11345 Oak Bridge Road, Tulsa OK

但是对于大文件来说,前者比后者效率更高,因为前者读取到第N行之后就退出了。后者虽然打印了前N行,但是后续的行还是要继续读入,只不会不作处理。

到此为止,sed基础命令的部分就介绍完了。下面一篇的内容会在最近几天更新上来,主要会介绍sed高级命令。不过一般情况下,会使用基础命令已经差不多了,高级命令不一定需要去了解。

下面是我整理的一份思维导图(附.xmind文件下载地址):
sed基础命令

上一篇中介绍的基础命令都是面向行的,一般情况下,这种处理并没有什么问题,但是当匹配的内容是错开在两行时就会有问题,最明显的例子就是某些英文单词会被分成两行。

幸运地是,sed允许将多行内容读取到模式空间,这样你就可以匹配跨越多行的内容。本篇笔记主要介绍这些命令,它们能够创建多行模式空间并且处理之。其中,N/D/P这三个多行命令分别对应于小写的n/d/p命令,后者我们在上一篇已经介绍。它们的功能是类似的,区别在于命令影响的内容不同。例如D命令与d命令同样是删除模式空间的内容,只不过d命令会删除模式空间中所有的内容,而D命令仅会删除模式空间中的第一行。

读下一行:N

N命令将下一行的内容读取到当前模式空间,但是下n命令不一样的地方是N命令并没有直接输出当前模式空间中的行,而是把下一行追加到当前模式空间,两行之间用回车符\n连接,如下图所示:

模式空间包含多行之后,正则表达式的^/$符号的意思就变了,^是匹配模式空间的最开始而非行首,$是匹配模式空间的最后位置而非行尾。

书中给的第一例子,是替换以下文本中的"Owner and Operator Guide"为"Installation Guide",正如我们在本篇开头说的Owner and Operator Guide跨越在两行:

Consult Section 3.1 in the Owner and Operator
Guide for a description of the tape drives
available on your system.

我们用N命令试下手:

$ sed '/Operator$/{N;s/Owner and Operator\nGuide/Installation Guide/}' text
Consult Section 3.1 in the Installation Guide for a description of the tape drives
available on your system.

不过这个例子有两个局限:

  • 我们知道Owner and Operator Guide分割的位置;
  • 执行替换命令后,前后两行拼接在一起,导致这行过长;

第二点,可以这样解决:

$ sed '/Operator$/{N;s/Owner and Operator\nGuide /Installation Guide\n/}' text
Consult Section 3.1 in the Installation Guide
for a description of the tape drives
available on your system.

现在去掉第1个前提,我们引入更加复杂的测试文本,这段文本中Owner and Operator Guide有位于一行的,也有跨越多行的情况:

$ cat text
Consult Section 3.1 in the Owner and Operator
Guide for a description of the tape drives
available on your system.

Look in the Owner and Operator Guide shipped with your system.

Two manuals are provided including the Owner and
Operator Guide and the User Guide.

The Owner and Operator Guide is shipped with your system.

相应地修改下之前执行的命令,如下所示:

$ sed 's/Owner and Operator Guide/Installation Guide/
/Owner/{
N
s/ *\n/ /
s/Owner and Operator Guide */Installation Guide\
/
}' text
Consult Section 3.1 in the Installation Guide
for a description of the tape drives
available on your system.

Look in the Installation Guide shipped with your system.

Two manuals are provided including the Installation Guide
and the User Guide.

The Installation Guide is shipped with your system.

这里我们首先将在单行出现的Owner and Operator Guide替换为Installation Guide,然后再寻找匹配Owner的行,匹配后读取下一行的内容到模式空间,并且将中间的换行符替换成空格,最后再替换Owner and Operator Guide。

解释起来很简单,但是中间还是有些门道的。比如你可能觉得这里最前面的s/Owner and Operator Guide/Installation Guide/命令是多余的,假设你删除这一句:

$ sed '/Owner/{
> N
> s/ *\n/ /
> s/Owner and Operator Guide */Installation Guide\
> /
> }' text
Consult Section 3.1 in the Installation Guide
for a description of the tape drives
available on your system.

Look in the Installation Guide
shipped with your system. 
Two manuals are provided including the Installation Guide
and the User Guide.

The Owner and Operator Guide is shipped with your system.

最明显的问题是最后一行没有被替换,原因是当最后一行的被读入到模式空间后,匹配Owner,执行N命令读入下一行,但是因为当前已经是最后一行,所以N读取替换,告诉sed可以退出了,sed也不会继续执行接下来的替换命令(注:书中说最后一行不会被打印输出,我这边测试是会输出的)。所以这里需要在使用N的时候加一个判断,即当前行为最后一行时,不读取下一行的内容:$!N,这一点在很多场合都是有用的,更改后重新执行:

$ sed '/Owner/{
> $!N
> s/ *\n/ /
> s/Owner and Operator Guide */Installation Guide\
> /
> }' text
Consult Section 3.1 in the Installation Guide
for a description of the tape drives
available on your system.

Look in the Installation Guide
shipped with your system. 
Two manuals are provided including the Installation Guide
and the User Guide.

The Installation Guide
is shipped with your system.

上面只是为了用例子说明N的用法,可能解决方案未必是最好的。

删除行:D

该命令删除模式空间中第一行的内容,而它对应的小d命令删除模式空间的所有内容。D不会导致读入新行,相反它会回到最初的编辑命令,重要应用在模式空间剩余的内容上。

后面半句开始比较难以理解,用书中的一个例子来解释下。现在我们有一个文本文件,内容如下所示,行之间有空行:

$ cat text
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命令来删除,很简单的逻辑:

$ sed '/^$/{N;/^\n$/d}' text
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命令来处理,这样做就得到预期的结果了:

$ sed '/^$/{N;/^\n$/D}' text
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$导致模式空间中的第一行被删除。现在模式空间中的内容是空的,重新执行编辑命令,此时匹配/^$/。继续读取下一行,当下一行依然为空行时,重复之前的动作,否则输出当前模式空间的内容。造成的结果是连续多个空行,只有最后一个空行是保留输出的,其余的都被删除了。这样的结果才是我们最初希望得到的。

打印行:P

P命令与p命令一样是打印模式空间的内容,不同的是前者仅打印模式空间的第一行内容,而后者是打印所有的内容。因为编辑命令全部执行完之后,sed默认会输出模式空间的内容,所以一般情况下,p和P命令都是与-n选项一起使用的。但是有一种情况是例外的,即编辑命令的执行流程被改变的情况,例如N,D等。很多情况下,P命令都是用在N命令之后,D命令之前的。这三个命令合起来,可以形成一人输入输出的循环,并且每次只打印一行:读入一行后,N继续读下一行,P命令打印第一行,D命令删除第一行,执行流程回到最开始重复该过程。

$ echo -e "line1\nline2\nline3" | sed '$!N;P;D'

不过多行命令用起来要格外小心,你要用自己的大脑去演算一遍执行的过程,要不然很容易出错,比如:

$ echo -e "line1\nline2\nline3" | sed -n 'N;1P'

你可能期望打印第一行的内容,事实上并没有输出。原因是当N继续读入第二行后,当前行号已经是2了,我们在第二篇笔记中曾经说过,行号只是sed在内部维护的一个计数变量而已,每当读入新的一行,行号就加一:

$ echo -e "line1\nline2\nline3" | sed -n '$!N;='
2
3

我们依然用替换Unix System为Unix Operating System作为例子,介绍N/P/D三个命令是如何配合使用的。

示例文本如下所示,为了举例方便,这段文本仅有三行内容,刚好可以演示一个循环的处理过程:

$ cat text
The UNIX
System and UNIX
...

执行的命令:

$ sed '/UNIX$/{
> N
> s/\nSystem/ Operating &/
> P
> D
> }' text
The UNIX Operating 
System and UNIX
...

执行过程如下图所示:
NDP Loop

以上三个命令的用法与之前介绍的基础命令是截然不同的,有些同学可能都没有接触过。在下一篇中,我会介绍更多高级的命令,同时为引入一个新的概念:保持空间(Hold Space)。

保持空间

保持空间用于保存模式空间的内容,模式空间的内容可以复制到保持空间,同样地保持空间的内容可以复制回模式空间。sed提供了几组命令用来完成复制的工作,其它命令无法匹配也不能修改模式空间的内容。

操作保持空间的命令如下所示:

名称命令说明
保存(Hold) h/H 将模式空间的内容复制或者追加到保持空间
取回(Get) g/G 将保持空间的内容复制或者追加到模式空间
交换(Exchange) x 交换模式空间和保持空间的内容

这几组命令提供了保存、取回以及交换三个动作,交换命令比较容易理解,保存命令和取回命令都有大写和小写两种形式,这两种形式的区别是小写的是将会覆盖目的空间的内容,而大写的是将内容追加到目的空间,追加的内容和原有的内容是以\n分隔。

基本使用

我们随便试试这几个命令,假设有如下测试文本:

$ cat text
1
2
11
22
111
222

1. 首先,仅使用h/H或者g/G命令:

使用h命令:

$ sed 'h' text
1
2
11
22
111
222

使用G命令:

$ sed 'G' text
1

2

11

22

111

222

前者返回的结果正常,因为复制到保持空间的内容并没有取回;后者每一行的后面都多了一个空行,原因是每行都会从保持空间取回一行,追加(大写的G)到模式空间的内容之后,以\n分隔。

2. 使用x命令交换空间

$ sed 'x' text

1
2
11
22
111

命令执行后,发现前面多了一个空行并且最后一行不见了。我在前面一直强调sed命令用好,要有用大脑回顾命令执行过程的能力:

* 当读入第一行的时候,模式空间中的内容是第一行的内容,而保持空间是空的,这个时候交换两个空间,导致模式空间为空,保持空间为第一行的内容,因此输出为空行;
* 当读入下一行之后,模式空间为第2行的内容,保持空间为第一行的内容,交换后输出第1行的内容;
* 依次读入每一行,输出上一行的内容;
* 直到最后一行被读入到模式空间,交换后输出倒数第二行的内容,而最后一行的内容并没有输出,此时命令执行结束。

深入使用

上面的例子简单地介绍了保持空间命令的基本使用方法,这些命令单个使用可能效果不大,但是组合起来的效果是非常好的。

1.第一个例子: 使用逗号拼接行

$ sed 'H;$!d;${x;s/^\n//;s/\n/,/g}' text
1,11,2,11,22,111,222

上面的命令执行过程是这样的,使用H将每一行都追加到保持空间,这里利用d命令打断常规的命令执行流程,让sed继续读入新的一行,直接到将最后一行都放到保持空间。这个时候使用x命令将保持空间的内容交换到模式空间,模式空间的内容现在是这样的:\n1\n11\n2\n11\n22\n111\n222。替换的步骤分成两个,首先去掉首个回车符,然后把剩余的回车符替换成逗号。

其实上面的过程可以包装成一个常用的函数:

$ function join_lines()
> {
>    sed 'H;$!d;${x;s/^\n//;s/\n/,/g}' $1
> }
$ join_lines text
1,11,2,11,22,111,222

进一步,我们可以让分隔符可以通过参数设置,另外一方面删除文件名的参数,而改成常见的过滤器命令形式(即通过管道传递输入):

$ function join_lines()
> {
>     local delim=${1:-,}
>     sed 'H;$!d;${x;s/^\n//;s/\n/'$delim'/g}'
> }
$ cat text | join_lines ';'
1;11;2;11;22;111;222

但是如果我们要用&作为符号,就会出现问题:

$ cat text | join_lines '&'
1
11
2
11
22
111
222

上面并没有&作为分隔符,这是因为&是元字符,表示匹配的部分,这里刚好是回车符\n。因此我们需要对分隔符进行转义:

$ function join_lines()
> {
>      local delim=${1:-,}
>      sed 'H;$!d;${x;s/^\n//;s/\n/\'$delim'/g}'
> } 
$ cat text | join_lines '&'
1&11&2&11&22&111&222

2.第二个例子:将语句中的特定单词转换成大写

现在有这样的文本,有许多类似这样的find the Match statement语句,其中Match是语句的名称,但是这个单词的大小写不统一,有些地方是小写的,有些地方是首字符大写,现在我们要做的是把这个单词统一转换成大写。

容易联想到的是Sed&awk笔记之sed篇:基础命令中介绍的y命令,利用y命令确实可以做到小写转换成大写,转换的命令是这样的:

y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/

但是y命令是会把模式空间的所有内容都转换,这样不能满足我们的需求。但是我们可以利用保持空间保存当前行,然后处理模式空间中的内容:

/the .* statement/{
h
s/.*the \(.*\) statement.*/\1/
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
G
s/\(.*\)\n\(.*the \).*\( statement.*\)/\2\1\3/
}

老规矩一条一条过上面的命令,为了方便说明,每个命令解释后我都会给出当前模式空间和保持空间的内容。

首先找到需要处理的行(假设当前行为find the Match statement)。

Pattern space: find the Match statement

将当前行保存到保持空间:

Pattern space: find the Match statement
Hold space: find the Match statement

然后利用替换命令获取需要处理的单词:

Pattern space: Match
Hold space: find the Match statement

然后通过转换命令将其转换成大写:

Pattern space: MATCH
Hold space: find the Match statement

现在再利用G命令将保持空间的内容追加到模式空间最后:

Pattern space: MATCH\nfind the Match statement
Hold space: find the Match statement

最后再次利用替换命令处理下:

Pattern space: find the MATCH statement
Hold space: find the Match statement

流程控制

一般情况下,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,并且跳转到最后。

上面的例子都是用到了分支命令,分支命令的跳转是无条件的。而与之相对的是测试命令,测试命令的跳转是有条件的,当且仅当当前行发生成功的替换时才跳转。

为了说明测试命令的用法,我们用它来实现前文定义过的join_lines函数:

$ sed ':a;$!N;s/\n/,/;ta' text
1,11,2,11,22,111,222

在最前面我们添加了一个标签:a,然后在再最后面利用测试命令跳转到该标签。可能,你会觉得这里也可以用分支命令,但是事实上分支命令会导致死循环,因为在它里他没有结束的条件。

但是测试命令就不同了,这一点直到最后才体现出来。当最后一行被N命令读入之后,回车替换成逗号,此时ta继续跳转到最开头,因为所有行都已经被读入,所以$!不匹配,同时模式空间中的回车已经全部被替换成逗号,所以替换也不会发生。之前我们说过,当且仅当当前行发生成功的替换时测试命令才跳转。所以此时跳转不会发生,退出sed命令。

我们可以利用sedsed这个工具来验证上面的过程,sedsed可以用来调试sed脚本。

$ ./sedsed -d -e ':a;$!N;s/\n/,/;ta' text
PATT:1$
HOLD:$
COMM::a
COMM:$ !N
PATT:1\n11$
HOLD:$
COMM:s/\n/,/
PATT:1,11$
HOLD:$
COMM:t a
COMM:$ !N
PATT:1,11\n2$
HOLD:$
...
...
COMM:$ !N
PATT:1,11,2,11,22,111,222\n1111$
HOLD:$
COMM:s/\n/,/
PATT:1,11,2,11,22,111,222,1111$
HOLD:$
COMM:t a
COMM:$ !N
PATT:1,11,2,11,22,111,222,1111$
HOLD:$
COMM:s/\n/,/
PATT:1,11,2,11,22,111,222,1111$
HOLD:$
COMM:t a
PATT:1,11,2,11,22,111,222,1111$
HOLD:$
1,11,2,11,22,111,222,1111

看第27行替换命令发生的时候,此时模式空间的内容为PATT:1,11,2,11,22,111,222,1111$,因此替换失败,ta命令不会发生跳转,脚本执行退出。

而如果在这里把测试命令换成分支命令,整个执行过程就会陷入死循环了:

COMM:b a
COMM:$ !N
PATT:1,11,2,11,22,111,222,1111$
HOLD:$
COMM:s/\n/,/
PATT:1,11,2,11,22,111,222,1111$
HOLD:$
COMM:b a
COMM:$ !N
PATT:1,11,2,11,22,111,222,1111$

高级命令总结

到此为止,所有高级命令的用法就已经介绍完了。最后一段内容由于时间的关系,写得比较仓促。高级命令的用法比起基础命令相对复杂一点,而且容易出错,需要十分小心,如果不确定可以用上面介绍的sedsed工具来调式下,而且便于加深各种命令行为的理解。

相信大家肯定用过grep这个命令,它可以找出匹配某个正则表达式的行,例如查看包含"the word"的行:

$ grep "the word" filename

但是grep是针对单行作匹配的,所以如果一个短句跨越了两行就无法匹配。这就给我们出了一个题目,如何用sed模仿grep的行为,并且支持跨行短句的匹配呢?

当单词仅出现在一行时很简单,用普通的正则就可以匹配,这里我们用到分支命令,当匹配时则跳转到最后:

/the word/b

当单词跨越两行,容易想到用N命令将下一行读取到模式空间再做处理。例如我们同样要查找短句"the word",现在的文本是这样的:

$ cat text
we want to find the phrase the
word, but it appears across two lines.

当用N玲读入下一行时,模式空间的内容如下所示:

Pattern space: we want to find the phrase the\nword, but it appears across two lines.

因此,需要将回车符号删除,然后再作匹配。

$!N
s/ *\n/ /
/the word/b

可是这里会有一个问题,如果短句恰好在读入的第二行的话,虽然匹配,但是会打印出模式空间中的所有内容,这样不符合grep的行为,只显示包含短句的那一行。所以这里要再加一个处理,将模式空间的第一行内容删除,再在第二行中匹配。但是在此之前,首先要保存模式空间的内容,否则可没有后悔药可吃。

h命令可以将模式空间的内容保存到保持空间,然后利用替换命令将模式空间的第一行内容清除,然后再作匹配处理:

$!N
h
s/.*\n//
/the word/b

如果不匹配,则短句可能确实是跨越两行,这时候我们首先用g命令将之前保存的内容从保持空间取回到模式空间,替换掉回车后再进行匹配:

g
s/ *\n/ /
/the word/b

这里如果匹配则直接跳转到最后,并且输出的内容是替换后的内容。

但是我们要输出的是匹配的原文,而原文现在在保持空间还有一份拷贝,因此当匹配时,需要将原文从保持空间取回:

g
s/ *\n/ /
/the word/{
g
b
}

同样地,我们要考虑不匹配的情况,不匹配的时候也要将会原文从保持空间取回,并且将模式空间的第一行删除,继续处理下一行:

g
s/ *\n/ /
/the word/{
g
b
}
g
D

将所有的sed脚本合在一起,假设我们将以下内容保存到phrase.sed文件中:

/the word/b
$!N
h
s/.*\n//
/the word/b
g
s/ *\n//
/the word/{
g
b
}
g
D

接下来,我们用一段文本来测试下以上的脚本是否正确:

$ cat text
We will use phrase.sed to print any line which contains
the word. Or if the word appears across two lines, like
below:

It will print this line, because the
word appears across two lines.

You can run sed -f phrase.sed text to test this.

执行命令如下所示:

$ sed -f phrase.sed text
the word. Or if the word appears across two lines, like
It will print this line, because the
word appears across two lines.

上面的命令中的"the word"其实可以是一个变量,这样我们就可以将这个功能写成一个脚本或者函数,用在更多地方:

$ cat phrase.sh 
#! /bin/sh
# phrase -- search for words across lines
# $1 = search string; remaining args = filenames

search=$1

for file in ${@:2}; do
    sed "/$search/b
    \$!N
    h
    s/.*\n//
    /$search/b
    g
    s/ *\n/ /
    /$search/{
    g
    b
    }
    g
    D" $file
done

$ chmod +x phrase.sh
$ ./phrase.sh 'the word' text
the word. Or if the word appears across two lines, like
It will print this line, because the
word appears across two lines.

这只是一个开头,或者你也可以在此基础上扩展更多的功能。sed的命令从单个看来并没是很复杂,但是要用得好就有点难度了,所以需要多多实践,多多思考,这一点跟正则表达式的学习是一样的。如果觉得没有现成的学习环境,sed1line是一个不错的开始。

posted on 2015-02-12 18:21  一个人的天空@  阅读(772)  评论(0编辑  收藏  举报