学习sed的用法(基础知识)  

 
进入 sed

如果可以使编辑文件的过程自动化,以便用“批处理”方式编辑文件,甚至编写可以对现有文件进行复杂更改的脚本,那将太好了。幸运的是,对于这种情况,有一种更好的方法 -- 这种更好的方法称为 "sed"。



sed 是一种几乎包括在所有 UNIX 平台(包括 Linux)的轻量级流编辑器。sed 有许多很好的特性。首先,它相当小巧,通常要比您所喜爱的脚本语言小很多倍。其次,因为 sed 是一种 流编辑器,所以,它可以对从如管道这样的标准输入接收的数据进行编辑。因此,无需将要编辑的数据存储在磁盘上的文件中。因为可以轻易将数据管道输出到 sed,所以,将 sed 用作强大的 shell 脚本中长而复杂的管道很容易。试一下用您所喜爱的编辑器去那样做。




GNU sed

对 Linux 用户来说幸运的是,最好的 sed 版本之一恰好是 GNU sed,其当前版本是 3.02。每一个 Linux 发行版都有(或至少应该有)GNU sed。GNU sed 之所以流行不仅因为可以自由分发其源代码,还因为它恰巧有许多对 POSIX sed 标准便利、省时的扩展。另外,GNU 没有 sed 早期专门版本的很多限制,如行长度限制 -- GNU 可以轻松处理任意长度的行。



sed 示例

sed 通过对输入数据执行任意数量用户指定的编辑操作(“命令”)来工作。sed 是基于行的,因此按顺序对每一行执行命令。然后,sed 将其结果写入标准输出 (stdout),它不修改任何输入文件。



(1)$ sed -e 'd' /etc/services



如 果输入该命令,将得不到任何输出。那么,发生了什么?在该例中,用一个编辑命令 'd' 调用 sed。sed 打开 /etc/services 文件,将一行读入其模式缓冲区,执行编辑命令(“删除行”),然后打印模式缓冲区(缓冲区已为空)。然后,它对后面的每一行重复这些步骤。这不会产生输 出,因为 "d" 命令除去了模式缓冲区中的每一行!



在该例中,还有几件事要注意。首先,根本没有修改 /etc/services。这还是因为 sed 只读取在命令行指定的文件,将其用作输入 -- 它不试图修改该文件。第二件要注意的事是 sed 是面向行的。'd' 命令不是简单地告诉 sed 一下子删除所有输入数据。相反,sed 逐行将 /etc/services 的每一行读入其称为模式缓冲区的内部缓冲区。一旦将一行读入模式缓冲区,它就执行 'd' 命令,然后打印模式缓冲区的内容(在本例中没有内容)。我将在后面为您演示如何使用地址范围来控制将命令应用到哪些行 -- 但是,如果不使用地址,命令将应用到 所有行。



第三件要注意的事是括起 'd' 命令的单引号的用法。养成使用单引号来括起 sed 命令的习惯是个好注意,这样可以禁用 shell 扩展。



(2)地址范围



    下面是使用 sed 从输出流除去 /etc/services 文件第一行的示例:



     $ sed -e '1d' /etc/services | more



     如您所见,除了前面有 '1' 之外,该命令与第一个 'd' 命令十分类似。如果您猜到 '1' 指的是第一行,那您就猜对了。与第一个示例中只使用 'd' 不同的是,这一次使用的 'd' 前面有一个可选的数字地址。通过使用地址,可以告诉 sed 只对某一或某些特定行进行编辑。



    在本例中,sed 将删除输出的第 1 到 10 行:    



     $ sed -e '1,10d' /etc/services | more



   当用逗号将两个地址分开时,sed 将把后面的命令应用到从第一个地址开始、到第二个地址结束的范围。在本例中,将 'd' 命令应用到第 1 到 10 行(包括这两行)。所有其它行都被忽略。



(3)带规则表达式的地址



    现在演示一个更有用的示例。假设要查看 /etc/services 文件的内容,但是对查看其中包括的注释部分不感兴趣。如您所知,可以通过以 '#' 字符开头的行在 /etc/services 文件中放置注释。为了避免注释,我们希望 sed 删除以 '#' 开始的行。以下是具体做法:



   $ sed -e '/^#/d' /etc/services | more



   试一下该例,看看发生了什么。您将注意到,sed 成功完成了预期任务。现在,让我们分析发生的情况。


要 理解 '/^#/d' 命令,首先需要对其剖析。首先,让我们除去 'd' -- 这是我们前面所使用的同一个删除行命令。新增加的是 '/^#/' 部分,它是一种新的 规则表达式地址。规则表达式地址总是由斜杠括起。它们指定一种 模式,紧跟在规则表达式地址之后的命令将仅适用于正好与该特定模式匹配的行。



规则表达式复习



可以使用规则表达式来表示可能会在文本中发现的模式。您在 shell 命令行中用过 '*' 字符吗?这种用法与规则表达式类似,但并不相同。下面是可以在规则表达式中使用的特殊字符:


字符     描述


  ^    与行首匹配


  $    与行末尾匹配


  .    与任一个字符匹配


  *    将与 前一个字符的零或多个出现匹配


[ ]     与 [ ] 之内的所有字符匹配



感受规则表达式的最好方法可能是看几个示例。所有这些示例都将被 sed 作为合法地址接受,这些地址出现在命令的左边。下面是几个示例:


规则 表达式     描述


^

锚定行的开始 如:/^sed/匹配所有以sed开头的行。

$

锚定行的结束 如:/sed$/匹配所有以sed结尾的行。

.

匹配一个非换行符的字符 如:/s.d/匹配s后接一个任意字符,然后是d。

*

匹配零或多个字符 如:/*sed/匹配所有模板是一个或多个空格后紧跟sed的行。

[]

匹配一个指定范围内的字符,如/[Ss]ed/匹配sed和Sed。

[^]

匹配一个不在指定范围内的字符,如:/[^A-RT-Z]ed/匹配不包含A-R和T-Z的一个字母开头,紧跟ed的行。

\(..\)

保存匹配的字符,如s/\(love\)able/\1rs,loveable被替换成lovers。

&

保存搜索字符用来替换其他字符,如s/love/**&**/,love这成**love**。

\<

锚定单词的开始,如:/\<love/匹配包含以love开头的单词的行。

\>

锚定单词的结束,如/love\>/匹配包含以love结尾的单词的行。

x\{m\}

重复字符x,m次,如:/0\{5\}/匹配包含5个o的行。

x\{m,\}

重复字符x,至少m次,如:/o\{5,\}/匹配至少有5个o的行。

x\{m,n\}

重复字符x,至少m次,不多于n次,如:/o\{5,10\}/匹配5--10个o的行。

在这些示例中,鼓励您尝试几个。花一些时间熟悉规则表达式,然后尝试几个自己创建的规则表达式。可以如下使用 regexp:


$ sed -e '/regexp/d' /path//file | more


这将导致 sed 删除任何匹配的行。然而,通过告诉 sed 打印regexp 匹配并删除不匹配的内容,而不是与之相反的方法,会更有利于熟悉规则表达式。可以用以下命令这样做:


$ sed -n -e '/regexp/p' /path/to/my/test/file | more


请注意新的 '-n' 选项,该选项告诉 sed 除非明确要求打印模式空间,否则不这样做。您还会注意到,我们用 'p' 命令替换了 'd' 命令,如您所猜想的那样,这明确要求 sed 打印模式空间。就这样,将只打印匹配部分。



有关地址的更多内容

目 前为止,我们已经看到了行地址、行范围地址和 regexp 地址。但是,还有更多的可能。我们可以指定两个用逗号分开的规则表达式,sed 将与所有从匹配第一个规则表达式的第一行开始,到匹配第二个规则表达式的行结束(包括该行)的所有行匹配。例如,以下命令将打印从包含 "BEGIN" 的行开始,并且以包含 "END" 的行结束的文本块:


$ sed -n -e '/BEGIN/,/END/p' /my/test/file | more


如果没发现 "BEGIN",那么将不打印数据。如果发现了 "BEGIN",但是在这之后的所有行中都没发现 "END",那么将打印所有后续行。发生这种情况是因为 sed 面向流的特性 -- 它不知道是否会出现 "END"。



如果只要打印 C 源文件中的 main() 函数,可输入:



$ sed -n -e '/main[[:space:]]*(/,/^}/p' main.c



该 命令有两个规则表达式 '/main[[:space:]]*(/' 和 '/^}/',以及一个命令 'p'。第一个规则表达式将与后面依次跟有任意数量的空格或制表键以及开始圆括号的字符串 "main" 匹配。这应该与一般 ANSI C main() 声明的开始匹配。



在这个特别的规则表达式中,出现了 '[[:space:]]' 字符类。这只是一个特殊的关键字,它告诉 sed 与 TAB 或空格匹配。如果愿意的话,可以不输入 '[[:space:]]',而输入 '[',然后是空格字母,然后是 -V,然后再输入制表键字母和 ']' -- Control-V 告诉 bash 要插入“真正”的制表键,而不是执行命令扩展。使用 '[[:space:]]' 命令类(特别是在脚本中)会更清楚。



好,现在看一下第二个 regexp。'/^}' 将与任何出现在新行行首的 '}' 字符匹配。如果代码的格式很好,那么这将与 main() 函数的结束花括号匹配。如果格式不好,则不会正确匹配 -- 这是执行模式匹配任务的一件棘手之事。



因为是处于 '-n' 安静方式,所以 'p' 命令还是完成其惯有任务,即明确告诉 sed 打印该行。试着对 C 源文件运行该命令 -- 它应该输出整个 main() { } 块,包括开始的 "main()" 和结束的 '}'。




Sed的替换操作


让我们看一下 sed 最有用的命令之一,替换命令。使用该命令,可以将特定字符串或匹配的规则表达式用另一个字符串替换。下面是该命令最基本用法的示例:




 $ sed -e 's/foo/bar/'  myfile.txt




上 面的命令将 myfile.txt 中每行第一次出现的 'foo'(如果有的话)用字符串 'bar' 替换,然后将该文件内容输出到标准输出。请注意,我说的是 每行第一次出现,尽管这通常不是您想要的。在进行字符串替换时,通常想执行全局替换。也就是说,要替换每行中的 所有出现,如下所示:




$ sed -e 's/foo/bar/g' myfile.txt




在最后一个斜杠之后附加的 'g' 选项告诉 sed 执行全局替换。



关于 's///' 替换命令,还有其它几件要了解的事。首先,它是一个命令,并且只是一个命令,在所有上例中都没有指定地址。这意味着,'s///' 还可以与地址一起使用来控制要将命令应用到哪些行,如下所示:




 $ sed -e '1,10s/enchantment/entrapment/g' myfile2.txt


 




上例将导致用短语 'entrapment' 替换所有出现的短语 'enchantment',但是只在第一到第十行(包括这两行)上这样做。




 $ sed -e '/^$/,/^END/s/hills/mountains/g' myfile3.txt 




该例将用 'mountains' 替换 'hills',但是,只从空行开始,到以三个字符 'END' 开始的行结束(包括这两行)的文本块上这样做。



关 于 's///' 命令的另一个妙处是 '/' 分隔符有许多替换选项。如果正在执行字符串替换,并且规则表达式或替换字符串中有许多斜杠,则可以通过在 's' 之后指定一个不同的字符来更改分隔符。例如,下例将把所有出现的 /usr/local 替换成 /usr:




 $ sed -e 's:/usr/local:/usr:g' mylist.txt




在该例中,使用冒号作为分隔符。如果需要在规则表达式中指定分隔符字符,可以在它前面加入反斜杠。



规则表达式混乱

目前为止,我们只执行了简单的字符串替换。虽然这很方便,但是我们还可以匹配规则表达式。例如,以下 sed 命令将匹配从 '<' 开始、到 '>' 结束、并且在其中包含任意数量字符的短语。下例将删除该短语(用空字符串替换):




 $ sed -e 's/<.*>//g' myfile.html 




这 是要从文件除去 HTML 标记的第一个很好的 sed 脚本尝试,但是由于规则表达式的特有规则,它不会很好地工作。原因何在?当 sed 试图在行中匹配规则表达式时,它要在行中查找 最长的匹配。在我的 前一篇 sed 文章中,这不成问题,因为我们使用的是 'd' 和 'p' 命令,这些命令总要删除或打印整行。但是,在使用 's///' 命令时,确实有很大不同,因为规则表达式匹配的整个部分将被目标字符串替换,或者,在本例中,被删除。这意味着,上例将把下行:




 <b>This</b> is what <b>I</b> meant. 




变成:




 meant. 




我们要的不是这个,而是:




 This is what I meant. 




幸 运的是,有一种简便方法来纠正该问题。我们不输入“'<' 字符后面跟有一些字符并以 '>' 字符结束”的规则表达式,而只需输入一个“'<' 字符后面跟有任意数量非 '>' 字符并以 '>' 字符结束”的规则表达式。这将与最短、而不是最长的可能性匹配。新命令如下:




 $ sed -e 's/<[^>]*>//g' myfile.html




在上例中,'[^>]' 指定“非 '>'”字符,其后的 '*' 完成该表达式以表示“零或多个非 '>' 字符”。对几个 html 文件测试该命令,将它们管道输出到 "more",然后仔细查看其结果。



高级替换功能

我们已经看到如何执行简单甚至有些复杂的直接替换,但是 sed 还可以做更多的事。实际上可以引用匹配规则表达式的部分或全部,并使用这些部分来构造替换字符串。作为示例,假设您正在回复一条消息。下例将在每一行前面加上短语 "ralph said: ":




 $ sed -e 's/.*/ralph said: &/' origmsg.txt




输出如下:




 ralph said: Hiya Jim, ralph said: 


 ralph said: I sure like this sed stuff! ralph said: 




该例的替换字符串中使用了 '&' 字符,该字符告诉 sed 插入整个匹配的规则表达式。因此,可以将与 '.*' 匹配的任何内容(行中的零或多个字符的最大组或整行)插入到替换字符串中的任何位置,甚至多次插入。这非常好,但 sed 甚至更强大。



那些极好的带反斜杠的圆括号

's///' 命令甚至比 '&' 更好,它允许我们在规则表达式中定义 区域,然后可以在替换字符串中引用这些特定区域。作为示例,假设有一个包含以下文本的文件:




 foo bar oni eeny meeny miny larry curly moe jimmy the weasel 




现在假设要编写一个 sed 脚本,该脚本将把 "eeny meeny miny" 替换成 "Victor eeny-meeny Von miny" 等等。要这样做,首先要编写一个由空格分隔并与三个字符串匹配的规则表达式。




 '.* .* .*' 




现在,将在其中每个感兴趣的区域两边插入带反斜杠的圆括号来定义区域:




 '\(.*\) \(.*\) \(.*\)' 




除了要定义三个可在替换字符串中引用的逻辑区域以外,该规则表达式的工作原理将与第一个规则表达式相同。下面是最终脚本:




 $ sed -e 's/\(.*\) \(.*\) \(.*\)/Victor \1-\2 Von \3/' myfile.txt 




如您所见,通过输入 '\x'(其中,x 是从 1 开始的区域号)来引用每个由圆括号定界的区域。输入如下:




 Victor foo-bar Von oni Victor eeny-meeny Von miny Victor larry-curly Von moe Victor jimmy-the Von weasel 




随着对 sed 越来越熟悉,您可以花最小力气来进行相当强大的文本处理。您可能想如何使用熟悉的脚本语言来处理这种问题 -- 能用一行代码轻易实现这样的解决方案吗?



组合使用

在 开始创建更复杂的 sed 脚本时,需要有输入多个命令的能力。有几种方法这样做。首先,可以在命令之间使用分号。例如,以下命令系列使用 '=' 命令和 'p' 命令,'=' 命令告诉 sed 打印行号,'p' 命令明确告诉 sed 打印该行(因为处于 '-n' 模式)。




 $ sed -n -e '=;p' myfile.txt 




无 论什么时候指定了两个或更多命令,都按顺序将每个命令应用到文件的每一行。在上例中,首先将 '=' 命令应用到第 1 行,然后应用 'p' 命令。接着,sed 继续处理第 2 行,并重复该过程。虽然分号很方便,但是在某些场合下,它不能正常工作。另一种替换方法是使用两个 -e 选项来指定两个不同的命令:




 $ sed -n -e '=' -e 'p' myfile.txt 




然而,在使用更为复杂的附加和插入命令时,甚至多个 '-e' 选项也不能帮我们的忙。对于复杂的多行脚本,最好的方法是将命令放入一个单独的文件中。然后,用 -f 选项引用该脚本文件:




 $ sed -n -f mycommands.sed myfile.txt




这种方法虽然可能不太方便,但总是管用。



一个地址的多个命令



有时,可能要指定应用到一个地址的多个命令。这在执行许多 's///' 以变换源文件中的字和语法时特别方便。要对一个地址执行多个命令,可在文件中输入 sed 命令,然后使用 '{ }' 字符将这些命令分组,如下所示:




 1,20{     s/[Ll]inux/GNU\/Linux/g     s/samba/Samba/g     s/posix/POSIX/g } 




上例将把三个替换命令应用到第 1 行到第 20 行(包括这两行)。还可以使用规则表达式地址或者二者的组合:




 1,/^END/{         s/[Ll]inux/GNU\/Linux/g         s/samba/Samba/g         s/posix/POSIX/g     p }




该例将把 '{ }' 之间的所有命令应用到从第 1 行开始,到以字母 "END" 开始的行结束(如果在源文件中没发现 "END",则到文件结束)的所有行。



附加、插入和更改行

既然在单独的文件中编写 sed 脚本,我们可以利用附加、插入和更改行命令。这些命令将在当前行之后插入一行,在当前行之前插入一行,或者替换模式空间中的当前行。它们也可以用来将多行插入到输出。插入行命令用法如下:




i\ This line will be inserted before each line 




如果不为该命令指定地址,那么它将应用到每一行,并产生如下的输出:




This line will be inserted before each line line 1 here


 This line will be inserted before each line line 2 here


 This line will be inserted before each line line 3 here


 This line will be inserted before each line line 4 here 




如果要在当前行之前插入多行,可以通过在前一行之后附加一个反斜杠来添加附加行,如下所示:




 i\ insert this line\ and this one\ and this one\ and, uh, this one too. 




附加命令的用法与之类似,但是它将把一行或多行插入到模式空间中的当前行之后。其用法如下:




 a\ insert this line after each line.  Thanks! :) 




另一方面,“更改行”命令将实际 替换模式空间中的当前行,其用法如下:




 c\ You're history, original line! Muhahaha!




因为附加、插入和更改行命令需要在多行输入,所以将把它们输入到一个文本 sed 脚本中,然后通过使用 '-f' 选项告诉 sed 执行它们。使用其它方法将命令传递给 sed 会出现问题。