多行命令

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

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

next命令

单行的next命令

小写的n命令会告诉sed编辑器移动到数据流中的下一文本行,而不用重新回到命令的最开始 再执行一遍。记住,通常sed编辑器在移动到数据流中的下一文本行之前,会在当前行上执行完 所有定义好的命令。单行next命令改变了这个流程。

$ sed '/header/{n ; d}' data1.txt

脚本要查找含有单词header的那一行。找到之后,n命令会让sed编辑器移动到文 本的下一行,这时,sed编辑器会继续执行命令列表,该命令列表使用d命令来删除行。

sed编辑器执 行完命令脚本后,会从数据流中读取下一行文本,并从头开始执行命令脚本。因为sed编辑器再 也找不到包含单词header的行了。所以也不会有其他行会被删掉。

合并文本行

单行next命令会将数据流中的下一文本 行移动到sed编辑器的工作空间(称为模式空间)。多行版本的next命令(用大写N)会将下一文 本行添加到模式空间中已有的文本后。

这样的作用是将数据流中的两个文本行合并到同一个模式空间中。文本行仍然用换行符分 隔,但sed编辑器现在会将两行文本当成一行来处理。

$ sed '/first/{ N ; s/\n/ / }' data2.txt

sed编辑器脚本查找含有单词first的那行文本。找到该行后,它会用N命令将下一行合并到那 5 行,然后用替换命令s将换行符替换成空格。结果是,文本文件中的两行在sed编辑器的输出中成了一行。

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

$ sed 'N ; s/System Administrator/Desktop User/' data3.txt

替换命令会在文本文件中查找特定的双词短语System Administrator。如果短语在一行 中的话,事情很好处理,替换命令可以直接替换文本。但如果短语分散在两行中的话,替换命令 就没法识别匹配的模式了。这时N命令就可以派上用场了。

$ sed 'N ; s/System.Administrator/Desktop User/' data3.txt

注意,替换命令在System和Administrator之间用了通配符模式(.)来匹配空格和换行符这两种情况。但当它匹配了换行符时,它就从字符串中删掉了换行符,导致两行合并成一行。这 可能不是你想要的。

要解决这个问题,可以在sed编辑器脚本中用两个替换命令:一个用来匹配短语出现在多行 中的情况,一个用来匹配短语出现在单行中的情况。

$ sed 'N
> s/System\nAdministrator/Desktop\nUser/ 
> s/System Administrator/Desktop User/
> ' data3.txt

但这个脚本中仍有个小问题。这个脚本总是在执行sed编辑器命令前将下一行文本读入到模 式空间。当它到了最后一行文本时,就没有下一行可读了,所以N命令会叫sed编辑器停止。如果 要匹配的文本正好在数据流的最后一行上,命令就不会发现要匹配的数据。

你可以轻松地解决这个问题——将单行命令放到N命令 前面,并将多行命令放到N命令后面

$ sed '
> s/System Administrator/Desktop User/ 
>N
> s/System\nAdministrator/Desktop\nUser/ 
> ' data4.txt

多行删除命令

sed编辑器用单行删除命令(d)来删除模式空间中的当前行。但和N命令一起使用时,使用单行删除命令就要小心了。

$ sed 'N ; /System\nAdministrator/d' data4.txt

删除命令会在不同的行中查找单词System和Administrator,然后在模式空间中将两行都删掉。 这未必是你想要的结果。

sed编辑器提供了多行删除命令D,它只删除模式空间中的第一行。该命令会删除到换行符(含换行符)为止的所有字符。

这里有个例子,它会删除数据流中出现在第一行前的空白行。

$ sed '/^$/{N ; /header/D}' data5.txt

sed编辑器脚本会查找空白行,然后用N命令来将下一文本行添加到模式空间。如果新的模式 空间内容含有单词header,则D命令会删除模式空间中的第一行。

多行打印命令

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

保持空间

模式空间(pattern space)是一块活跃的缓冲区,在sed编辑器执行命令时它会保存待检查的文本。但它并不是sed编辑器保存文本的唯一空间。

sed编辑器有另一块称作保持空间(hold space)的缓冲区域。在处理模式空间中的某些行时, 可以用保持空间来临时保存一些行。有5条命令可用来操作保持空间,

通常,在使用h或H命令将字符串移动到保持空间后,最终还要用g、G或x命令将保存的字符 串移回模式空间(否则,你就不用在一开始考虑保存它们了)。

由于有两个缓冲区域,弄明白哪行文本在哪个缓冲区域有时会比较麻烦。这里有个简短的例 子演示了如何用h和g命令来将数据在sed编辑器缓冲空间之间移动。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ 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.
$
$ 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命令,你可以以相反的顺序输出这两行。

排除命令

感叹号命令(!)用来排除(negate)命令,也就是让原本会起作用的命令不起作用。下面的例子演示了这一特性。

$ sed -n '/header/!p' data2.txt

普通p命令只打印data2文件中包含单词header的那行。加了感叹号之后,情况就相反了:除了包含单词header那一行外,文件中其他所有的行都被打印出来了。

很多情况下sed编辑器无法处理数据流中最后一行文本,因为之后再没有其他行了。可以使用感叹号与N命令以及与美元符特殊地址。

$ sed '$!N;
> s/System\nAdministrator/Desktop\nUser/ 
> s/System Administrator/Desktop User/
> ' data4.txt

美元符表示数据流中 的最后一行文本,所以当sed编辑器到了最后一行时,它没有执行N命令,但它对所有其他行都执行了这个命令。

改变流

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

分支

sed编辑器提供了一种方法,可以基于地址、地址模式或地址区间排除一整块命令。这允许你只对数据流中的特定行执行一组命令。

分支(branch)命令b的格式如下:

[address]b [label]

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

sed '{2,3b ; s/This is/Is this/ ; s/line./test?/}' data2.txt

分支命令在数据流中的第2行和第3行处跳过了两个替换命令。

要是不想直接跳到脚本的结尾,可以为分支命令定义一个要跳转到的标签。标签以冒号开始, 最多可以是7个字符长度。

:label2 

要指定标签,将它加到b命令后即可。使用标签允许你跳过地址匹配处的命令,但仍然执行 脚本中的其他命令。

$ sed '{/first/b jump1 ; s/This is the/No jump on/ 
> :jump1
> s/This is the/Jump here on/}' data2.txt

跳转命令指定如果文本行中出现了first,程序应该跳到标签为jump1的脚本行。如果分支命令的模式没有匹配,sed编辑器会继续执行脚本中的命令,包括分支标签后的命令(因此,所 有的替换命令都会在不匹配分支模式的行上执行)。

如果某行匹配了分支模式, sed编辑器就会跳转到带有分支标签的那行。因此,只有最后一 个替换命令会执行。

这个例子演示了跳转到sed脚本后面的标签上。也可以跳转到脚本中靠前面的标签上,这样 就达到了循环的效果。

$ echo "This, is, a, test, to, remove, commas." | sed -n 	'{ > :start
> s/,//1p
> b start
> }'

脚本的每次迭代都会删除文本中的第一个逗号,并打印字符串。这个脚本有个问题:它永远 不会结束。这就形成了一个无穷循环,不停地查找逗号,直到使用Ctrl+C组合键发送一个信号,手动停止这个脚本。

要防止这个问题,可以为分支命令指定一个地址模式来查找。如果没有模式,跳转就应该 结束。

$ echo "This, is, a, test, to, remove, commas." | sed -n '{ > :start
> s/,//1p
> /,/b start
> }'

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

测试

类似于分支命令,测试(test)命令(t)也可以用来改变sed编辑器脚本的执行流程。测 试命令会根据替换命令的结果跳转到某个标签,而不是根据地址进行跳转。

如果替换命令成功匹配并替换了一个模式,测试命令就会跳转到指定的标签。如果替换命令 未能匹配指定的模式,测试命令就不会跳转。

测试命令使用与分支命令相同的格式。

[address]t [label] 

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

$ sed '{
> s/first/matched/
>t
> s/This is the/No match on/ > }' data2.txt

第一个替换命令会查找模式文本first。如果匹配了行中的模式,它就会替换文本,而且测 试命令会跳过后面的替换命令。如果第一个替换命令未能匹配模式,第二个替换命令就会被执行。

有了测试命令,你就能结束之前用分支命令形成的无限循环。

1
2
3
4
5
$ echo "This, is, a, test, to, remove, commas. " | sed -n '{ 
> :start
> s/,//1p
> t start
> }'

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

模式替代

你已经知道了如何在sed命令中使用模式来替代数据流中的文本。然而在使用通配符时,很 难知道到底哪些文本会匹配模式。用于替代的字符串无法匹配 已匹配单词中的通配符字符。

$ echo "The cat sleeps in his hat." | sed 's/.at/".at"/g' 
The ".at" sleeps in his ".at".
$

&符号

sed编辑器提供了一个解决办法。&符号可以用来代表替换命令中的匹配的模式。不管模式匹 配的是什么样的文本,你都可以在替代模式中使用&符号来使用这段文本。这样就可以操作模式 所匹配到的任何单词了。

$ echo "The cat sleeps in his hat." | sed 's/.at/"&"/g'
 The "cat" sleeps in his "hat".
$

替代单独的单词

sed编辑器用圆括号来定义替换模式中的子模式。你可以在替代模式中使用特殊字符来引用 每个子模式。替代字符由反斜线和数字组成。数字表明子模式的位置。sed编辑器会给第一个子模式分配字符\1,给第二个子模式分配字符\2,依此类推。

当在替换命令中使用圆括号时,必须用转义字符将它们标示为分组字符而不是普通的圆 括号。这跟转义其他特殊字符正好相反。

$ echo "The System Administrator manual" | sed '
> s/\(System\) Administrator/\1 User/'
The System User manual 12 
$

这个替换命令用一对圆括号将单词System括起来,将其标示为一个子模式。然后它在替代模 式中使用\1来提取第一个匹配的子模式。这没什么特别的,但在处理通配符模式时却特别有用。

如果需要用一个单词来替换一个短语,而这个单词刚好是该短语的子字符串,但那个子字符 串碰巧使用了通配符,这时使用子模式会方便很多。

$ echo "That furry cat is pretty" | sed 's/furry \(.at\)/\1/'

当需要在两个或多个子模式间插入文本时,这个特性尤其有用。这里有个脚本,它使用子模 式在大数字中插入逗号。

1
2
3
4
5
6
7
$ echo "1234567" | sed '{
> :start
> s/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/ 
> t start
> }'
1,234,567
$

这个模式会查找两个子模式。第一个子模式是以数字结尾的任意长度的字符。第二个子模式 是若干组三位数字.如果这个模式 在文本中找到了,替代文本会在两个子模式之间加一个逗号,每个子模式都会通过其位置来标示。 这个脚本使用测试命令来遍历这个数字,直到放置好所有的逗号。

在脚本中使用sed

使用包装脚本

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

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

1
2
3
4
5
6
7
8
$ cat reverse.sh
#!/bin/bash
# Shell wrapper for sed editor script.
# to reverse text file lines.
#
sed -n '{ 1!G ; h ; $p }' $1
#
$

名为reverse的shell脚本用sed编辑器脚本来反转数据流中的文本行。它使用shell参数$1从命令 2 行中提取第一个参数,这正是需要进行反转的文件名。

重定向sed的输出

默认情况下,sed编辑器会将脚本的结果输出到STDOUT上。你可以在shell脚本中使用各种标 准方法对sed编辑器的输出进行重定向。

可以在脚本中用$()将sed编辑器命令的输出重定向到一个变量中,以备后用。

1
2
3
4

result=$(echo $factorial | sed '{ :start s/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/
t start
}')