Shell学习笔记(10)——sed进阶

多行命令

sed编辑器包含了三个可用来处理多行文本的特殊命令,其中:

  1. N:将数据流中的下一行加进来创建一个多行组(multiline group)来处理;
  2. D:删除多行组中的一行
  3. P:打印多行组中的一行。

next 命令

小写的n命令(n表示next)会告诉sed编辑器移动到数据流中的下一文本行,而不用重新回到命令的最开始再执行一遍。通常sed编辑器在移动到数据流中的下一文本行之前,会在当前行上执行完所有定义好的命令。单行next命令改变了这个流程。在下面的这个例子中,你有个数据文件,共有5行内容,其中的两行是空的。目标是删除首行之后的空白行,而留下最后一行之前的空白行。如果写一个删掉空白行的sed脚本,你会删掉两个空白行,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/advance_sed$ cat data1.txt
THis is the header line.
This is a data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed '/^$/d' data1.txt
# 删除空行,^$表示空行
THis is the header line.
This is a data line.
This is the last line.

如果只是删除第1行之后的空行,就要用到n命令,在下面的脚本中,首先要查找含有单词header的那一行。找到之后,n命令会让sed编辑器移动到文本的下一行,也就是那个空行,删除,如下所示:

1
2
3
4
5
biotest@ubuntu:~/advance_sed$ sed '/header/{n;d}' data1.txt
THis is the header line.
This is a data line.
This is the last line.

这时,sed编辑器会继续执行命令列表,该命令列表使用d命令来删除空白行。sed编辑器执行完命令脚本后,会从数据流中读取下一行文本,并从头开始执行命令脚本。因为sed编辑器再也找不到包含单词header的行了。所以也不会有其他行会被删掉,如果再给文添加上一行,里面含有header字符串,之后再留1个空格,也会被删除,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
biotest@ubuntu:~/advance_sed$ cat data1a.txt
THis is the header line.
This is a data line.
This is another line containing string 'header'. #这行文中也含有header,随后跟了一个空行
This is the last line.
biotest@ubuntu:~/advance_sed$ sed '/header/{n;d}' data1a.txt # 这个命令会删除含有header之后的空行
THis is the header line.
This is a data line.
This is another line containing string 'header'. # 后面的空行删除了
This is the last line.

合并文本行

单行next命令会将数据流中的下一文本行移动到sed编辑器的工作空间(称为模式空间)。多行版本的next命令(用大写N)会将下一文本行添加到模式空间中已有的文本后。这样的作用是将数据流中的两个文本行合并到同一个模式空间中。文本行仍然用换行符分隔,但sed编辑器现在会将两行文本当成一行来处理。下面的例子演示了N命令的工作方式:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/advance_sed$ cat data2.txtThis is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed '/first/{N;s/\n/ / }' data2.txt
This is the first data line. This is the second data line.
This is the last line.

在这段代码中,sed '/first/{N;s/line/replace/ }' data2.txt查找含有单词first的那行文本,找到该行后,使用N命令,将下一行合并到那行,然后用替换命令将换行符替换为空格。

如果要在数据文件中查找一个可能会分散在两行中的文本短语的话,这是个很实用的应用程序,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_sed$ cat data3.txt
On Tuesday, the Linux System
Administrator's group meeting will be held.
All System Administrators should attend.
Thank you for your attendance.
biotest@ubuntu:~/advance_sed$ sed 'N; s/System Administrator/Desktop User/' data3.txt
On Tuesday, the Linux System
Administrator's group meeting will be held.
All Desktop Users should attend.
Thank you for your attendance.

替换命令会在文本文件中查找特定的双词短语System Administrator。如果短语在一行中的话,事情很好处理,替换命令可以直接替换文本。但如果短语分散在两行中的话,替换命令就没法识别匹配的模式了,在这个案例中,是第1行中有一个System,第2行有一个Administrator,使用System administrator时,这种情况下就无法匹配,此时就需要使用N命令,如下所示:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/advance_sed$ cat data3.txt
On Tuesday, the Linux System
Administrator's group meeting will be held.
All System Administrators should attend.
Thank you for your attendance.
biotest@ubuntu:~/advance_sed$ sed 'N;s/System.Administrator/Desktop User/' data3.txt
On Tuesday, the Linux Desktop User's group meeting will be held.
All Desktop Users should attend.
Thank you for your attendance.

在这段代码中,使用N命令将发现第一个单词的那行和下一行合并后,即使短语内出现了换行,你仍然可以找到它。注意,替换命令在System和Administrator之间用了通配符模式(.)来匹配空格和换行符这两种情况。但当它匹配了换行符时,它就从字符串中删掉了换行符,导致两行合并成一行。这可能不是你想要的。要解决这个问题,可以在sed编辑器脚本中用两个替换命令:一个用来匹配短语出现在多行中的情况,一个用来匹配短语出现在单行中的情况,如下所示:

1
2
3
4
5
6
7
8
9
biotest@ubuntu:~/advance_sed$ sed 'N
s/System\nAdministrator/Desktop\nUser/
> s/System Administrator/Desktop User/
> ' data3.txt
On Tuesday, the Linux Desktop
User's group meeting will be held.
All Desktop Users should attend.
Thank you for your attendance.

第一个替换命令专门查找两个单词间的换行符,并将它放在了替换字符串中。这样你就能在第一个替换命令专门在两个检索词之间寻找换行符,并将其纳入替换字符串。这样就允许你在新文本的同样位置添加换行符了。但这个脚本中仍有个小问题。这个脚本总是在执行sed编辑器命令前将下一行文本读入到模式空间。当它到了最后一行文本时,就没有下一行可读了,所以N命令会叫sed编辑器停止。如果要匹配的文本正好在数据流的最后一行上,命令就不会发现要匹配的数据,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/advance_sed$ cat data4.txt
On Tuesday, the Linux System
Administrator's group meeting will be held.
All System Administrators should attend.
biotest@ubuntu:~/advance_sed$ sed 'N
s/System\nAdministrator/Desktop\nUser/
s/System Administrator/Desktop User/
' data4.txt
On Tuesday, the Linux Desktop
User's group meeting will be held.
All System Administrators should attend.

在这个案例中,匹配的文正好在数据流的最后一行,并没有发生文本的替换,N命令会错过它,因为没有其他行可读入到模式空间跟这行合并。如果要解决这个问题,就要将单行命令放到N命令前面,并将多行命令放到N命令后面,如下所示:

1
2
3
4
5
6
7
8
9
biotest@ubuntu:~/advance_sed$ sed '
> s/System Administrator/Desktop User/
> N
> s/System\nAdministrator/Desktop\nUser/
> ' data4.txt
On Tuesday, the Linux Desktop
User's group meeting will be held.
All Desktop Users should attend.

现在,查找单行中短语的替换命令在数据流的最后一行也能正常工作,多行替换命令则会负责短语出现在数据流中间的情况。

多行删除命令

单行删除命令(d)和N命令一起使用时,需要注意,看一个案例:

1
2
3
4
5
6
7
biotest@ubuntu:~/advance_sed$ cat data4.txt
On Tuesday, the Linux System
Administrator's group meeting will be held.
All System Administrators should attend.
biotest@ubuntu:~/advance_sed$ sed 'N; /System\nAdministrator/d' data4.txt
All System Administrators should attend.

删除命令会在不同的行中查找单词System和Administrator,然后在模式空间中将两行都删掉。sed编辑器提供了多行删除命令D,它只删除模式空间中的第一行。该命令会删除到换行符(含换行符)为止的所有字符,如下所示:

1
2
3
biotest@ubuntu:~/advance_sed$ sed 'N;/System\nAdministrator/D' data4.txt
Administrator's group meeting will be held.
All System Administrators should attend.

文本的第二行被N命令加到了模式空间,最终结果只是删除了第1行,第2行但仍然完好。如果遇到一种情况,即需要删掉目标数据字符串所在行的前一文本行,此时就能派得上用场,看下面的一个案例,在这个案例中,它会删除数据流中出现在第一行前的空白行:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_sed$ cat data5.txt
# 这里是一个空行
This is the header line.
This is a data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed '/^$/{N;/header/D}' data5.txt
This is the header line.
This is a data line.
This is the last line.

sed编辑器脚本会查找空白行,然后用N命令来将下一文本行添加到模式空间。如果新的模式空间内容含有单词header,则D命令会删除模式空间中的第一行。如果不结合使用N命令和D命令,就不可能在不删除其他空白行的情况下只删除第一个空白行。

多行打印命令

多行打印命令(P)只打印多行模式空间中的第一行,这包括模式空间中直到换行符为止的所有字符。当你用-n选项来阻止脚本输出时,它和显示文本的单行p命令的用法大同小异,如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/advance_sed$ cat data3.txt
On Tuesday, the Linux System
Administrator's group meeting will be held.
All System Administrators should attend.
Thank you for your attendance.
biotest@ubuntu:~/advance_sed$ sed -n 'N;/System\nAdministrator/P' data3.txt
On Tuesday, the Linux System

当多行匹配出现时,P命令只会打印模式空间中的第一行。

多行P命令的强大之处在和N命令及D命令组合使用时才能显现出来。D命令的独特之处在于强制sed编辑器返回到脚本的起始处,对同一模式空间中的内容重新执行这些命令(它不会从数据流中读取新的文本行)。在命令脚本中加入N命令,你就能单步扫过整个模式空间,将多行一起匹配。接下来,使用P命令打印出第一行,然后用D命令删除第一行并绕回到脚本的起始处。一旦返回,N命令会读取下一行文本并重新开始这个过程。这个循环会一直继续下去,直到数据流结束。

保持空间

模式空间(pattern space)是一块活跃的缓冲区,在sed编辑器执行命令时它会保存待检查的文本。但它并不是sed编辑器保存文本的唯一空间。sed编辑器有另一块称作保持空间(hold space)的缓冲区域。在处理模式空间中的某些行时,可以用保持空间来临时保存一些行。有5条命令可用来操作保持空间,如下表所示:

命 令 描 述
h 将模式空间复制到保持空间
H 将模式空间附加到保持空间
g 将保持空间复制到模式空间
G 将保持空间附加到模式空间
x 交换模式空间和保持空间的内容

这些命令用来将文本从模式空间复制到保持空间。这可以清空模式空间来加载其他要处理的字符串。通常,在使用h或H命令将字符串移动到保持空间后,最终还要用g、G或x命令将保存的字符串移回模式空间。由于有两个缓冲区域,弄明白哪行文本在哪个缓冲区域有时会比较麻烦。这里有个简短的例子演示了如何用h和g命令来将数据在sed编辑器缓冲空间之间移动,如下所示:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/advance_sed$ cat data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed -n '/first/{h;p;n;p;g;p}' data2.txt
This is the first data line.
This is the second data line.
This is the first data line.

代码与结果解释:

  1. sed脚本在地址中使用正则表达式来过滤出含有单词first的行;
  2. 当含有单词first的行出现时,h命令将该行放到保持空间;
  3. p命令打印模式空间的内容,也就是第一个数据行的内容;
  4. n命令提取数据流中的下行(This is the second data line),并将它放到模式空间
  5. p命令打印模式空间的内容,现在是第二个数据行;
  6. g命令将保持空间的内容(This is the first data line)放回模式空间,替换当前文本;
  7. p命令打印模式空间的当前内容,现在变回第一个数据行。

通过使用保持空间来回移动文本行,可以强制输出中第一个数据行出现在第二个数据行后面。如果丢掉了第一个p命令,可以以相反的顺序输出这两行,如下所示:

1
2
3
4
5
6
7
8
9
biotest@ubuntu:~/advance_sed$ cat data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed -n '/first/{h;n;p;g;p}' data2.txt
This is the second data line.
This is the first data line.

代码及结果解释;

  1. sed过滤出含有单词first的行,打到This is the first data line后,将它保存到保持空间;
  2. 接着使用n命令,提取这一行的下一行,即This is the second data line,打印这一行的内容;
  3. 使用g命令,将保持空间的内容提取出来,进入模式空间,接着用p命令打印这一行,即This is the first data line.

从上面的例子可以看出,保持空间就相当于一个临时的储存空间,可以把匹配到的文本暂时放在这里面,随后再取出来。

排除命令

感叹号命令(!)用来排除(negate)命令,也就是让原本会起作用的命令不起作用,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/advance_sed$ cat data2.txtThis is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed -n '/header/p' data2.txt
This is the header line.
biotest@ubuntu:~/advance_sed$ sed -n '/header/!p' data2.txt
This is the first data line.
This is the second data line.
This is the last line.

普通p命令只打印data2文件中包含单词header的那行。加了感叹号之后,情况就相反了:除了包含单词header那一行外,文件中其他所有的行都被打印出来了。感叹号在有些应用中用起来很方便。在前面内容中的案例里,sed编辑器无法处理数据流中最后一行文本,因为之后再没有其他行了。可以用感叹号来解决这个问题,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
biotest@ubuntu:~/advance_sed$ cat data4.txt
On Tuesday, the Linux System
Administrator's group meeting will be held.
All System Administrators should attend.
biotest@ubuntu:~/advance_sed$ sed 'N
> s/System\nAdministrator/Desktop\nUser/
> s/System Administrator/Desktop User/
> ' data4.txt
On Tuesday, the Linux Desktop # 这一行进行了修改
User's group meeting will be held.
All System Administrators should attend. # 这一行无法修改
biotest@ubuntu:~/advance_sed$ sed '$!N;
s/System\nAdministrator/Desktop\nUser/
s/System Administrator/Desktop User/
' data4.txt
On Tuesday, the Linux Desktop
User's group meeting will be held.
All Desktop Users should attend.

这个例子演示了如何配合使用感叹号与N命令以及与美元符特殊地址。美元符表示数据流中的最后一行文本,所以当sed编辑器到了最后一行时,它没有执行N命令,但它对所有其他行都执行了这个命令。

利用这种方法,可以反转数据流中文本行的顺序。要实现这个效果(先显示最后一行,最后显示第一行),需要像如下顺序那样利用模式空间:

  1. 在模式空间中放置一行;
  2. 将模式空间中的行放到保持空间中;
  3. 在模式空间中放入下一行;
  4. 将保持空间附加到模式空间后;
  5. 将模式空间中的所有内容都放到保持空间中;
  6. 重复执行第3~5步,直到所有行都反序放到了保持空间中;
  7. 提取并打印行。

这个顺序的示意图如下所示:

mark

在使用这种方法时,如果不想在处理时打印行可以使用sed的-n命令行选项。下一步是决定如何将保持空间文本附加到模式空间文本后面。这可以用G命令完成。唯一的问题是你不想将保持空间附加到要处理的第一行文本后面。这可以用感叹号命令解决,即1!G。下一步是将新的模式空间(含有已反转的行)放到保持空间,使用h命令,将模式𡫊的整个数据流都反转了之后,打印结果,当到达数据流中的最后一行时,就已经得到了模式空间的整个数据流,打印结果需要用到$p命令,现在看一个案例:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/advance_sed$ cat data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed -n '{1!G;h;$p}' data2.txt
This is the last line.
This is the second data line.
This is the first data line.
This is the header line.

Linux中也有反转文本的命令,就是tac,它就是cat反过来写。

改变流

通常,sed编辑器会从脚本的顶部开始,一直执行到脚本的结尾(D命令是个例外,它会强制sed编辑器返回到脚本的顶部,而不读取新的行)。sed编辑器提供了一个方法来改变命令脚本的执行流程,其结果与结构化编程类似。

分支

sed编辑器可以基于地址、地址模式或地址区间排除一整块命令。这允许用户只对数据流中的特定 行执行一组命令。,分支(branch)命令b的格式如下所示:

1
[address]b [label]

address参数决定了哪些行的数据会触发分支命令。label参数定义了要中转到的位置,如下果没有label参数,跳转命令会跳转到脚本的结尾,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_sed$ cat data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed '{2,3b;s/This is/Is this/;s/line./test?/}' data2.txt
Is this the header test?
This is the first data line.
This is the second data line.
Is this the last test?

分支命令在数据流中的第2行和第3行处跳过了两个替换命令,也就是说,不在第2行与第3行进行替换,保持原样,此时没有加入label,因此就跳转到结尾。如果不不想直接跳到脚本的结尾,可以为分支命令定义一个要跳转到的标签。标签以冒号开始,最多可以是7个字符长度,格式为:label2,如果要指定标签,将它加到b命令后即可。使用标签允许你跳过地址匹配处的命令,但仍然执行脚本中的其他命令,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/advance_sed$ cat data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed '{/first/b jump1;s/This is the/No jump on/
:jump1
s/This is the/Jump here on/}' data2.txt
No jump on header line.
Jump here on first data line.
No jump on second data line.
No jump on last line.

跳转命令指定如果文本行中出现了first,程序应该跳到标签为jump1的脚本行,即s/This is the/Jump here on/进行执行,执行完后,再跳回原来的脚本行,即s/This is the/No jump on。如果这里的分支命令的模式没有匹配,sed编辑器就会继续执行脚本中的命令,包括分支标签后的命令(因此,所有的替换命令都会在不匹配分支模式的行上执行)。如果某行匹配了分支模式,sed编辑器就会跳转到带有分支标签的那行。因此,只有最后一个替换命令会执行。在这个例子中,就演示了跳转到sed脚本后面的标签上。也可以跳转到脚本中靠前面的标签上,这样就达到了循环的效果,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_sed$ echo "This, is, a, test, to, remove, commas."|sed -n '{
> :start
> s/,//1p
> b start
> }'
This is, a, test, to, remove, commas.
This is a, test, to, remove, commas.
This is a test, to, remove, commas.
This is a test to, remove, commas.
This is a test to remove, commas.
This is a test to remove commas.

从结果来看,每一代运行,就删除了一个逗号,如果把命令写到一行,是这样的,即sed -n '{:start s/,//1p b start }',其中:start是一个标签;s/,//1p表示,将第1行中的逗号删除,并显示第1行,s是替换,1p表示显示第1行。这个脚本有个问题:它永远不会结束。这就形成了一个无穷循环,不停地查找逗号,直到使用Ctrl+C组合键发送一个信号,手动停止这个脚本。要防止这个问题,可以为分支命令指定一个地址模式来查找。如果没有模式,跳转就应该结束,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_sed$ echo "This, is, a, test, to, remove, commas."|sed -n '{
:start
s/,//1p
/,/b start
}'
This is, a, test, to, remove, commas.
This is a, test, to, remove, commas.
This is a test, to, remove, commas.
This is a test to, remove, commas.
This is a test to remove, commas.
This is a test to remove commas.

现在分支命令只会在行中有逗号的情况下跳转。在最后一个逗号被删除后,分支命令不会再执行,脚本也就能正常停止了。

测试

测试(test)命令(t)也可以用来改变sed编辑器脚本的执行流程。测试命令会根据替换命令的结果跳转到某个标签,而不是根据地址进行跳转。如果替换命令成功匹配并替换了一个模式,测试命令就会跳转到指定的标签。如果替换命令未能匹配指定的模式,测试命令就不会跳转。测试命令使用与分支命令相同的格式,即[address]t [label]

跟分支命令一样,在没有指定标签的情况下,如果测试成功,sed会跳转到脚本的结尾。测试命令提供了对数据流中的文本执行基本的if-then语句的一个低成本办法。举个例子,如果已经做了一个替换,不需要再做另一个替换,那么测试命令能帮上忙,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/advance_sed$ cat data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed '{
s/first/matched/
t
s/This is the/No mathch on/
}' data2.txt
No mathch on header line.
This is the matched data line.
No mathch on second data line.
No mathch on last line.

第一个替换命令会查找模式文本first,如果匹配了行中的模式,它就会替换文本,也就是将first替换为matched,如果第一个替换命令未能匹配模式,第二个替换命令就会被执行。

有了测试命令,就能结束之前用分支命令形成的无限循环,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_sed$ echo "This, is, a, test, to, remove, commas."|sed -n '{
> :start
> s/,//1p
> t start
> }'
This is, a, test, to, remove, commas.
This is a, test, to, remove, commas.
This is a test, to, remove, commas.
This is a test to, remove, commas.
This is a test to remove, commas.
This is a test to remove commas.

当无需替换时,测试命令不会跳转而是继续执行剩下的脚本。

模式替代

在使用通配符时,很难知道到底哪些文本会匹配模式。举个例子,如果想在行中匹配的单词两边上放上引号,只是要匹配模式中的一个单词的话,就比较简单,如下所示:

1
2
biotest@ubuntu:~/advance_sed$ echo "The cat sleeps in his hat."|sed 's/cat/"cat"/'
The "cat" sleeps in his hat.

如果要匹配多个单词,可能就会出问题了,如下所示:

1
2
"|sed 's/.at/".at"/g'
The ".at" sleeps in his ".at".

实际上,我想匹配的结果是The "cat" sleeps in his "hat".这样的,但是使用了通配符后,把原来的字符串也给替换掉了,不符合预期。

&符号

为了解决上述问题,sed中有一个&符号可以达到这个目的,如下所示:

1
2
biotest@ubuntu:~/advance_sed$ echo "The cat sleeps in his hat."|sed 's/.at/"&"/g'
The "cat" sleeps in his "hat".

当模式匹配了单词cat"cat"就会出现在了替换后的单词里。当它匹配了单词hat"hat"就出现在了替换后的单词中。

替代单独的单词

&符号会提取匹配替换命令中指定模式的整个字符串。有时你只想提取这个字符串的一部分。当然可以这么做,只是要稍微花点心思而已。sed编辑器用圆括号来定义替换模式中的子模式。你可以在替代模式中使用特殊字符来引用每个子模式。替代字符由反斜线和数字组成。数字表明子模式的位置。sed编辑器会给第一个子模式分配字符\1,给第二个子模式分配字符\2,依此类推。当在替换命令中使用圆括号时,必须用转义字符将它们标示为分组字符而不是普通的圆括号。这跟转义其他特殊字符正好相反,看一个案例:

1
2
3
biotest@ubuntu:~/advance_sed$ echo "The System Administrator manual" | sed '
s/\(System\) Administrator/\1 User/'
The System User manual

代码及结果解释:这个替换命令用一对圆括号将单词System括起来,将其标示为一个子模式。然后它在替代模式中使用\1来提取第一个匹配的子模式。这没什么特别的,但在处理通配符模式时却特别有用。如果需要用一个单词来替换一个短语,而这个单词刚好是该短语的子字符串,但那个子字符串碰巧使用了通配符,这时使用子模式会方便很多,再看两个案例,如下所示:

1
2
3
4
biotest@ubuntu:~/advance_sed$ echo "That furry cat is pretty" |sed 's/furry \(.at\)/\1/'
That cat is pretty
biotest@ubuntu:~/advance_sed$ echo "That furry hat is pretty"|sed 's/furry \(.at\)/\1/'
That hat is pretty

代码及结果解释:'s/furry \(.at\)/\1/'表示,将原来文本中的furry cat替换为cat\1是子模式1。

当需要在两个或多个子模式间插入文本时,这个特性尤其有用,在下面的这个案例中,使用子模式在大数字中插入逗号,如下所示:

1
2
3
4
5
6
biotest@ubuntu:~/advance_sed$ echo "1233456" |sed '{
> :start
> s/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/
> t start
> }'
1,233,456

这段代码将匹配模式分成了2个部分,分别为.*[0-9][0-9]{3}。第一个子模式是以数字结尾的任意长度的字符。第二个子模式是若干组三位数字。如果这个模式在文本中找到了,替代文本会在两个子模式之间加一个逗号,每个子模式都会通过其位置来标示。这个脚本使用测试命令来遍历这个数字,直到放置好所有的逗号。

在脚本中使用sed

在shell脚本中也可以使用sed编辑器。

使用包装脚本

实现sed编辑器脚本的过程很烦琐,尤其是脚本很长的话。此时可以将sed编辑器命令放到shell包装脚本(wrapper)中,不用每次使用时都重新键入整个脚本。包装脚本充当着sed编辑器脚本和命令行之间的中间人角色。

在shell脚本中,可以将普通的shell变量及参数和sed编辑器脚本一起使用。下面有个将命令行参数变量作为sed脚本输入的例子,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
biotest@ubuntu:~/advance_sed$ cat reverse.sh #!/bin/bash
# Shell wrapper for sed editor script.
# to reverse text file lines.
sed -n '{1!G;h;$p}' $1
biotest@ubuntu:~/advance_sed$ cat data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ bash ./reverse.sh data2.txt
This is the last line.
This is the second data line.
This is the first data line.
This is the header line.

此时,就能在任何文件上轻松使用这个sed编辑器脚本,再不用每次都在命令行上重新输入了。

重定向 sed 的输出

默认情况下,sed编辑器会将脚本的结果输出到STDOUT上。在shell脚本中,可以使用各种标准方法对sed编辑器的输出进行重定向。可以在脚本中用$()将sed编辑器命令的输出重定向到一个变量中,以备后用,在下面的例子中,就是使用sed脚本来向数值计算结果添加逗号,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# Add commas to number in factorial answer
factorial=1
counter=1
number=$1
while [ $counter -le $number ]
do
factorial=$[ $factorial*$counter ]
counter=$[ $counter + 1 ]
done
result=$(echo $factorial | sed '{
:start
s/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/
t start
}')
echo "The result is $result"

运行结果如下所示:

1
2
biotest@ubuntu:~/advance_sed$ bash ./fact.sh 20
The result is 2,432,902,008,176,640,000

在使用普通的阶乘计算脚本后,脚本的结果会被作为sed编辑器脚本的输入,它会给结果加上逗号。然后echo语句使用这个值产生最终结果。

创建sed实用工具

加倍行间距

下面是一个向文本文件的行间插入空白行的简单sed脚本,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/advance_sed$ cat data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed 'G' data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
#这里有个空行

G命令会简单地将保持空间内容附加到模式空间内容后。当启动sed编辑器时,保持空间只有一个空行。将它附加到已有行后面,你就在已有行后面创建了一个空白行。你可能已经注意到了,这个脚本在数据流的最后一行后面也加了一个空白行,使得文件的末尾也产生了一个空白行。如果你不想要这个空白行,可以用排除符号(!)和尾行符号($)来确保脚本不会将空白行加到数据流的最后一行后面,如下所示:

1
2
3
4
5
6
7
8
biotest@ubuntu:~/advance_sed$ sed '$!G' data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line. # 这一行后面已经没有了空行

代码解释:只要该行不是最后一行, G 命令就会附加保持空间内容。当sed编辑器到了最后一行时,它会跳过 G 命令。

对可能含有空白行的文件加倍行间距

再进一步探索上面的例子:如果文本文件已经有一些空白行,但你想给所有行加倍行间距要怎么办呢?如果用前面的脚本,有些区域会有两个餞行,因为每个已有的空白行也会被加倍,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
biotest@ubuntu:~/advance_sed$ cat data6.txt
This is line one.
This is line two.
This is line three.
This is line four.
biotest@ubuntu:~/advance_sed$ sed '$!G' data6.txt
This is line one.
This is line two.
This is line three.
This is line four.

原来空白行的位置有了三个空白行。这个问题的解决办法是,首先删除数据流中的所有空白行,然后用G命令在所有行后插入新的空白行。要删除已有的空白行,需要将d命令和一个匹配空白行的模式一起使用,即使用/^$/d,如下所示:

1
2

给文件中的行编号

等号可以显示数据流中的数据,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
biotest@ubuntu:~/advance_sed$ cat data2.txt
This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed '=' data2.txt
1
This is the header line.
2
This is the first data line.
3
This is the second data line.
4
This is the last line.

虽然添加了行号,但是不太美观,行号在文本的上方,比较好看的方式就是将等号与文本放在同一行。在获得了等号命令的输出之后,可以通过管道将输出传给另一个sed编辑器脚本,它会使用N命令来合并这两行。还需要用替换命令将换行符更换成空格或制表符,如下所示:

1
2
3
4
5
6
7
8
9
biotest@ubuntu:~/advance_sed$ cat data2.txt This is the header line.
This is the first data line.
This is the second data line.
This is the last line.
biotest@ubuntu:~/advance_sed$ sed '=' data2.txt | sed 'N; s/\n/ /'
1 This is the header line.
2 This is the first data line.
3 This is the second data line.
4 This is the last line.

在bash中,nl命令也可以添加行号,如下所示:

1
2
3
4
5
6
7
8
9
10
11
biotest@ubuntu:~/advance_sed$ nl data2.txt
1 This is the header line.
2 This is the first data line.
3 This is the second data line.
4 This is the last line.
biotest@ubuntu:~/advance_sed$ cat -n data2.txt
1 This is the header line.
2 This is the first data line.
3 This is the second data line.
4 This is the last line.
biotest@ubuntu:~/advance_sed$

通过nl添加行号时会在前面添加一个间隔。

打印末尾行

美元符代表数据流中最后一行,所以只显示最后一行很容易,如下所示:

1
2
biotest@ubuntu:~/advance_sed$ sed -n '$p' data2.txt
This is the last line.

如果要用美元符来显示数据流末尾的若干行,就需要创建滚动窗口。滚动窗口是检验模式空间中文本行块的常用方法,它使用N命令将这些块合并起来。N命令将下一行文本附加到模式空间中已有文本行后面。一旦你在模式空间有了一个10行的文本块,你可以用美元符来检查你是否已经处于数据流的尾部。如果不在,就继续向模式空间增加行,同时删除原来的行(记住,D命令会删除模式空间的第一行),如下所示:

通过循环N命令和D命令,在向模式空间的文本行块增加新行的同时也删除了旧行。分支命令非常适合这个循环。要结束循环,只要识别出最后一行并用q命令退出就可以了如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
biotest@ubuntu:~/advance_sed$ cat data7.txt
This is line 1.
This is line 2.
This is line 3.
This is line 4.
This is line 5.
This is line 6.
This is line 7.
This is line 8.
This is line 9.
This is line 10.
This is line 11.
This is line 12.
This is line 13.
This is line 14.
This is line 15.
biotest@ubuntu:~/advance_sed$ sed '{
> :start
> $q;N;11,$D
> b start
> }' data7.txt
This is line 6.
This is line 7.
This is line 8.
This is line 9.
This is line 10.
This is line 11.
This is line 12.
This is line 13.
This is line 14.
This is line 15.

这个脚本会首先检查这行是不是数据流中最后一行。如果是,退出(quit)命令会停止循环。N命令会将下一行附加到模式空间中当前行之后。如果当前行在第10行后面,11,$D命令会删除模式空间中的第一行。这就会在模式空间中创建出滑动窗口效果。因此,这个sed程序脚本只会显示出data7.txt文件最后10行。

删除行

删除连续的空白行

删除连续空白行的最简单办法是用地址区间来检查数据流。通过在地址中使用区间,sed编辑器会对所有匹配指定地址区间的行执行该命令。删除连续空白行的关键在于创建包含一个非空白行和一个空白行的地址区间。如果sed编辑器遇到了这个区间,它不会删除行。但对于不匹配这个区间的行(两个或更多的空白行),它会删除这些行,使用的主要代码是/./,/^$/!d,这段代码的的模式是/.//^$/,这个区间的开始地址会匹配任何含有至少一个字符的行,区间的结束地址会匹配一个窄,在这个区间内的行不会被删除,如下所示:

1
2

结果显示,无论文件的数据行之间出现了多少空白行,在输出中只会在行间保留一个空白行。

删除开头的空白行

主要的脚本是/./,$!d,这个脚本用地址区间来决定哪些行需要删掉,这个区间从含有字符的行开始,一直到数据流结束,在这个区间内的任何行都不会从输出中删除,这表明,含有字符的第一行之前的任何行都会被删除,如下所示:

1
2
3
4
5
6
7
8
9
10
biotest@ubuntu:~/advance_sed$ cat data9.txt
This is line one.
This is line two.
biotest@ubuntu:~/advance_sed$ sed '/./,$!d' data9.txt
This is line one.
This is line two.

测试文件在数据行之前有两个空白行。这个脚本成功地删除了开头的两个空白行,保留了数据中的空白行。

删除结尾的空白行

删除结尾的空白行主要是通过循环来实验,脚本如下所示:

1
2
3
4
sed '{
:start
/^\n*$/{$d;N; b start }
}'

在正常脚本的花括号里还有花括号。这允许你在整个命令脚本中将一些命令分组。该命令组会被应用在指定的地址模式上。地址模式能够匹配只含有一个换行符的行。如果找到了这样的行,而且还是最后一行,删除命令会删掉它。如果不是最后一行,N命令会将下一行附加到它后面,分支命令会跳到循环起始位置重新开始,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
biotest@ubuntu:~/advance_sed$ cat data10.txt
This is the first line.
This is the second line.
biotest@ubuntu:~/advance_sed$ sed '{
:start
/^\n*$/{$d; N; b start }
}' data10.txt
This is the first line.
This is the second line.

删除 HTML 标签

标准的HTML Web页面包含一些不同类型的HTML标签,标明了正确显示页面信息所需要的格式化功能,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
biotest@ubuntu:~/advance_sed$ cat data11.txt
<html>
<head>
<title>This is the page title</title>
</head>
<body>
<p>
This is the <b>first</b> line in the Web page.
This should provide some <i>useful</i>
information to use in our sed script.
</body>
</html>

HTML标签由小于号和大于号来识别。大多数HTML标签都是成对出现的:一个起始标签(比如<b>用来加粗),以及另一个结束标签(比如</b>用来结束加粗)。但如果不够小心的话,删除HTML标签可能会带来问题,因为按照常规思路,用户可能认为删除HTML标签的办法就是查找以小于号(<)开头、大于号(>)结尾且其中有数据的文本字符串,即s/<.*.//g,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
biotest@ubuntu:~/advance_sed$ sed 's/<.*>//g' data11.txt
This is the line in the Web page.
This should provide some
information to use in our sed script.
biotest@ubuntu:~/advance_sed$

从结果来看,标题文本以及加粗和倾斜的文本都不见了。sed编辑器将这个脚本理解为小于号和大于号之间的任何文本,且包括其他的小于号和大于号。每次文本出现在HTML标签中(比如<b>first</b>),这个sed脚本都会删掉整个文本。这个问题的解决办法是让sed编辑器忽略掉任何嵌入到原始标签中的大于号。要这么做的话,你可以创建一个字符组来排除大于号。脚本改为如下所示代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
biotest@ubuntu:~/advance_sed$ sed 's/<[^>]*>//g' data11.txt
This is the page title
This is the first line in the Web page.
This should provide some useful
information to use in our sed script.
biotest@ubuntu:~/advance_sed$

现在就能正常删除了,此外,还可以添加一条删除多余空白行的命令,如下所示:

1
2
3
4
5
biotest@ubuntu:~/advance_sed$ sed 's/<[^>]*>//g; /^$/d' data11.txt
This is the page title
This is the first line in the Web page.
This should provide some useful
information to use in our sed script.