好用的 sed

作者:Hans de Vreught

当我于 1982 年开始使用 UNIX 时,计算机世界的生活相当残酷。那时,大多数程序员仍在使用行编辑器。UNIX 行编辑器 ed,与大多数其他操作系统的行编辑器相比,是一种解脱。它合理地使用正则表达式是一种恩赐,有趣的是,大多数 UNIX 工具都使用了相同类型的正则表达式。

尽管 UNIX 拥有虚拟内存,但 ed 可以处理的文件大小是有限的;用于内存的磁盘空间非常昂贵。对于大型文件,程序员不得不求助于流编辑器 sedsed 逐行读取其输入,并逐行执行其编辑操作。在 sed 中,一些命令允许使用多行,因此有一个暂存空间,但总的来说,所需的内存量很小。

除了偶尔使用单行命令外,我经常编写 sed 脚本。在那些日子里,大多数系统管理脚本都是用 sed 编写的;awk 太慢且体积太大。这些 sed 脚本所展示的力量过去和现在仍然令人惊叹。它们是真正的艺术品——庞大且完全难以理解,但它们完成了工作。

由于 sed 是图灵完备的,它与任何编程语言一样强大。编写计算特定功能的 sed 脚本成为了一种乐趣。 Linux Network Administrator's Guide 的作者 Olaf Kirch 在他的前言中说,他很自豪用 sed 编写了一个质数生成器。我的个人脚本计算 Ackermann 函数,并且可以在 ftp://ftp.linuxjournal.com/pub/lj/listings/issue60/2628.tgz 文件中匿名下载, साथ एक संक्षिप्त 설명 के साथ। 它就像用汇编语言编程一样。

今天,sed 脚本完全不同了——它们更简单,几乎总是单行命令。大多数单行 sed 命令(通常包含在 Bourne 脚本中或在您选择的 shell 中交互使用)修改或删除文件中的某些行。在某些情况下,您可能仍然编写 sed 脚本;但是,命令仍然很简单。除了刚刚提到的两个操作之外,您还可以插入、追加和更改成块的行组。

高级 sed 命令已经消失了(我必须说我很高兴)。尽管这些高级命令使 sed 功能强大,但它们也使脚本难以阅读。今天,如果您需要做一些高级的事情,您将使用 awk 或 Perl。

语法

我不会描述 sed 的所有功能。相反,我将仅限于我经常使用的那些命令。有关 sed 的更多信息,最好的资源是由 Dale Dougherty 和 Arnold Robbins 编写的 sed & awk (O'Reilly & Associates, 1997)。

sed 命令具有以下形式,没有尾随空格

[address][,address][!]command[arguments]

我将从地址开始。地址可以是行号($ 代表最后一行)或用斜杠括起来的正则表达式。正则表达式类似于你在 vi 中看到的那些(实际上是 vi 的 ex 部分):“.”(任何字符),“*”(紧接在前的正则表达式的任意数量),“[class]”(类中的任何字符),“[^class]”(不在类中的任何字符),“^”(行首),“$”(行尾)和 ''\''(在需要时转义字符)。

可以通过给出两个地址来指定行范围。地址规范后的“!”排除该范围被处理。最常用的 sed 命令是“d”(删除)和“s”(替换)。删除命令很简单;它删除任何与整个地址规范匹配的行。替换更有趣

s/pattern/replacement/[g]

基本上,pattern 只是一个正则表达式,但它有一个奇怪的特性:模式的部分可以存储在替换中。要保留的部分必须用字符“\(”和“\)”括起来。在替换部分,可以通过指定“\1”、“\2”、...、“\9”(第一个、第二个、...、第九个存储部分)来使用这些存储的部分。如果要使用整个匹配部分,则指定“&”字符。可以使用 g(全局)标志来替换 pattern 的所有出现项为 replacement

示例

假设我们有一个由两列数字组成的小表格,我们希望交换它们

s/\([0-9]*\) \([0-9]*\)/\2 \1/

“[0-9]”部分匹配任何数字,“*”表示我们允许匹配任何数字字符串。由于“[0-9]*”被“\(”和“\)”包围,因此它被存储起来。该行的第一个数字将在“\1”中,第二个数字将在“\2”中。在替换中,它们被回忆起来:先是第二个数字,然后是第一个数字。虽然编写一个只有两行的文件有点傻,但它是

#!/usr/bin/sed -f
s/\([0-9]*\) \([0-9]*\)/\2 \1/
-f 标志表示下一个参数是一个包含 sed 脚本的文件名。UNIX 执行脚本的方式,这意味着脚本名称将用作 -f 的参数(即,此文件)。这是在 shell 的命令行提示符下使用的单行等效命令(-e 标志指定下一个参数是 sed 命令)
sed -e 's/\([0-9]*\) \([0-9]*\)/\2 \1/'
要在特定范围内执行多个命令,请使用 { 和 } 括起一个命令块。假设在该文件的头部,我们希望将 C++ 风格的注释(“// ...”)替换为 C 风格的注释(“/* ... */”),并且我们希望更新版权年份。页眉后会出现一个空行,如下所示
// Copyright 1997 John Doe
// This is just an example.
...
我们可以使用以下脚本(注意反斜杠的大量转义)来修改此页眉
#!/usr/bin/sed -f"
1,/^$/{
s/Copyright 1997/Copyright 1998/
s/\/\/\(.*\)/\/*\1 *\//
}
这些命令将修改上面的页眉为
/* Copyright 1998 John Doe */
/* This is just an example. */
...
插入、追加和更改

我在脚本中经常使用的下一组 sed 命令是“i”(插入)、“a”(追加)和“c”(更改)。插入和追加类似,因为它们都需要一个地址,文本将被插入或追加到该地址之前或之后。插入的文本在接下来的行中。插入或追加命令的所有行都必须以反斜杠“终止”,但最后一行除外。例如

1i\
This is a test\
to insert three lines\
at the beginning

使用 change 命令,我们还可以指定行范围。如果我们只指定一个地址,则只会更改该行;否则,将修改整个范围。

有时,从文件读取(“r”)或写入(“w”)文件很方便。在读取的情况下,我们可以指定一个地址,在该地址之后读取文件;在写入的情况下,我们可以指定一个地址或要写入文件的范围。例如,要将第一个空行之前的行写入名为 foo 的文件,请输入命令

#!/usr/bin/sed -f 1,/^$/w foo

sed 对空格非常挑剔——“w”或“r”后只允许一个空格。此外,永远不要在 sed 命令中使用尾随空格;它不喜欢它们,并且会给出神秘的错误。

下一个和退出

我将讨论的最后两个命令是 next(“n”)和 quit(“q”)。next 命令采用单个地址。在输出指定的行后,读取下一行,脚本从顶部恢复。通常,next 命令在命令块内找到(即,在花括号内)。quit 命令也采用地址作为参数。当 sed 遇到 quit 时,它会立即终止脚本,而不会有任何进一步的输出。

高级

sed 还有其他几个命令,可以被认为是高级命令。您不会在现代 sed 脚本中看到它们。有三种高级命令可用

  • 条件命令:在 sed 中,您可以通过在标识符(最多 7 个字符)前面加上冒号(“:”)来定义标签,然后使用 test (-t) 命令跳转到该标签。Test 检查是否进行了替换。

  • 多行命令:sed 有几个命令可以处理多行,将它们视为带有嵌入换行符的单行。这些命令使 sed 脚本难以阅读。

  • 暂存空间命令:sed 能够保存当前行并将其与单独的行(称为暂存空间)交换。对于多行命令来说是真的,对于处理暂存空间的命令来说更是如此——它们真的使您的脚本难以阅读。

正如我之前所说,对于单行命令和简单脚本,请使用 sed,对于高级命令,请使用 awk 和 Perl。对于那些希望赢得虚拟啤酒的人:编写一个计算阶乘函数的 sed 脚本。

Hans de Vreught 是代尔夫特理工大学的计算机科学研究员。他自 1982 年以来一直使用 UNIX(自 0.99.13 以来使用 Linux)。他喜欢非虚拟的比利时啤酒,并且是一个真正的环球旅行家(已经环游世界两次)。可以通过 J.P.M.deVreught@cs.tudelft.nl 与他联系。

加载 Disqus 评论