文本处理三剑客之 sed 流编辑器(高级部分)

文本处理三剑客之 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 多行技术

使用 DGHNP 等命令可以在一个缓冲区中处理多行。这些命令的作用类似于小写的对应项 dghnp。它们除了在空间中追加或删除内容外,还要考虑在两行之间内嵌一个换行符,以实现在缓冲区中处理多行。

具体规则如下:

命令
说明
H先在保持空间中追加一个换行符,再追加模式空间中的内容。
G先在模式空间中追加一个换行符,再追加保持空间中的内容。
N先在模式空间中追加一个换行符,再追加下一输入行文本。
D如果模式空间中没有内嵌换行符,则与执行命令 d 一样,删除模式空间的内容且启动下一轮循环,否则,删除模式空间中第一个换行符及其前面的内容后,立即开始下一轮循环。
P打印模式空间中第一个换行符止及其前面的内容。

下面的示例演示 ND 命令的操作:

$ 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 命令以清空模式空间中的内容,并开始下一轮循环。

后续的两个命令 xs,只有在遇到空行时才执行。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 分支和流程控制

分支命令 btT 能够改变 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///’ 命令,或者另一个条件分支执行后,才会跳转到指定的标签处。
Tt 命令相似,但是条件相反。即只有在上一输入行上执行失败,才会跳转到指定的标签处。

下面的两个 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 分支和循环

分支命令 btT 后面可以指定一个标签(通常是一个单字符),而标签是通过一个冒号和一个或多个字符组成(例如,: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

分支命令通常与 nN 命令互补使用:后面的两个命令都会读取下一输入行到模式空间,而不会开启下一轮循环。在读取下一输入行之前,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 合并行

使用 NDP 命令处理多行、分支命令 bt 的跳转功能,从而实现合并行。

以下示例演示连接指定行,即合并第 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
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值