The Sed uppercase `Next` command
这些命令的使用场景主要用于实现队列( FIFO 列表 )。从一个输入文件中删除最后 5 行就是一个很权威的例子:
cat -n inputfile | sed -En -e '
1 { N;N;N;N } # 确保模式空间中包含 5 行
N # 追加第 6 行到队列中
P # 输出队列的第 1 行
D # 删除队列的第 1 行
'
作为第二个示例,我们可以在两个列上显示输入数据:
分支# 输出两列
sed < inputfile -En -e '
$!N # 追加一个新行到模式空间
# 除了输入文件的最后一行
# 当在输入文件的最后一行使用 N 命令时
# GNU Sed 和 POSIX Sed 的行为是有差异的
# 需要使用一个技巧去处理这种情况
# https://www.gnu.org/software/sed/manual/sed.html#N_005fcommand_005flast_005fline
# 用空间填充第 1 行的第 1 个字段
# 并丢弃其余行
s/:.*\n/ \n/
s/:.*// # 除了第 2 行上的第 1 个字段外,丢弃其余的行
s/(.{20}).*\n/\1/ # 修剪并连接行
p # 输出结果
'
我们刚才已经看到,Sed 因为有保持空间所以有了缓存的功能。其实它还有测试和分支的指令。因为有这些特性使得 Sed 是一个 图灵完备 的语言。虽然它可能看起来很傻,但意味着你可以使用 Sed 写任何程序。你可以实现任何你的目的,但并不意味着实现起来会很容易,而且结果也不一定会很高效。
不过不用担心。在本文中,我们将使用能够展示测试和分支功能的最简单的例子。虽然这些功能乍一看似乎很有限,但请记住,有些人用 Sed 写了 http://www.catonmat.net/ftp/sed/dc.sed [计算器]、 http://www.catonmat.net/ftp/sed/sedtris.sed [俄罗斯方块] 或许多其它类型的应用程序!
标签和分支从某些方面,你可以将 Sed 看到是一个功能有限的汇编语言。因此,你不会找到在高级语言中常见的 “for” 或 “while” 循环,或者 “if … else” 语句,但是你可以使用分支来实现同样的功能。
The Sed branch command
如果你在本文开始部分看到了用流程图描述的 Sed 运行模型,那么你应该知道 Sed 会自动增加程序计数器(PC)的值,命令是按程序的指令顺序来运行的。但是,使用分支(b)指令,你可以通过选择执行程序中的任意命令来改变顺序运行的程序。跳转目的地是使用一个标签(:)来显式定义的。
The Sed label command
这是一个这样的示例:
echo hello | sed -ne '
:start # 在程序的该行上放置一个 “start” 标签
p # 输出模式空间内容
b start # 继续在 :start 标签上运行
' | less
那个 Sed 程序的行为非常类似于 yes 命令:它获取一个字符串并产生一个包含那个字符串的无限流。
切换到一个标签就像我们绕开了 Sed 的自动化特性一样:它既不读取任何输入,也不输出任何内容,更不更新任何缓冲区。它只是跳转到源程序指令顺序中下一条的另外一个指令。
值得一提的是,如果在分支命令(b)上没有指定一个标签作为它的参数,那么分支将直接切换到程序结束的地方。因此,Sed 将启动一个新的循环。这个特性可以用于去跳过一些指令并且因此可以用于作为“块”的替代者:
条件分支cat -n inputfile | sed -ne '
/usb/!b
/daemon/!b
p
'
到目前为止,我们已经看到了无条件分支,这个术语可能有点误导嫌疑,因为 Sed 命令总是基于它们的可选地址来作为条件的。
但是,在传统意义上,一个无条件分支也是一个分支,当它运行时,将跳转到特定的目的地,而条件分支既有可能也或许不可能跳转到特定的指令,这取决于系统的当前状态。
Sed 只有一个条件指令,就是测试(t)命令。只有在当前循环的开始或因为前一个条件分支运行了替换,它才跳转到不同的指令。更多的情况是,只有替换标志被设置时,测试命令才会切换分支。
The Sed `test` command
使用测试指令,你可以在一个 Sed 程序中很轻松地执行一个循环。作为一个特定的示例,你可以用它将一个行填充到某个长度(这是使用正则表达式无法实现的):
# 居中文本
cut -d: -f1 inputfile | sed -Ee '
:start
s/^(.{,19})$/ \1 / # 用一个空格填充少于 20 个字符的行的开始处
# 并在结束处添加另一个空格
t start # 如果我们已经添加了一个空格,则返回到 :start 标签
s/(.{20}).*/| \1 |/ # 只保留一个行的前 20 个字符
# 以修复由于奇数行引起的差一错误
'
如果你仔细读前面的示例,你可能注意到,在将要把数据“喂”给 Sed 之前,我通过 cut 命令做了一点小修正去预处理数据。
不过,我们也可以只使用 Sed 对程序做一些小修改来执行相同的任务:
cat inputfile | sed -Ee '
s/:.*// # 除第 1 个字段外删除剩余字段
t start
:start
s/^(.{,19})$/ \1 / # 用一个空格填充少于 20 个字符的行的开始处
# 并在结束处添加另一个空格
t start # 如果我们已经添加了一个空格,则返回到 :start 标签
s/(.{20}).*/| \1 |/ # 仅保留一个行的前 20 个字符
# 以修复由于奇数行引起的差一错误
'
在上面的示例中,你或许对下列的结构感到惊奇:
t start
:start
乍一看,在这里的分支并没有用,因为它只是跳转到将要运行的指令处。但是,如果你仔细阅读了测试命令的定义,你将会看到,如果在当前循环的开始或者前一个测试命令运行后发生了一个替换,分支才会起作用。换句话说就是,测试指令有清除替换标志的副作用。这也正是上面的代码片段的真实目的。这是一个在包含条件分支的 Sed 程序中经常看到的技巧,用于在使用多个替换命令时避免出现 误报(false positive)的情况。
通过它并不能绝对强制地清除替换标志,我同意这一说法。因为在将字符串填充到正确的长度时我使用的特定的替换命令是 幂等(idempotent)的。因此,一个多余的迭代并不会改变结果。不过,我们可以现在再次看一下第二个示例:
# 基于它们的登录程序来分类用户帐户
cat inputfile | sed -Ene '
s/^/login=/
/nologin/s/^/type=SERV /
/false/s/^/type=SERV /
t print
s/^/type=USER /
s/:.*//p
'
我希望在这里根据用户默认配置的登录程序,为用户帐户打上 “SERV” 或 “USER” 的标签。如果你运行它,预计你将看到 “SERV” 标签。然而,并没有在输出中跟踪到 “USER” 标签。为什么呢?因为 t print 指令不论行的内容是什么,它总是切换,替换标志总是由程序的第一个替换命令来设置。一旦替换标志设置完成后,在下一个行被读取或直到下一个测试命令之前,这个标志将保持不变。下面我们给出修复这个程序的解决方案:
精确地处理文本# 基于用户登录程序来分类用户帐户
cat inputfile | sed -Ene '
s/^/login=/
t classify # clear the "substitution flag"
:classify
/nologin/s/^/type=SERV /
/false/s/^/type=SERV /
t print
s/^/type=USER /
s/:.*//p
'
Sed 是一个非交互式文本编辑器。虽然是非交互式的,但仍然是文本编辑器。而如果没有在输出中插入一些东西的功能,那它就不算一个完整的文本编辑器。我不是很喜欢它的文本编辑的特性,因为我发现它的语法太难用了(即便是以 Sed 的标准而言),但有时你难免会用到它。
采用严格的 POSIX 语法的只有三个命令:改变(c)、插入(i)或追加(a)一些文字文本到输出,都遵循相同的特定语法:命令字母后面跟着一个反斜杠,并且文本从脚本的下一行上开始插入:
head -5 inputfile | sed '
1i\
# List of user accounts
$a\
# end
'
插入多行文本,你必须每一行结束的位置使用一个反斜杠:
head -5 inputfile | sed '
1i\
# List of user accounts\
# (users 1 through 5)
$a\
# end
'
一些 Sed 实现,比如 GNU Sed,在初始的反斜杠后面的换行符是可选的,即便是在 --posix 模式下仍然如此。我在标准中并没有找到任何关于该替代语法的说明(如果是因为我没有在标准中找到那个特性,请在评论区留言告诉我!)。因此,如果对可移植性要求很高,请注意使用它的风险:
# 非 POSIX 语法:
head -5 inputfile | sed -e '
1i\# List of user accounts
$a\# end
'
也有一些 Sed 的实现,让初始的反斜杠完全是可选的。因此毫无疑问,它是一个厂商对 POSIX 标准进行扩展的特定版本,它是否支持那个语法,你需要去查看那个 Sed 版本的手册。
在简单概述之后,我们现在来回顾一下这些命令的更多细节,从我还没有介绍的改变命令开始。
改变命令改变命令(c\)就像 d 命令一样删除模式空间的内容并开始一个新的循环。唯一的不同在于,当命令运行之后,用户提供的文本是写往输出的。