文本处理三剑客之 sed 流编辑器(高级部分)
6. 高级 sed :循环和缓冲
6.1 sed 如何工作
sed 本身也是一种语言,并且自带循环执行功能。其维护了两个数据缓冲区:一个是活动的模式空间,另一个是辅助的保持空间。程序初始化时两者均为空。组合使用两个空间,其威力大增。
sed 是通过对每一输入行执行以下操作进行循环的:
-
首先,sed 从输入流中读取一行,删除行尾的换行符,把它放入模式空间中。
-
然后,执行脚本中包含的一系列命令。每个命令之前如果没有条件地址,则命令都会执行,如果有条件地址,则只在条件测试通过后命令才执行。
-
再者,当执行完脚本后,除非指定了
-n
选项,否则,模式空间中的内容结尾追加换行符后打印至输出流中。 -
最后,开始下一轮循环处理下一输入行。
除非使用了像 D
这样的特殊命令,在两个循环中间会删除模式空间中的部分内容。另一方面,保持空间的内容在循环中仍然保持其内容不变。
以下是一些高级使用的示例:
sed -n 'n;p' filename # 打印偶数行
sed '1!G;h;$!d' filename # 文件内容逆序
sed 'N;D' filename # 打印文件的最后一行
seq 10 | sed '3h;9G;9!d'
sed '$!N;$!D' filename
sed '$!d' filename
sed 'G' filename
sed 'g' filename
sed '^$/d;G' filename
sed 'n;d' filename
sed -n '1!G;h;$p' filename
6.2 多行技术
使用 D
、G
、H
、N
、P
等命令可以在一个缓冲区中处理多行。这些命令的作用类似于小写的对应项 d
、g
、h
、n
、p
。它们除了在空间中追加或删除内容外,还要考虑在两行之间内嵌一个换行符,以实现在缓冲区中处理多行。
具体规则如下:
命令 |
|
---|---|
H | 先在保持空间中追加一个换行符,再追加模式空间中的内容。 |
G | 先在模式空间中追加一个换行符,再追加保持空间中的内容。 |
N | 先在模式空间中追加一个换行符,再追加下一输入行文本。 |
D | 如果模式空间中没有内嵌换行符,则与执行命令 d 一样,删除模式空间的内容且启动下一轮循环,否则,删除模式空间中第一个换行符及其前面的内容后,立即开始下一轮循环。 |
P | 打印模式空间中第一个换行符止及其前面的内容。 |
下面的示例演示 N
和 D
命令的操作:
$ seq 6 | sed -n 'N; l; D'
1\n2$
2\n3$
3\n4$
4\n5$
5\n6$
以上命令解释如下:
sed 首先读入第一行文本 ‘1’ 到模式空间。执行 N
命令,在模式空间中的内容后追加一个换行符及读入的下一输入行 ‘2’,此时模式空间中的内容为 ‘1\n2’。命令 l
(list) 以一种可视的形式打印模式空间中的内容,即输出 ‘1\n2$’,‘\n’ 表示换行符,最后加一个 ‘$’ 表示行尾。然后执行命令 D
,删除模式空间中的第一个换行符及其前面的内容。此时模式空间中留下文本还有 ‘2’。
到此,第一轮脚本执行完毕,开始下一轮循环,N
命令追加一个换行符及下一输入行,此时模式空间中的内容为 ‘2\n3’。…
如果要处理段落这样的块文本(段落是由两个空行包围的连续行组成),使用以下通用的技术结构:
sed '/./{H; $!d}; x; s/REGEXP/REPLACEMENT/'
以上命令解释如下:
第一个表达式 /./{H; $!d}
操作所有非空行,输入行如果能够匹配 .
(句号), 说明是非空行,则执行 H
命令,在保持空间内容后面追加一个换行符及模式空间中的内容,然后,地址条件 $!
表示只要不是最后输入行,执行 D
命令以清空模式空间中的内容,并开始下一轮循环。
后续的两个命令 x
和 s
,只有在遇到空行时才执行。x
命令相互交换保持空间(累计的多行内容)和模式空间中的内容(实际是空的)。s///
命令对上一段落的所有文本(包括内嵌的换行符)进行替换操作。
下面的示例演示这个技术:
$ cat input.txt
a a a aa aaa
aaaa aaaa aa
aaaa aaa aaa
bbbb bbb bbb
bb bb bbb bb
bbbbbbbb bbb
ccc ccc cccc
cccc ccccc c
cc cc cc cc
$ sed '/./{H; $!d}; x; s/^/\nSTART-->/; s/$/\n<--END/' input.txt
START-->
a a a aa aaa
aaaa aaaa aa
aaaa aaa aaa
<--END
START-->
bbbb bbb bbb
bb bb bbb bb
bbbbbbbb bbb
<--END
START-->
ccc ccc cccc
cccc ccccc c
cc cc cc cc
<--END
6.3 分支和流程控制
分支命令 b
、t
、T
能够改变 sed 程序的执行流程。无条件分支命令 b
类似于其它语言的 goto
语句,而条件分支命令类似于其它语言的 if/then
语句。
默认情况下,sed 在读入一行到模式空间后,按顺序执行脚本中的命令。没有地址限制的命令影响所有读入行,有地址条件的只会影响匹配的行。
sed 不支持传统的 if/then
结构,取而代之的是,使用以下命令当成条件或更改默认的流程。
命令 |
|
---|---|
d | 删除或清空当前模式空间,并在不执行后面剩余命令和不打印模式空间内容下,开启下一轮循环。 |
D | 删除模式空间中第一个换行符及其前面的内容,并在不执行剩余命令和不打印模式空间内容的基础上,开启下一轮循环,如果模式空间中没有内嵌的换行符,则执行 d 命令一样。 |
[addr]X , [addr]{X; X; X} , /regexp/X , /regexp/{X; X; X} | 这里 X 是命令占位符。地址和正则表达式用作 if/then 语句的条件。如果 [addr] 或 /regexp/ 匹配当前行号或内容,则执行这个或这些 X 命令。例如,'/^#/d' 的作用是:删除以 ‘#’ 开头的行,也不会打印删除的行,立即启动下一轮循环。 |
b | 无条件分支 b 总使流程跳到指定的标签处,这样就会跳过或重复执行一部分命令,也不会开启新的一轮循环。如果结合使用一个地址条件,则分支命令可以在匹配的行上有条件地执行。如果没有指定标签,则跳到脚本的结尾,开始下一轮循环。 |
t | 条件分支 t 的条件是,在上一输入行上成功执行 ‘s///’ 命令,或者另一个条件分支执行后,才会跳转到指定的标签处。 |
T | 与 t 命令相似,但是条件相反。即只有在上一输入行上执行失败,才会跳转到指定的标签处。 |
下面的两个 sed 命令是等价的。第一个示例巧妙使用行号地址及 b
命令跳过对第 ‘1’ 行执行替换 s///
操作。第二个示例使用行号地址取反,达到不对指定的行执行替换操作,当然,‘y///’ 命令仍然会对所有行上执行。
$ printf "%s\n" a1 a2 a3 | sed -E '/1/bx; s/a/z/; :x; y/123/456/'
a4
z5
z6
$ printf "%s\n" a1 a2 a3 | sed -E '/1/!s/a/z/; y/123/456/'
6.3.1 分支和循环
分支命令 b
、t
、T
后面可以指定一个标签(通常是一个单字符),而标签是通过一个冒号和一个或多个字符组成(例如,:x
)。如果省略了标签,那么分支命令就会开启新循环。
注意,通过分支命令跳转到指定的标签和开启新一轮循环的区别:开启新循环时,如果没有禁用自动打印功能,程序会先打印模式空间中的内容,然后读取下一输入到模式空间。而跳转到指定标签操作不会打印空间的内容,也不会读取下一输入行,即使标签处于脚本的开始位置。
下面的命令没有操作。sed 脚本中仅有的命令 b
没有指定标签,从而起到开启下一轮循环而已,而每次循环都会打印空间内容,并读取下一输入行,这时类似于 cat
命令。
$ seq 3 | sed b
1
2
3
下面的示例是一个无限循环,不会终止,也不会打印任何内容。
# 以下命令要求 GNU sed 扩展
$ seq 3 | sed ':x; bx'
# 这个可移植性更好
$ seq 3 | sed -e ':x' -e bx
分支命令通常与 n
或 N
命令互补使用:后面的两个命令都会读取下一输入行到模式空间,而不会开启下一轮循环。在读取下一输入行之前,n
命令先打印当前模式空间内容,然后清空空间;而 N
命令先在模式空间结尾追加一个换行符,再追加下一输入行。比较以下两个示例:
$ seq 3 | sed ':x; n; bx'
1
2
3
$ seq 3 | sed ':x; N; bx'
1
2
3
两个示例结果是一样的,都不是无限循环,尽管从来没有开启新一轮循环。
第一个示例,n
命令先打印模式空间中的内容,然后清空模式空间,再读取下一输入行。
第二个示例,执行 N
命令会在模式空间结尾追加一个换行符和下一输入行。通过 b
命令循环执行 N
命令,因此,文件的内容会经累计在模式空间中,直到没有更多的输入为止,这时,sed 就要终止,而在终止之前,先打印模式空间中所有的内容。
为了演示两个命令之间的不同,使用以下两个命令:
$ printf "%s\n" aa bb cc | sed ':x; n; =; bx'
aa
2
bb
3
cc
printf "%s\n" aa bb cc | sed ':x; N; =; bx'
2
3
aa
bb
cc
第一个命令的执行过程:sed 启动后先读入第一行 ‘aa’ 到模式空间,遇到命令 n
,打印 ‘aa’,清空模式空间,然后读入下一行 ‘bb’ 到模式空间,执行等号命令打印行号,这时行号是 ‘2’;遇到命令 b
跳转到 ‘x’ 标签处,然后继续前面的操作,直到没有更多的输入行为止。
第二个命令的执行过程:sed 启动后先读入第一行 ‘aa’ 到模式空间,遇到命令 N
,在模式空间中追加一个换行符和下一输入行 ‘bb’,这时模式空间中的内容为 ‘aa\nbb’,执行等号命令打印当前行号 ‘2’;遇到命令 b
程序跳转到标签 ‘x’ 处,然后继续前面的操作。这样每次跳转前打印行号,而输入的行全部累积在模式空间中,直到没有可读入的行为止,sed 结束前打印模式空间的多行,即 ‘aa\nbb\ncc’。
再提供两个示例:
$ printf "%s\n" aa bb cc dd | sed ':x; n; s/\n/***/; bx'
aa
bb
cc
dd
$ printf "%s\n" aa bb cc dd | sed ':x; N; s/\n/***/; bx'
aa***bb***cc***dd
6.3.2 分支命令示例:跳转行
作为一个真实使用分支命令示例,考虑“带引号可打印”(quoted-printable)文件,通常用于对电子邮件进行编码。在这些文件中,长行被拆分,并在行尾使用单个字符 ‘=’ 字符作为软换行符进行标记:(参考网页如下:https://en.wikipedia.org/wiki/Quoted-printable )。
$ cat jaques.txt
All the wor=
ld’s a stag=
e,
And all the=
men and wo=
men merely =
players:
They have t=
heir exits =
and their e=
ntrances;
And one man=
in his tim=
e plays man=
y parts.
处理用的命令如下:
$ sed ':x; /=$/{N; s/=\n//g; bx}' jaques.txt
All the world’s a stage,
And all themen and women merely players:
They have their exits and their entrances;
And one manin his time plays many parts.
程序使用一个地址表达式 ‘/=$/’ 作为条件,如果输入行内容以 ‘=’ 结尾,就执行命令集 {N; s/=\n//g; bx}
,即执行命令 N
读入下一行,使用 s
命令清除所有 ‘=\n’ 字符序列,然后,在没有开启下一轮循环前提下,无条件跳转到标签 ‘x’ 处,重复执行该命令集。如果输入行不是以 ‘=’ 结尾,则不会执行命令集,到本次循环结束前默认执行打印模式空间内容,且开启新一轮循环。
下面使用不同的方法:
$ sed ':x; $!N; s/=\n//; tx; P; D' jaques.txt
除了最后一行以外的所有输入行,每次执行 N
命令都会在模式空间中追加一个换行符和下一输入行。然后执行替换 s///
命令删除软换行符。如果替换成功,意味着模式空间中有需要进行连接,那么条件分支命令 t
的执行使流程跳转到脚本的开始处,这样不会开启新的一轮循环。如果替换不成功,那么,不会执行分支跳转,接着执行命令 P
打印模式空间中第一个换行符及其前面的内容,然后 D
命令删除刚才打印过的部分。
7. 一些高级脚本示例
7.1 合并行
使用 N
、D
、P
命令处理多行、分支命令 b
、t
的跳转功能,从而实现合并行。
以下示例演示连接指定行,即合并第 2
行和第 3
行。
$ cat lines.txt
hello
hel
lo
hello
$ sed '2{N; s/\n//}' lines.txt
hello
hello
hello
以下示例演示如何合并特定行:
$ cat 1.txt
this \
is \
a \
long \
line
and another \
line
$ sed ':x; /\\$/{N; s/\\\n//g; bx}' 1.txt
this is a long line
and another line
7.2 行文本居中
以下脚本能使输入文件中的所有文本行都在一个 80 列宽的行中居中。为了实现这个功能,需要先生成出 80 个空格。
我们把实现该功能部分独立成一个脚本文件 ‘center.sed’,这是一种常见的技术:
$ cat center.sed
#!/usr/bin/sed -f
# center.sed
# 生成80个空格,并保存到保持空间中
1{ # 地址行号 1,表示只在第一输入行时才执行该命令集
x # 交互两个空间的内容,起到临时保存数据的作用
s/^$/ / # 替换模式空间中的空行成为十个空格
s/^.*$/&&&&&&&&/ # &引用匹配到的十个空格,8个&替换后形成80个空格
x # 交换两个空间的内容,80个空格临时保存,
# 第一行内容重回模式空间
}
# 删除当前行的前导和尾随空白(包括空格和制表符)
s/\t\+/ / # 把制表符替换成空格
s/^ *// # 删除前导空格
s/ *$// # 删除尾随空格
# 当前文本后追加一个换行符和保持空间中的80个空格
G
# 使用替换命令中的分组和反向引用技术,截取模式空间中的前81个字符。
# 截取的内容中包含一个换行符。
s/^\(.\{81\}\).*$/\1/
# 利用替换中的分组和反向引用技术,实现居中
# 替换命令的前半部分 '^\(.*\)\n' 能匹配当前输入行文本及一个换行符,
# 而文本部分为第一分组。则后半部分'\(.*\)\2'设计非常巧妙,通过反向引用,
# 把后面的空格分成相同长度的两半,而空格的数量视文本长度而定,此消彼长。
# 替换部分 '\2\1',巧妙地把一半空格放在当前文本的前面,从而实现功能。
s/^\(.*\)\n\(.*\)\2/\2\1/
以下使用这个脚本实现居中的功能。
$ cat 2.txt
Subject: Hello
world
Content-Type: multipart/alternative;
boundary=94eb2c190cc6370f06054535da6a
Date: Tue, 3 Jan 2017 19:41:16 +0000 (GMT)
Authentication-Results: mx.gnu.org;
dkim=pass header.i=@gnu.org;
spf=pass
Message-ID: <abcdef@gnu.org>
From: John Doe <jdoe@gnu.org>
To: Jane Smith <jsmith@gnu.org>
$ chmod +x center.sed
$./center.sed 2.txt
Subject: Hello
world
Content-Type: multipart/alternative;
boundary=94eb2c190cc6370f06054535da6a
Date: Tue, 3 Jan 2017 19:41:16 +0000 (GMT)
Authentication-Results: mx.gnu.org;
dkim=pass header.i=@gnu.org;
spf=pass
Message-ID: <abcdef@gnu.org>
From: John Doe <jdoe@gnu.org>
To: Jane Smith <jsmith@gnu.org> ./center.sed 2.txt
7.3 数递增
以下脚本演示如何在 sed 中进行算术计算。
要实现输入的整数加 1
,您只需在该数的个位数上加 1
,然后用相加后得到的数字替换原来的数字,例如个位数是 2
,增加 1
后变成 3
,用 3
替换原来的 2
。当然有一个例外,当被加数是 9
时,加 1
后,该位数是 0
,它前面的位要加 1
,这样往前推进,一直到前位的数不是 9
为止。例如数字 1299999
,加 1
后,变成 1300000
。
$ cat increment.sed
#!/usr/bin/sed -f
# increment.sed
# 删除包含有非数字的输入行,立即开启下一轮循环
/[^0-9]/d
# 使用下划线 '_' 替换所有尾随的连续的数字 9
# 例如,299 --> 29_ --> 2__
:d
s/9\(_*\)$/_\1/
td
# 只递增最后一位数字,如果需要进位,下面的第一个
# 's///' 命令将新增一个有效的数字 1
# 以下的十个替换命令类似于多条件选择语句,'switch/case'
s/^\(_*\)$/1\1/; tn # 匹配空行、0 个或多个 _,则在行前增加一个 1
s/8\(_*\)$/9\1/; tn # 在行尾匹配到 8 后跟 0 个或多个 _,则 8 --> 9
s/7\(_*\)$/8\1/; tn
s/6\(_*\)$/7\1/; tn
s/5\(_*\)$/6\1/; tn
s/4\(_*\)$/5\1/; tn
s/3\(_*\)$/4\1/; tn
s/2\(_*\)$/3\1/; tn
s/1\(_*\)$/2\1/; tn
s/0\(_*\)$/1\1/; tn
:n
# 利用 y 命令,替换所有的 _ 成 0
y/_/0/
$ chmod +x increment.sed
$ echo 8 | ./increment.sed
9
$ echo 8299 | ./increment.sed
8300