- Published on
sed介绍和教程(译)
- Authors
- Name
- 祝你好运
这是一篇Sed的简单介绍和教程,是我翻译的,很实用,适合新手。这篇教程需要你了解正则表达式
sed介绍
如果你想写一个程序来对一个文件做一些改动,那sed就是你应该使用的工具。这篇文章就是教你如何很自然的使用修改文件的特殊编辑器。
在Unix工具中有一些程序是真正的重型武器。这些程序不仅在简单的任务中很容易使用,而且还有大量的命令可以进行复杂的操作。不要因为惧怕它强大但是比较复杂的命令,而放弃使用它简单的一面。我会先介绍一些简单的概念,然后引入一些高级的话题。 当我开始写这篇文章时(1994年),大多数版本的sed不允许你将注释放到脚本里,以’#’字符开始的行就是注释。新的版本的sed可能也支持行尾注释。 你可以这么来想:这个古老的、经典的sed是GNU、FreeBSD和Solaris的sed的鼻祖。你可以参考一下Sun/Oracle的sed手册来帮助你理解。
关于sed的可怕事实
Sed是一个终极的流编辑器(stream edditor)。如果你感觉流编辑器听起来很奇怪,就想象一下管道里面的水流。好吧,如果这个水流是在管道里,你不可能看到水流。这就是我拿水流比喻的原因。
不管怎么说,sed是一个非常有用的工具。不幸的是,大多数人没见识过它真正的力量。它很简单,但文档糟糕透了。Solais上关于sed的在线文档有5页长,其中2页描述了34种你可能会看到的错。如果一个程序的文档通篇都在讲各种错误,那你学习它时会有着很陡峭的学习曲线。
别发愁!不懂sed不是你的错,我将在下面介绍sed的方方面面,但我介绍sed的那些特性时,我会以我学习它们的顺序来介绍,我也不是一下子就把它们全学会了。嗯,你当然也不需要。
基本命令s:替换
Sed有好几个命令,但大多数人只学了一个替换命令:s。替换命令把所有匹配正则表达式的值替换为一个新值,一个简单的例子可以看下面的命令,它将在“old”文件中的“day”替换为“night”后存入“new”文件中。
sed s/day/night/ <old >new
或者用另一种方式(对于Unix新手来说),
sed s/day/night/ old >new
你可以这样在命令行中试一下:
echo day | sed s/day/night/
这会输出“night”
我没有给参数加引号是因为在这个例子中不需要加。但是我建议你加上引号。如果你在命令中有元字符,引号是必须的。我了为强调这是一个好的习惯,我会在下面的所有示例中都加上单引号,如果加上引号,那么上例就是:
sed 's/day/night/' <old >new
我必须强调的是sed会严格执行你的命令,如果你执行
echo Sunday | sed 's/day/night/'
这将会输出“Sunnight”因为它发现输入有“day”这个字符串。
另一个重要的概念是sed是基于行的,假设是下面的输入文件:
one two three, one two three
four three two one
one hundred
然后你用命令:
sed 's/one/ONE/' <file
输出就是:
ONE two three, one two three
four three two ONE
ONE hundred
注意这里每一行的第一个“one”都换成了“ONE”,即使是后面还有也不会继续替换,这是默认行为。如果你想全部都替换,你就需要指定一些替换的选项,后面我会介绍。
好,让我们继续
替换命令由四个组成部分:
组成部分 | 解释 |
---|---|
s | 替换命令 |
/../../ | 分隔符 |
one | 正则表达式模式,查找模式 |
ONE | 替换字符串 |
查找模式在左边,替换字符串在右边。
我们已经学习了引用和正则表达式。那是学习替换命令90%的工作量。换句话说,你已经能用sed处理90%最常见的情况,但仍有那么一些sed高手需要了解的内容(你已经看了第1节,还有63节就看完了。),噢,你应该收藏这个页面,万一你这次看不完呢。
用斜线作为分隔符
在s后面的字符是分隔符。这是一个通用的分隔符,因为ed,more和vi都这么用。但是你也可以用任何你喜欢的东西。如果你想要替换一个包含斜线的路径-比如说把/usr/local/bin替换成/common/bin-你可以用反斜线来引住斜线:
sed 's/\/usr\/local\/bin/\/common\/bin/' <old >new
呼,一些人把这称作“Pocket Fence”(译注:这是一个电视剧,我也没看过,貌似是想说很难看?),这太丑了。如果你用下划线而不是斜线作为分隔符,它就会好读很多:
sed 's_/usr/local/bin_/common/bin_' <old >new
一些人用引号:
sed 's:/usr/local/bin:/common/bin:' <old >new
也有人用“|”:
sed '|/usr/local/bin|/common/bin|' <old >new
挑一个你喜欢的,只要不是出现在你要查找的字符串中的字符都可以。记住你需要3个分隔符。如果你碰到一个"Unterminated 's' command"错误,那就是说你少输入了分隔符。
用&作为匹配到的串
有时你想查找一个模式,然后加上几个字符,比如在匹配的串前后加上括号。如果你是找一个确定的字符串,还是比较简单的,如下即可:
sed 's/abc/(abc)/' <old >new
但是如果你不确定要查找的东西的话,这个方式就行不通了。你怎么把连你都不知道是什么的匹配到的字符串放到替换字符串里面呢?
解决办法需要特殊字符“&”,它代表匹配到的模式:
sed 's/[a-z]*/(&)/' <old >new
你可以在替换时多次使用“&”。比如,你可以把每行开头的数字复制一次:
% echo "123 abc" | sed 's/[0-9]*/& &/'
123 123 abc
让我再修正一下这个例子,sed会匹配第一个字符串,并且是以贪婪的方式匹配,我将会在后面讲到。如果你不想要贪婪匹配(也就是有限制的匹配),你需要在匹配上面做限制。
对于‘[0-9]*’的第一个区配是第一个字符,因为这个正则是匹配0个或多个数字。所以如果输入是”abc 123”,输出就不会改变(当然,除了有一个空格在字母之前)。一个更好的复制数字的方法是确保匹配到的就是一个数字:
% echo "123 abc" | sed 's/[0-9][0-9]*/& &/'
123 123 abc
字符串“abc”没有改变,因为它没有匹配正则表达式,如果你想在输出中删除“abc”,你必须扩展正则表达式来匹配行的其它的部分,并显式地用“(”,“)”和“\1”来指名,这就是下节的内容了。
扩展的正则表达式
让我简单的解释一下,因为有另外一种方式来写这个脚本。“[0-9]”匹配0到多个数字。“[0-9][0-9]”匹配一到多个数字。另一种方式是用元字符“+”和模式“[0-9]+”,因为“+”是“扩展的正则表达式”中的一个特殊字符。扩展的正则表达式更加强大,但是sed只会把“+”作为一般字符因而无法工作。因此你必须显式的用一个命令行选项来启用它。
如果你用“-r”命令行选项,GNU sed就会启用这个功能。那么上面的脚本就变成:
% echo "123 abc" | sed -r 's/[0-9]+/& &/'
123 123 abc
Mac OS X和FreeBSD用-E而不是-r。更多关于扩展表达式的信息请看正则表达式和-r命令行参数的描述
用\1来指明匹配的部分
我已经在正则表达式中介绍了“(” “)”和“\1”的用法。现在复习一下,被转义的括号(即,有反斜杠在前面的括号)来记录正则表达的某一部分。“\1”是被记录的第一个模式,”\2”是第二个被记录的模式。Sed可以记录最多9个模式。
如果你想要保留一行的第一个单词删除剩余的部分,用括号来标记这个重要的部分:
sed 's/\([a-z]*\).*/\1/'
我应该再详细地解释一下。正则表达式是以贪婪的方式匹配。“[a-z]”匹配0个或多个小写字母,它会尽量匹配更多的小写字母。“.”会在第一个匹配后匹配0个或多个字符。因为第一个模式已经匹配了所有的小写字母,第二个模式会匹配剩下的字符,所以你使用命令:
echo abcd123 | sed 's/\([a-z]*\).*/\1/'
会删除后面的数字输出“abcd” 如果你想调换两个单词的位置,你可记录两个模式,并改变它们的次序。
sed 's/\([a-z]*\) \([a-z]*\)/\2 \1/'
注意两个模式中间是有空格的。这个可保证找到两个单词。但是[a-z]*这种模式会匹配0个字母,如果你想至少匹配一个字母,你可以用下面的命令:
sed 's/\([a-z][a-z]*\) \([a-z][a-z]*\)/\2 \1/'
或者是用不需要反斜线的扩展的正则表达式
sed -r 's/([a-z]+) ([a-z]+)/\2 \1/' # GNU sed
sed -E 's/([a-z]+) ([a-z]+)/\2 \1/' # Apple Mac OS X
“\1”并不是只能出现在替换串中(右部),它你也可以在查找的模式中(左部)。如果你想删除重复的单词,你可以用:
sed 's/\([a-z]*\) \1/\1/'
如果你想要检测重复的单词,你可以用
sed -n '/\([a-z][a-z]*\) \1/p'
或者是用扩展的正则表达式
sed -rn '/([a-z]+) \1/p' # GNU sed
sed -En '/([a-z]+) \1/p' # Mac OS X
当你把这个用作过滤器的时候它就会打印带有重复单词的行。
注意你最多使用9个值,从”\1”到”\9”。如果你想要逆序一行的前三个字符,你可以用:
sed 's/^\(.\)\(.\)\(.\)/\3\2\1/'
sed模式标志
你可以在最后一个分隔符后加上标志,这些标志可以指明当当有多个模式在一行中被匹配,应该如何替换,让我们来看看它们吧。
/g全局替换
大多数Unix工具是以每次读一行的方式对文件进行操作。sed在默认情况下也是这样的。如果你告诉它你想改变一个单词,它只会改变每行第一次出现的这个单词,你可能想改变行中的每个单词。比如,我想给一行的每个单词加上括号,如果用“[A-Za-z]”这种模式,将不会匹配“won’t”这种单词,我们用另一种模式,”[^ ]”,它会匹配除空格外的一切串。是的,这将会匹配任何串,因为””表示0个或多个。现在的Solaris sed版本见到这个模式会不太开心,它会产生如”Output line to long”的错误或是一直运行下去。我将它视为一个bug,并已经报告给Sun公司了,作为一个暂时的解决方法,你在使用“g”标志时必须避免匹配空串。一个变通的方法是“[^ ][^ ]”,下面的命令会将第一个单词加上括号。
sed 's/[^ ]*/(&)/' <old >new
如果你想能每一个单词进行改动,你可以用下面的变通方法:
sed 's/[^ ][^ ]*/(&)/g' <old > new
sed是递归的吗?
Sed只在原始的数据中匹配模式,也就是在读取输入的一行时,当一个模式匹配了,就修改的输出就产生了,然后再会去扫描这行的余下的内容。“s”命令不会再去扫描新产生的输出。也就是,你不用去担心下面的这种正则表达式:
sed 's/loop/loop the loop/g' <old >new
这不会产生死循环。如果执行第二个”s”命令,它会去修改第一个”s”命令的输出结果,我将在下面告诉如何执行多个命令。
/1, /2 等等 指明哪一个匹配
不带标志,只有第一个匹配会被改变。如果带上“g”标志,所有的匹配都会被改变。如果你想改变一个特定的模式,但它又不是这行的第一个匹配,你可以用“(”和“)”来标记每个模式,并且可以用“\1”表示第一个匹配的串,下面的例子是保留的第一个单词,删除第二个单词的命令:
sed 's/\([a-zA-Z]*\) \([a-zA-Z]*\) /\1 /' <old >new
呼。还有一个更容易的方法,你可以在替换命令后加一个数字表明你只想匹配某个特定的模式,比如:
sed 's/[a-zA-Z]* //2' <old >new
你也可以和“g” (global)标志结合使用,比如,如果你想只保留第一个单词,并把第二个,第三,等等都改为DELETED,使用/2g:
sed 's/[a-zA-Z]* /DELETED/2g' <old >new
不要把/2和\2的意义搞混了,/2用在命令的末尾,而\2用在替换部分。
注意在“*”字符后有一个空格,没有这个空格,sed会运行很长很长时间(注意,这个bug可能已经被修正了),这是因为数字标志和“g”标志有着相同的bug,你可以运行下面的命令:
sed 's/[^ ]*//2' <old >new
但这会把CPU消耗完,如果你在一台Unix系统上运行下面的例子,它会从密码文件中移除加密后的密码:
sed 's/[^:]*//2' </etc/passwd/ >/etc/password.new
但在我写这个例子时,它还不能正常工作。并且用[^:][:^]也不能解决这个问题,因为它不会匹配一个不存在的密码,所以它会删除第三列,也就是user ID!所以你必须用下面这种丑陋的括号来做。
sed 's/^\([^:]*\):[^:]:/\1::/2' </etc/passwd >/etc/password.new
你还应该在第一个模式后加一个“:”,这样它就不会匹配空了:
sed 's/[^:]*:/:/2' </etc/passwd >/etc/password.new
数字标志不一定非要是一位的,它可以是从1到512的任何值。如果你想在每行的80个字符后加一个冒号,你可写:
sed 's/./&:/80' <file >new
你也可以用蛮力解决:
sed 's/^................................................................................/&:/' <file >new
/p 打印
sed默认会打印每一行。如果它做了替换,就会打印新的文本。如果你使用“sed -n”,它就不会打印,默认,会打印所有新产生的行。当使用“-n”选项时,使用“p”标志会打印修改过的串。下面的例子执行的效果与grep相同。
sed -n 's/pattern/&/p' <file
但是一个更加简单的版本在这里
用/w filename 写入一个文件
在第三个分隔符后还可以跟另外一个标志,带上”w”标志可以指定一个接收修改后数据的文件。下面的例子是将所有以一个偶数开头且后面为空格的行写入even文件。
sed -n 's/^[0-9]*[02468 /&/w even' <file
在这个例子中,输出文件是不需要的,因为输出没有改变。在使用“w”标志时,你必须确信你在“w”和文件名有且只有一个空格。你可以在一个sed命令中打开10个文件,这样你可以将数据流拆分到不同的文件中,使用上例和下面将介绍的多个替换命令,你可以将一个文件根据最后一个数字拆分到十个文件。你还可以用这个方法将错误日志或调试信息写到一个特定文件中。
/I 忽略大小写
GNU添加了另一个模式标志-/I
这个标志让模式匹配忽略大小写,下面这个会匹配abc,aBc,ABC,AbC等等:
sed -n '/abc/I p' <old >new
注意在‘/I’和‘p’(print)命令中间的空格强调‘p’不是模式匹配处理的修正符,而是一个在模式匹配之后执行的命令。
组合替换命令
你可以将标志组合起来使用,当然”w”必须是最后一个标志,比如下面的例子:
sed -n 's/a/A/2pw /tmp/file' <old >new
接下来我将介绍sed的选项和调用sed的不同方法。
sed的参数和调用
先前,我只使用了一个替换命令,如果你想做两次替换,并且不想去读sed手册,你可以用管道将多个sed连起来:
sed 's/BEGIN/begin/' <old | sed 's/END/end/' >new
这种写法使用了两个处理过程,但是sed高手在一个处理过程可以完成时,绝不会用两个。
使用-e选项来执行多个命令
将多个命令结合起来的一种方法是在每个命令前加上-e选项:
sed -e 's/a/A/' -e 's/b/B/' <old >new
在先前的例子中并不需要“-e”选项,因为sed知道至少会有一个命令,如果你给sed一个参数,它必须是一个命令,并且sed处理来自标准输入的数据。 长参数版本是:
sed --expression='s/a/A/' --expression='s/b/B' <old >new
另见:TODO
命令行中的文件名
你可以在命令行中指定你想要的文件,如果在没有使用选项的情况下,有多于一个参数传递给sed,它必须是一个文件名,下面的例子是在三个文件中不以“#”开头的行数:
sed 's/^#.*//' f1 f2 f3 | grep -V '^$' | wc -l
让我们来分解以下,sed
替换命令会将以“#”开头的行替换为空行,grep
用于过滤空行,wc
计算剩余的行数,sed
还有别的命令可以不用grep
来完成这个功能,也可以用grep -c
来替换wc -l
。我过会再介绍如何复制grep
的功能。
当然你可以用“-e”选项来执行上面的例子:
sed -e 's/^#.*//' f1 f2 f3 | grep -v '^$' | wc -l
sed
还有两个选项没介绍。
sed –n:不打印
使用“-n”选项后,除非明确指明要打印,否则不打印任何内容。我在前在提到过“/p”标志可以让它又可以打印。让我再讲清楚点,命令:
sed 's/PATTERN/&/p' file
如果PATTERN没在文件中出现,这个命令表现的就和cat一样,即没有内容被改变。如果有PATTERN在文件中出现,那么每行被打印两次。加上”-n”选择,如下面的这个例子就如同grep命令:
sed -n 's/PATTERN/&/p' file
那它只打印包含PATTERN的行。 -n命令的长参数是:
sed --quiet 's/PATTERN/&/p' file
或者
sed --silent 's/PATTERN/&/p' file
使用'sed /pattern/'
sed
有通过在命令前面指定地址来指明要检查或者修改的行。现在我就只描述最简单的版本-/PATTERN/地址。当加上之后,之后匹配模式的行上才会执行地址后面的命令。简单来说,当用了/p标志之后,被匹配的行会被打印两次。
sed '/PATTERN/P' file
当然,PATTERN可以是任意的正则表达式。
请注意如果你不包括一个命令,比如说“p”来打印,你会得到一个错误。当我输入
echo abc | sed '/a/'
我得到错误:
sed: -e expression #, char 3: missing command
而且,虽然不必要,但是我还是推荐你在模式和命令中间放一个空格。这可以帮你区别修改模式匹配的标记和模式匹配之后执行的命令。因此我推荐这样:
sed '/PATTERN/ p' file
用'sed -n /pattern/p'来实现grep的功能
如果你想要实现grep的功能,把-n选项和/p打印标志结合起来:
sed -n '/PATTERN/p' file
sed –f scriptname
如果你有大量的sed命令,你可以把它们放到一个文件中,再用下面命令执行:
sed -f sedscript <old >new
其中sedscript类似于下面的例子:
# sed comment - This script changes lower case vowels to upper case
s/a/A/g
s/e/E/g
s/i/I/g
s/o/O/g
s/u/U/g
当有多个命令在一个文件中,每个命令必须是单独的一行。
长参数版本是:
sed --file=sedscript <old >new
另见TODO
shell脚本中的sed
如果你有许多命令,它们不适合写在一行中,你可以用反斜杠将它们分开:
sed -e 's/a/A/g' \
-e 's/e/E/g' \
-e 's/i/I/g' \
-e 's/o/O/g' \
-e 's/u/U/g' <old >new
在C Shell中引用多行sed
你可以在C Shell中写入一个大的多行sed脚本,但你必须告诉C shell sed引用跨了几行,这可以通过在每行后加上反斜杠:
#!/bin/csh -f
sed 's/a/A/g \
s/e/E/g \
s/i/I/g \
s/o/O/g \
s/u/U/g' <old >new
在Bourne shell中引用多行sed
在Bourne shell中不用加上斜杠,就可以引用多行:
#!/bin/sh
sed '
s/a/A/g
s/e/E/g
s/i/I/g
s/o/O/g
s/u/U/g' <old >new
sed -V
-V选项将会打印你正在使用的sed的版本。长参数版本是:
sed --version
sed -h
-h选项将会打印sed命令的摘要。长参数版本是:
sed --help
sed脚本
另一种执行sed的方法是用脚本,创建一个文件内容如下:
#!/bin/sed -f
s/a/A/g
s/e/E/g
s/i/I/g
s/o/O/g
s/u/U/g
点击这里来获取文件:TODO
如果这个脚本是存在名为“CapVowel”的文件中,而且它是可以执行的,你就可以用这个简单的命令来执行:
CapVowel <old >new
注释
Sed注释是第一个非空白字符是“#”的行,在许多系统上,sed只能一个注释,并且它必须是脚本的第一行。在Sun上(在1988年我写这篇文章时),你可以在脚本的任何地方写注释。现代的版本的sed支持这种写法。如果第一行以“#n”开头,那么它和使用“-n”选项的效果相同:默认关闭打印。但这种写法在sed脚本行不通,因为在sed脚本中第一行必须以“#!/bin/sed -f”开头,因为我认为“#!/bin/sed -nf”会产生一个错误。在我2008年尝试这种写法的时候还是不能工作的,因为sed会认为“n”是文件名,但是:
#!/bin/sed -nf
可用
将参数传递到sed脚本
如果你还记得Unix引用机制教程,将一个词传递到shell脚本,再调用sed是很简单的。复习一下,你可以用单引号可以打开引用或关闭引用。下面是一个很简单的脚本来模拟grep:
#!/bin/sh
sed -n 's/'$1'/&/p'
但是这个脚本有一个问题,如果你将空格作为参数,这个脚本会引起一个语法错误,一个更好的版本可以防止这种情况的发生。
#!/bin/sh
sed -n 's/'"$1"'/&/p'
将上面的内容保存到sedgrep文件中,你可以键入:
sedgrep '[A-Z][A-Z]' <file
这可以让sed表现的和grep命令一样。
在脚本中使用sed之here document
你可以用sed提示用户参数输入,然后用这些参数创建一个文件将这些参数填进去。你可创建一个有些待替换的值的文件,然后用sed来改变这些值。一个比较简单的方法是用“here is”是用here document,它将shell脚本当作标准输入使用。
#!/bin/sh
echo -n 'what is the value? '
read value
sed 's/XYZ/'$value'/' <<EOF
The value is XYZ
EOF
当执行之后,脚本会提示:
What is the value?
如果你输入“123”,下一行就是:
The value is 123
我承认这是一个牵强的例子,here document可以不用sed来做,下面的例子完成的是相同的任务。
#!/bin/sh
echo -n 'what is the value?'
read value
cat <<EOF
The value is $value
EOF
但是结合sed在here document中可以完成一些复杂的操作,注意:
sed 's/XYZ/'$value'/' <<EOF
如果用户输入一个带有空格的答案,比如“a b c”,就会出语法错误。更好的格式是用双引号把值引起来:
echo -n 'what is the value? '
read value
sed 's/XYZ/'"$value"'/' <<EOF
The value is XYZ
EOF
我在引号教程中有介绍。
多个命令和执行顺序
随着我们的学习,sed命令将变的更加复杂,并且真正的执行顺序会变得让人糊涂。其实它是很简单的,先读取一行,按用户指定的顺序来在这行上面执行命令。在替换之后,下一个命令将会在相同的行上操作,但这一行是上一命令修改过的行。无论你何时有疑问,最好的方法就是创建一个小例子来试一个。当你写一个复杂的命令不能工作时,就设法使它简单。如果你不能使一个复杂的脚本工作,就将它拆成两个小的脚本,然后用管道将两个脚本连起来。
文本的位置和区间
你已经学习了一个命令了,但你已经领教了sed的强大,然而,它现在做的只是grep加上替换。即是每个替换命令只关心一行,而不管附近几行。如果可以有限制在特定的一些行内操作,那该多有用呀。一些有用的限制可以是:
- 通过行号指定某行。
- 通过行号指定行的区间。
- 所有包含某一模式的行。
- 从文件开始到某一正则的所有行。
- 从某一正则到文件结尾的所有行。
- 在两个正则之间的所有行。
Sed不但可以完成上述功能,而且可以做得更好,在sed中的每个命令都可以指定它的操作位置,地址、区间或像上面所列的限制,命令操作范围的限制是下面的格式:
restriction command