Sed 介绍和教程
作者:Bruce Barnett
译者:Koala++
原文地址:http://www.grymoire.com/Unix/Sed.html
注:译者不懂sed
Sed 介绍
如果你想写一个程序对一个文件做一些改动,那就sed就是你应该使用的工具。这篇文章就是教你如何使用这个自动修改文件的特殊编辑器。
在Unix工具中有一些程序是真正的重型武器。这些程序在简单的应该中很容易使用,但是还有大量的命令可以进行复杂的操作。不要因为惧怕它强大能力但复杂指令,而使你放弃使用它的简单特性,这章,如果其它章一些,以介绍一些简单的概念为开始,然后介绍一些高级的话题。一个关于注释的注意事项,当我开始写这篇文章时,大多数版本的sed不允许你将注释放到脚本里,以’#’字符开始的行为注释。新的版本的sed也许会支持行尾注释。
关于Sed的可怕事实
Sed是一个终极的流编辑器。如果你感觉流编辑器听起来很奇怪,就想象一下一个通过管道的水流。是的,如果这个水流参管道里,你不可能看到水流。这就是我拿水流比喻的原因。
不管怎么说,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”这个字符串。
替换命令有四个组成部分:
s 替换命令
/http://www.cnblogs.com/ 分隔符
day 正则表达式模式,查找模式
night 替换字符串
查找模式在左边,替换字符串在右边。
我们已经学习了引用和正则表达式。那是学习替换命令90%的工作量。换句话说,你已经知道90%最常用的sed使用方法,但仍有一些未来的sed高手需要了解的内容(你已经看了第1节,还有63节就看完了。),噢。……,如果你没看完,那么你就收藏这个页面吧。
用&作为匹配的串
有时你想查找一个模式,然后加上几个字符,比如在匹配的串前后加上括号。如果你是找一个确定的字符串,还是比较简单的,如下即可:
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”来指名,这就是下节的内容了。
用\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/’
“\1”并不需要一定出现在替换串中(右部),它你也可以在查找的模式中(左部)。如果你想删除重复的单词,你可以用:
sed ‘s/\([a-z]*\) \1/\1/’
注意你最多使用9个值,从”\1”到”\9”
替换标志
你可以在最后一个分隔符后加上标志,这些标志可以指明当当有多个模式在一行中被匹配,应该如何替换,让我们来看看它们吧。
/g 全局替换
大多数Unix工具是以每次读一行的方式对文件进行操作,sed,默认也是以行为单位。如果你告诉它你想改变一个单词,它只会改变每行第一次出现的这个单词,你可能想改变行中的每个单词。比如,我想给一行的每个单词加上括号,如果用[A-Za-z]*这种模式,将不会匹配”won’t”这种单词,我们用另一种模式,”[^ ]”,它会匹配除空格外的一切串。是的,这将会匹配任何串,因为”*”表示0个或多个。现在的sed版本见到这个模式会不太开心,它会产生如”Output line to long”的错误或是一直运行。我将它视为一个bug,并已经报告给Sun公司了,作为一个暂时的解决方法,你在使用”g”标志时必须避免匹配空串。一个变通的方法是”[^ ][^ ]*”,下面的命令会将第一个单词加上括号。
如果你想能每一个单词进行改动,你可以用下面的变通方法:
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::/’ < /etc/passwd > /etc/password.new
你还应该在第一个模式后加一个”:”,这样它就不会匹配空了:
sed ‘s/[^:]*:/:/2’ < /etc/passwd > /etc/passwd.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个文件,这样你可以将数据流拆分到不同的文件中,使用上例和下面将介绍的多个替换命令,你可以将一个文件根据最后一个数字拆分到十个文件。你还可以用这个方法将错误日志或调试信息写到一个特定文件中。
将替换标志结合
你可以将标志结合起来使用,当然”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处理来自标准输入的数据。
命令行中的文件名(s)
你可以在命令行中指定你想要的文个,如果在没有使用选项的情况下,有多于一个参数传递给sed,它必须是一个文件名,下面的例子是在三个文件中数不以”#”开头的行数。
sed ‘s/^#.*//’ f1 f2 f3 | grep –v ‘^$’ | wc -l
sed替换命令会将以”#”开头的行替换为空行,grep用于过滤空行,wc计算剩余的行数,sed还有别的命令可以不用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的行。
sed –f scriptname
如果你有大量的sed命令,你可以把它们放到一个文件中,再用下面命令执行:
sed –f sedcript < 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
当有多个命令在一个文件中,每个命令必须是单独的一行。
在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 脚本
另一个执行sed的方法是用脚本,创建一个脚本名为CapVowel,并给它执行权限,内容如下:
#!/bin/sed -f
s/a/A/g
s/e/E/g
s/i/I/g
s/o/O/g
s/u/U/g
你可以用下面的命令执行:
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脚本
如果你还记得my tutorial on the Unix quoting mechanism,将一个词传递到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/XXX/'$value'/' <<EOF
The value is XXX
EOF
当执行之后,脚本会提示:
What is the value?
如果你输入”123”,下一行就是:
The valu 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/XXX/’$vallue’/’ << EOF
会给出语法错误,如果用户输入空格,下面的写法更好:
sed ‘s/XXX/’”$value”’/’ << EOF
多个命令和执行顺序
随着我们的学习,sed命令将变的更加复杂,并且真正的执行顺序会变得让人糊涂。其实它是很简单的,先读取一行,每个命令按用户指定的顺序执行来对输入的行操作。在替换之后,下一个命令将会在相同的行上操作,但这一行是上一命令修改过的行。无论你何时有疑问,最好的方法就是创建一个小例子来试一个。当你写一个复杂的命令不能工作时,就设法使它简单。如果你不能使一个复杂的脚本工作,就将它拆成两个小的脚本,然后用管道将两个脚本连起来。
文本的位置和区间
你已经学习了一个命令了,但你已经领教了sed的强大,然而,它现在做的只是grep加上替换。即是每个替换命令只关心一行,而不管附近几行。如果可以有限制在特定的一些行内操作,那该多有用呀。一些有用的限制可以是:
- 通过行号指定某行。
- 通过行号指过行的区间。
- 所有包含某一模式的行。
- 从文件开始到某一正则的所有行。
- 从某一正则到文件结尾的所有行。
- 在两个正则之间的所有行。
Sed不但可以完成上述功能,而且可以做得更好,在sed中的每个命令都可以指定它的操作位置,区间或像上面所列的功能,命令操作范围的限制是下面的格式:
restriction command
用行号限定
最简单的限制是用行号,如果你想删除第3行的第一个数字,只需要在命令前加3。
sed ‘3 s/[0-9][0-9]*//’ < file > new
模式
许多Unix工具,如vi和more用斜杠来搜索一个正则表达式。Sed遵循这一传统,你可以用一斜杠来结束正则表达式。下面的例子是删除以”#”开头的所有行的第一个数字。
sed ‘/^#/ s/[0-9][0-9]*//’
我在”/expression/”之后写了一个空格是为了方便阅读,它并不是必要的,但是不写空格,这个命令就比较难看懂,sed还提供了一些别的指定正则表达式的方法,但这个我稍后再讲,如果一个表达式以一个反斜杠开头,则下一个字符是一个分隔符,用逗号代替斜杠的写法如下:
sed ‘\,^#, s/[0-9][0-9]*//’
这种写法最大的好处是搜索斜杠,假设你想搜索”/usr/local/bin”,替换为”/common/all/bin”,你可以用反斜杠来转义斜杠:
sed '/\/usr\/local\/bin/ s/\/usr\/local/\/common\/all/'
如果你用下划线而不是斜杠来作为分隔符会更好理解一些,下面的例子在搜索命令和替换命令中都使用下划线做为分隔符:
sed '\_/usr/local/bin_ s_/usr/local_/common/all_'
这就解释了为什么sed脚本有隐晦的名声。我可以举出来一个恶心的例子:搜索所有以”g”开头的行,并将行中每个”g”替换为”s”:
sed '/^g/s/g/s/g'
用一个空格将命令分开,并在替换命令中用下划线分隔会更容易理解:
sed '/^g/ s_g_s_g'
嗯...,我收回我说的,它还是不好懂。这就是一个教训,如果在SunOS下写sed脚本,加上注释,你可以在别的操作系统上运行时删除它,你现在应该知道删除注释的方法了吧?注释是一件好事,当你写脚本时,你也许会完全理解脚本的含义,但过六个月之后,没有注释,它就是一堆乱码。
用行号限定范围
你可以用逗号分隔的行号指定范围,如果要限制替换在前100行,你可以用:
sed '1,100 s/A/a/'
如果你准确地知道一个文件中有多少行,你可以显式地说明处理剩余的行,如果是这样,假设你用wc命令知道这个文件有532行:
sed '101,532 s/A/a/'
一个更简单的办法是用一个特殊的字符”$”,它表示文件的最后一行。
sed '101,$ s/A/a/'
“$”符号表示最后也是一个传统,在cat –e vi和ed工具中都是这样,用cat –e就将会多个文件行号累记,即:
sed '200,300 s/A/a/' f1 f2 f3 >new
等价于:
cat f1 f2 f3 | sed '200,300 s/A/a/' >new
用模式限定范围
你可以用两个正则表达式限定范围,假设以”#”开头的是注释,你想找一个关键词,你移除所有注释直到你看到第二个关键词,假设两个关键词是”start”和”stop”:
sed '/start/,/stop/ s/#.*//'
第一个模式告诉sed开始对每行进行替换操作,第二个模式告诉sed停止对剩下的行进行操作。如果”start”和”stop”这两个模出现了两次,那么替换也会进行两次,如果”stop”模式没有匹配,那么替换就会对余下的所有行进行匹配。
你应该了解如果”start”模式如果找到了,替换就在包含”start”的行进行替换,它就像打开了一个面向行的开关,然后下一行被读入,进行替换,当然如果它包含”stop”,这个开关就关闭了,开头是面向行的,它并不是面向词的。
你可以结合行号和正则表达式来限定,下面的例子是从开始移除注释直到它找到”start”关键词:
sed -e '1,/start/ s/#.*//'
下面的例子展示了移除两个关键词以外的注释:
sed -e '1,/start/ s/#.*//' -e '/stop/,$ s/#.*//'
最后一个例子有一个重叠的区间”/start/,/stop/”,因为两个范围都在包含关键词的行上操作,我将会告诉你一行完成这个功能的方法。
在我继续介绍更多命令之前,我应该告诉你一些命令不能对一个范围内的行操作,在我介绍这些命令时,我会告诉你,在下节中我将会讲三个不能对一个范围的行进行操作的命令。
用d删除
使用范围限定容易让人糊涂,所以你在写一个新的脚本时,你应该先试试。一个删除符合限定行的命令是:”d”。如果你想看文件的前10行,你可以用:
sed '11,$ d' <file
它与head命令的功能很像。如果你想删除一个信件的头部信息,也就是删除从文件开始直到一个空行的所有内容,可以用:
sed '1,/^$/ d' <file
你还可以模拟tail命令,假设你知道文件的长度,wc命令可以统计行数,expr可以对文件行数减10.一个Bourne Shell想查看文件最后10行,可许可以这样写:
#!/bin/sh
#print last 10 lines of file
# First argument is the filename
lines=`wc -l $1 | awk '{print $1}' `
start=`expr $lines - 10`
sed "1,$start d" $1
也可以用正则表达式来标记删除操作的开始和结束位置,或者也可以只用一个正则表达式,删除所有以”#”开头的行很容易:
sed '/^#/ d'
删除注释和空行需要两个命令,第一个删除从”#”到行尾的字符,第二个命令删除所有的空行:
sed -e 's/#.*//' -e '/^$/ d'
第三个例子再增加一个功能:删除所有行前面的空格和tab:
sed -e 's/#.*//' -e 's/[ ^I]*$//' -e '/^$/ d'
“^I”字符是一个CTRL-I或是tab,你可以直接输入tab。注意上面操作的顺序,这样写是出于一个合理的理由,注释可能在前面有空格的一行的中间出现,所以注释第一个个被删除,这样就只剩空格在前面了,第二个命令就可把这样空格删除,最后一个命令会把空行删除了。
这展示了sed用模式空间来操作一行,sed真正的操作是:
1. 将输入行复制到模式空间。
使用第一个sed命令。
2. 如果这个行在限定范围内,sed命令在这行上操作。
3. 用下一个命令继续在这个模式空间上操作。
4. 当最后一个操作完成时,输出模式空间,然后读取下一行。
用p打印
另一个有用的命令是打印命令”p”,如果sed不以”-n”开头,那么”p”会重复输入,命令:
sed ‘p’
会重复会一行,如果你想每行打印两次,用:
sed '/^$/ p'
加上”-n”选项会关闭打印,除非你明确要求要打印。另一个模拟head功能的方法是只打印你想要的行,下面的例子打印前10行。
sed -n '1,10 p' <file
sed也可以完成grep的功能,打印匹配正则表达式的行:
sed -n '/match/ p'
它等价于:
grep match
用!对限定取反
有时你想对匹配一个正则表达式以外的行进某个操作,或是限定范围之外的行。”!”字符,在Unix系统中意义为”非”,对位置限定取反。你应该记得:
sed -n '/match/ p'
它的功能和grep相似,grep命令中的-v操作表示不包括某一模式,sed可以用下面的方式做到这一点:
sed -n '/match/ !p' </tmp/b
d,p和!之间的关系
正如你所注意到的一样,sed可以用多种方式来解决同一问题,这是因为print和delete有着相反的功能,似乎”!p”和”d”的功能相似,且”!d”和”p”功能相似,为这测试这一点,我建了一个20行的文件,尝试每一种组合,下表就是执行的结果,以来展示它们的差异:
d,p和!之间的关系
Sed Range Command Results
--------------------------------------------------------
sed -n 1,10 p Print first 10 lines
sed -n 11,$ !p Print first 10 lines
sed 1,10 !d Print first 10 lines
sed 11,$ d Print first 10 lines
--------------------------------------------------------
sed -n 1,10 !p Print last 10 lines
sed -n 11,$ p Print last 10 lines
sed 1,10 d Print last 10 lines
sed 11,$ !d Print last 10 lines
--------------------------------------------------------
sed -n 1,10 d Nothing printed
sed -n 1,10 !d Nothing printed
sed -n 11,$ d Nothing printed
sed -n 11,$ !d Nothing printed
--------------------------------------------------------
sed 1,10 p Print first 10 lines twice,
Then next 10 lines once
sed 11,$ !p Print first 10 lines twice,
Then last 10 lines once
--------------------------------------------------------
sed 1,10 !p Print first 10 lines once,
Then last 10 lines twice
sed 11,$ p Print first 10 lines once,
then last 10 lines twice
这个表表明下面的命令是相似的:
sed -n '1,10 p'
sed -n '11,$ !p'
sed '1,10 !d'
sed '11,$ d'
它还显示了”!”命令对位置区间取反了,命令在取反后的空间上操作。
q退出命令
还有一个简单的命令可以限定操作在一个指定范围内,它就是”q”命令,第三种模拟head命令的方法是:
sed '11 q'
当执行到第11行时,就会退出,它个命令在你想到达某种条件后就退出编辑时特别有用。
“q”命令是一个不接受范围限定的命令,很显然下面的命令:
sed '1,10 q'
无法退出10次,相反:
sed '1 q'
或:
sed '10 q'
是正确的。
用{和}组合
大括号”{”和”}”在sed中用来组合命令。
这个用法不太难,但是有一个难点,因为sed中的每个命令必须独起一行,下面的大括号和内嵌的sed命令必须在不同的行。
在前面,我向你介绍了如何移除了以”#”的注释,如果你想限定删除所有在”begin”和”end”关键词之间的行,你可以用:
#!/bin/sh
# This is a Bourne shell script that removes #-type comments
# between 'begin' and 'end' words.
sed -n '
/begin/,/end/ {
s/#.*//
s/[ ^I]*$//
/^$/ d
p
}
'
大括号是可以嵌套的,这也可以使你结合位置范围,你可以进行上面相同的操作,但只限操作前100行:
#!/bin/sh
# This is a Bourne shell script that removes #-type comments
# between 'begin' and 'end' words.
sed -n '
1,100 {
/begin/,/end/ {
s/#.*//
s/[ ^I]*$//
/^$/ d
p
}
}
'
你可以在一个大括号前写”!”,它可以反转地址范围,下面的例子是移除在begin和end之间范围外的所有注释:
#!/bin/sh
sed '
/begin/,/end/ !{
s/#.*//
s/[ ^I]*$//
/^$/ d
p
}
'
用’w’命令写一个文件
你也许还记得替换命令可以写入一个文件,这是另一个例子,它是将偶数开头(后面有一个空格)的行写入文件:
sed -n 's/^[0-9]*[02468] /&/w even' <file
我在命令的替换部分用&,这样行就不会被改。一个更简单的例子是用”w”命令,它和上面的例子有相同的功能:
sed -n '/^[0-9]*[02468]/ w even' <file
切记:命令后只能有一个空格,剩余的部分将会被当作是文件名。”w”命令也有相同的局限:最多只能打开10个文件。
用’r’命令读一个文件
这也是一个读文件的命令,命令:
sed '$r end' <in>out
会将”end”文件追加到out尾部(位置”$”),下面的命令会在有”INCLUDE”单词的行后插入文件:
sed '/INCLUDE/ r file' <in >out
你可以用大括号来删除有”INCLUDE”的行:
#!/bin/sh
sed '/INCLUDE/ {
r file
d
}'
删除命令”d”和读文件命令”r”的顺序是很重要的,如果把它们的顺序颠倒是不能正常工作的。这是因为两个微妙的原因,第一是”r”命令将文件写入输出流,文件不是被插入模式空间,所以不能被任何命令修改。所以删除命令不影响从文件中读到的数据。
另一个微妙之处在”d”命令删除模式空间中的当前数据,一旦数据被删除,显然不应该再对它做任何操作了,所以大括号中的”d”命令会使后面的命令都不会被执行。在下面的例子中,替换命令不会被执行:
#!/bin/sh
# this example is WRONG
sed -e '1 {
d
s/.*//
}'
下面是一个C预处理程序的粗糙版本,文件以一个预先定义的名字包含,这样就能让sed让一个变量(比如”\1”)很容易替换一个文件名,啊,sed没有这个功能,你在运行时使用sed命令来突破这个局限,或使用shell引号传递sed脚本参数,假设你想创建一个命令要包话一个如cpp的文件,但文件名即是这个脚本的参数,下面的脚本就是这样一个例子:
% include 'sys/param.h' <file.c >file.c.new
那么shell脚本应该这样写:
#!/bin/sh
# watch out for a '/' in the parameter
# use alternate search delimiter
sed -e '\_#INCLUDE <'"$1"'>_{
r '"$1"'
d
}'
让我再详细一点,如果你有一个文件包含:
Test first file
#INCLUDE <file1>
Test second file
#INCLUDE <file2>
你可以如下使用命令:
sed_include1.sh file1<input|sed_include1.sh file2
以来包含指定的文件。
sunOS和#注释命令
当我们更深入sed过程中,注释会使命令更容易理解,大多数sed版本只允许一行注释,但它必须是第一行,SunOS允许多行注释,并且这些注释不需要在开头,它可以写成下面这样:
#!/bin/sh
# watch out for a '/' in the parameter
# use alternate search delimiter
sed -e '\_#INCLUDE <'"$1"'>_{
# read the file
r '"$1"'
# delete any characters in the pattern space
# and read the next line in
d
}'
添加,修改,添加新行
Sed有三个命令可以添加新行到输出流,因为一个整行要添加,这个新行必须在单独的一行,这是必须的。如果你注释许unix工具,你会期待sed遵循相同的传统:用”\”表示一行的继续,这个语法像”r”和”w”命令一样,也是很严格的。
用’a’命令追加一行
“a”命令用于在一个模式范围之后追加一行,下面的例子可以在每个有”WORD”的行后添加一行:
#!/bin/sh
sed '
/WORD/ a\
Add this line after every line with WORD
'
如果你高兴,你也可以在shell中把它写成两行:
#!/bin/sh
sed '/WORD/ a\
Add this line after every line with WORD'
我更喜欢第一行,因为它添加一个新命令时更容易,而且它的意图也更清晰,注意在”\”之后不能有空格。
用i插入一行
你可以在一个模式前用i插入一个命令:
#!/bin/sh
sed '
/WORD/ i\
Add this line before every line with WORD
'
用’c’改变一行
你可以用一个新行改变当前行。
#!/bin/sh
sed '
/WORD/ c\
Replace the current line with the line
'
一个”a”命令跟在”d”命令之后是不会被执行的,原因已经说过了,因为”d”命令会中止以后的所有操作,你可以用大括号将这个操作结合起来:
#!/bin/sh
sed '
/WORD/ {
i\
Add this line before
a\
Add this line after
c\
Change the line to this one
}'
在sed脚本中开头的tab和空格
Sed会忽略所有命令开头的tabs和空格,但是空格开头的行如果后面是”a”,”c” 或是”i”命令,它们可能被忽略也可能不被忽略,在SunOS上,两种都有,Berkeley(和linux)方式的sed是在/usr/bin下,AT&T版本(System V)是在/usr/5bin/。
更详细一点,/usr/bin/sed命令保留空格,而/usr/5bin/sed会忽略开头的空格,如果你想保留开头的空格,而不管是什么版本的sed,你可以在第一个字符前加上”\”字符:
#!/bin/sh
sed '
a\
\ This line starts with a tab
'
添加多行
上面三种命令都允许你一次添加多行,只需要在每行后面加上一个”\”:
#!/bin/sh
sed '
/WORD/ a\
Add this line\
This line\
And this line
'
添加行和模式空间
我前面已经提到过模工空间,大多数命令在模式空上操作,其后的命令在上一次改动的结果上操作。前面三个命令,比如说读文件命令,添加新行到输出流去,这个过程跳过了模式空间。
位置范围和上面的命令
你也许还记得我在前面警告过你一些命令可以处理多行,而有一些命令则不可以。更准确地说:命令:”a”,”i”,”r”和”q”不可以接受一个范围,比如”1,100”或是”/begin/,/end/”。文档上说read命令可以接受一个范围,但我在尝试的时候却出错了,”c”命令,即修改命令可以接受一个范围,并且允许你将多行改为一行。
#!/bin/sh
sed '
/begin/,/end/ c\
***DELETED***
'
你也可以如下例一般使用大括号,下例是对每行进行操作。
#!/bin/sh
# add a blank line after every line
sed '1,$ {
a\
}'
多行模式
多数Unix工具都是面向行的,正则表达式也是向面行的。搜索跨多行的模式不是一件容易的事(提示,它会非常短)。
Sed读取文本中的一行,执行的命令可能会修改这一行,还可能会输出修改的内容,sed脚本的主循环可能看起来是样:
- 从输入文件读取一行,将它放入模式空间。如果当前文件到达文件结束处,还有其它文件要读,将当前文件关闭,将下一个文件打开,将新的文件的第一行读入模式空间。
- 文件行加1。打开一个新文件不会重置文件行数。
- 检查每一个sed命令,如果在命令前有一个限制,并且在模式空间的当前行符合这一限制,命令就会被执行,一些命令,如”n”或”d”会导致sed返回第1步,”q”命令会导致sed中止,否则下一条命令继续执行。
- 在所有命令被检查执行后,除非使用了”-n”参数,sed就将模式空间输出。
在命令前的限制会决定命令是否会被执行,如果限制是一个模式,并且命令是删除命令,下面的例子就是删除所有包含模式的行:
/PATTERN/ d
如果限制是一对行号,那么删除操作会对这个开区间内所有行进行删除。
10,20 d
如果限制是一对模式,其实有一个变量来标记这对模式,如果第一个模式找到了,如果这个变量为false,第一个模式找到时,将它置为true,如果变量为true,命令就会执行,如果变量为true,当找到后面的一个模式,那么这个变量就会被置为false。
/begin/,/end/ d
你要仔细地看看我前面说的话,我的措辞是有用意的,它会包含一些不常见的情况,如:
# what happens if the second number
# is less than the first number?
sed -n '20,1 p' file
和
# generate a 10 line file with line numbers
# and see what happens when two patterns overlap
yes | head -10 | cat -n | \
sed -n -e '/1/,/7/ p' -e '/5/,/9/ p'
天啊,这是什么逻辑。下面是一个新的检查,这次以一个表格的方式呈现,假设输入文件包含以下几行:
AB
CD
EF
GH
IJ
当sed开始工作后,第一行被放入模式空间,下一行是”CD”,”n”,”d”和”p”命令的总结如下:
+----------------+---------+------------------------------------------+
|Pattern Next | Command | Output New Pattern New Next |
|Space Input | | Space Input |
+----------------+---------+------------------------------------------+
|AB CD | n | <default> CD EF |
|AB CD | d | - CD EF |
|AB CD | p | AB CD EF |
+----------------+---------+------------------------------------------+
“n”命令可能也可能不会产生输出这取决于是否使用”-n”选项。
用=打印行号
“=”命令将当前行号输出到标准输出,其中一个作用就是找到包含某模式的行号:
# add line numbers first,
# then use grep,
# then just print the number
cat -n file | grep 'PATTERN' | awk '{print $1}'
用sed的做法是:
sed -n '/PATTERN/ =' file
我可以如下找到所有文件的行号:
#!/bin/sh
lines=`wc -l file | awk '{print $1}' `
使用sed可以更简单:
#!/bin/sh
lines=`sed -n '$=' file `
“=”只接收一个位置,所以如果你想打印一个范围的行号,你必须用大括号:
#!/bin/sh
# Just print the line numbers
sed -n '/begin/,/end/ {
=
d
}' file
因为”=”命令只打印到标准输出,如果你不能在同行的模式上输出它,你需要编辑多行模式来完成这个功能。
用y变换
如果你想将一个单词从小写变到大小,你可以写26个字符的替换,将”a”转换到”A”,等等。Sed有可以如tr程序的命令,它是”y”命令。比如,要将”a”到”f”变为大写,可以用:
sed 'y/abcdef/ABCDEF/' file
当然我可以举一个将26个字母转为大写的例子,但那样比较占空间。
如果你想将包含有16进制数字(比如,0x1aff)转为大写(0x1AFF),你可以用:
sed '/0x[0-9a-zA-Z]*/ y/abcdef/ABCDEF' file
这种方法只有在文件中只有一个数字的时候才有效,如果你想改变行中第二个单词为大写,你就没辙了,除非你用多行编辑。
用”l”打印控制字符
“l”命令可以打印当前模式空间,所以用它来调试sed是很有效的。而且它可将一个不可见字符转为可打印字符,转换规则是”\”加上字符八进制值。在探求sed的微妙的问题时,我感觉打印当前模式空间很有用。
在多行上工作
在多行模式下有三个新命令:”N”,”D”和”P”。我将介绍它们与”n”,”d”和”p”单行命令的关系。
“n”命令会打印当前模式空间(除非使用了-n选项),清空当前模式空间,从输入中读取下一行。”N”命令不打印不当模式空间,也不清空模式当间,它读取下一行,并将新行的字符追加到模式空间。
“d”命令会删除当前模式空间,并读取下一行,再将新行放入模式空间,并放弃当前操作,然后开始sed的第一个命令,即开始一次新的循环。”D”命令删除模式空间中的第一部分,直到新行的字符,而保留模式空间其余的部分。它像”d”一样,放弃当前操作并开始一个新的循环,但是它不会打印当前模式空间,你在前一步打印它,如果”D”命令在一个大括号里与其它命令一起执行,在”D”之后的命令会被忽略,然后另一组sed命令会被执行,除非模式空间已经空了。如果真是这样,那么循环会重新开始。
“p”命令会打印整个模式空间,”P”命令只打印模式空间的第一部分,直接新行的字符。
一些例子也许会说明只用”N”命令不是很有用,下例:
sed -e 'N'
它不会改变输入流,它会将第一行和第二行结合,并打印它们,将第三行和第四行结合,打印它们,等等。它允许你使用一个新的”锚”字符,它匹配在模式空间中在多行中的字符,如果你想搜索一个以”#”结尾的行,并将它追加到另一行,你可以用:
#!/bin/sh
sed '
# look for a "#" at the end of the line
/#$/ {
# Found one - now read in the next line
N
# delete the "#" and the new line character,
s/#\n//
}' file
你可以用下面命令查找前一行包含”ONE”下一行包含”TWO”的两行,并将它打印出来:
#!/bin/sh
sed -n '
/ONE/ {
# found "ONE" - read in next line
N
# look for "TWO" on the second line
# and print if there.
/\n.*TWO/ p
}' file
下一个例子会删除所有在”ONE”和”TWO”之间的字符:
#!/bin/sh
sed '
/ONE/ {
# append a line
N
# search for TWO on the second line
/\n.*TWO/ {
# found it - now edit making one line
s/ONE.*\n.*TWO/ONE TWO/
}
}' file
你可以在连续两行找一个特定的模式,或是你可以在连续两行中找一个被行边界分开的两个词,下面的例子是查找两个词或是在同一行,或是一个在上一行的行尾另一个在下一行的行首,如果找到,删除第一个词。
#!/bin/sh
sed '
/ONE/ {
# append a line
N
# "ONE TWO" on same line
s/ONE TWO/TWO/
# "ONE
# TWO" on two consecutive lines
s/ONE\nTWO/TWO/
}' file
如果我们找到一行包含”ONE”并且下一行含有”TWO”,我们用”D”命令删除第一行。
#!/bin/sh
sed '
/ONE/ {
# append a line
N
# if TWO found, delete the first line
/\n.*TWO/ D
}' file
如果我们想打印第一行,而不是删除它,也不打印第二行,可以用将上例的”D”命令换成”P”命令,并用”-n”参数。
#!/bin/sh
sed -n '
# by default - do not print anything
/ONE/ {
# append a line
N
# if TWO found, print the first line
/\n.*TWO/ P
}' file
将三个多行命令结合使用是很常见的,通常的顺序是”N”,”P”,最后”D”。下面的例子会删除在”ONE”和”TWO”之间的字符,如果它们没有相邻的两行。
#!/bin/sh
sed '
/ONE/ {
# append the next line
N
# look for "ONE" followed by "TWO"
/ONE.*TWO/ {
# delete everything between
s/ONE.*TWO/ONE TWO/
P
# then delete the first line
D
}
}' file
在前面我介绍过”=”命令,并用它向一个文件添加行号,你可以用sed的两次调用来实现(尽管可以用一次调用来实现,但这种是在下节介绍)。第一次调用是用sed输出文件行号,然后在下一行打印行内容,第二次调用会将两行合起来:
#!/bin/sh
sed '=' file | \
sed '{
N
s/\n/ /
}'
你可以把一行分成两行,再将它们合并,在下面的例子可,如果文件中有一个16进制数字后面跟了一个单词,你想将第一个单词变为大写,你可以用”y”命令,但你必须将行分为两行,改变其中之一,再将它们合起来,即,一行包含:
0x1fff table2
会被分成两行:
0x1fff
table2
并且第一行将被转换为大写,我用tr命令将空格将空间转为新行:
#!/bin/sh
tr ' ' '\012' file|
sed ' {
y/abcdef/ABCDEF/
N
s/\n/ /
}'
尽管用sed来写不太清晰,但它是能代替tr的,你可以在替换命令中嵌入一个新行,但你必须用一个反斜杠来转义。你必须在替换命令的左部用”\n”,而要在右部内嵌一个新行,这简直太不幸了,深叹一口气,下面是这个例子:
#!/bin/sh
sed '
s/ /\
/' | \
sed ' {
y/abcdef/ABCDEF/
N
s/\n/ /
}'
有时我添加一个特殊字符做为标记,,并在输入找这个特殊字符,当找到后,它用一个空格来标记,反斜杠是一个不错的选择,但它必须要用反斜杠来转义,这使得sed脚本不清晰,免得有人问白痴问题,下面的脚本是将一个空格转换成”\”和一个新行:
#!/bin/sh
sed 's/ /\\\
/' file
哈,这才对呢。或是用C Shell来真正把他搞晕!
#!/bin/csh -f
sed '\
s/ /\\\\
/' file
再来几个这样的例子,我想他再也不会问你问题了!我想我有点过火了。我在下图总结了我们讨论过的所有特性:
+----------------+---------+------------------------------------------+
|Pattern Next | Command | Output New Pattern New Next |
|Space Input | | Space Input |
+----------------+---------+------------------------------------------+
|AB CD | n | <default> CD EF |
|AB CD | N | - AB\nCD EF |
|AB CD | d | - CD EF |
|AB CD | D | - CD EF |
|AB CD | p | AB CD EF |
|AB CD | P | AB CD EF |
+----------------+---------+------------------------------------------+
|AB\nCD EF | n | <default> EF GH |
|AB\nCD EF | N | - AB\nCD\nEF GH |
|AB\nCD EF | d | - EF GH |
|AB\nCD EF | D | - CD EF |
|AB\nCD EF | p | AB\nCD AB\nCD EF |
|AB\nCD EF | P | AB AB\nCD EF |
+----------------+---------+------------------------------------------+
在sed脚本中使用换行
有时想在sed脚本中用换行符,嗯,这有一些微妙的地方,如果想查找换行符,可以用”\n”,下面是一个查找一个短语,在找到这个短语后,删除换行符的例子。
(echo a;echo x;echo y) | sed '/x$/ {
N
s:x\n:x:
}'
它会产生:
a
xy
但是,如果你想插入一个新行,不要用”\n”,而是直接输入新行:
(echo a;echo x;echo y) | sed 's:x:X\
:'
它会产生:
a
X
y
持有缓存
到现在为止,我们讨论了sed的三个概念:(1)输入流或是修改前的数据,(2)输出流或是修改后的数据,(3)模式空间,或是包含可被修改和可被发送到输出流的缓存。< xmlnamespace prefix ="o" ns ="urn:schemas-microsoft-com:office:office" />
但还有一个概念没有讲:持有缓存或保持空间,可以把它想成是一个空闲的模式缓存,它可以用来在模式空间复制或是记录数据为以后使用。有五个命令可以使用这个持有缓存。
用”x”交换
“x”命令用来交换(eXchange)模式空间和持有缓存,”x”命令单独使用没什么用,执行sed命令:
sed 'x'
它会在开始添加一个空白行,并删除最后一行,看起来它没有怎么改变输入流,但实际上sed命令改动了每一行。
持有缓存开始包含一个空白行,当”x”命令修改第一行,第一行被保存到了持有缓存中,且空白行占有了第一行的位置,第二个”x”命令交换第二行与持有缓存,这时持有缓存中是第一行内容。以此类推,每一行都与前一行交换。最后一行被放入保持空间,但是它没有机会交换,所以它在程序退出时还在持有缓存中,没有机会被打印。这表明了使用持有缓存时一定要小心,因为除非你指明要打印,否则它是不会输出的。
上下文grep的例子
持有缓存的其中一个作用是记录以前的行,这个作用的一个例子就是像grep一样,显示匹配一个模式的所有行,并且它可以显示这个模式的前一行和后一行,即如果第8行包含这个模式,这个例子可以打印第7,8,9行。
实现这一功能的其中一种方法是看一行中是否有这个模式,如果它没有包含这个模式,就将当前行放入持有缓存,如果有,就将持有缓存中的行打印,然后打印当前行,然后打印下一行。在每三行组成的组之间,打印连字符,脚本检查一个参数是否存大,如果不存在,打印错误。通过关闭单引号的机制将一个参数传入sed脚本,将”$< xmlnamespace prefix ="st1" ns ="urn:schemas-microsoft-com:office:smarttags" />1”插入脚本,再用单引号开始:
#!/bin/sh
# grep3 - prints out three lines around pattern
# if there is only one argument, exit
case $# in
1);;
*) echo "Usage: $0 pattern";exit;;
esac;
# I hope the argument doesn't contain a /
# if it does, sed will complain
# use sed -n to disable printing
# unless we ask for it
sed -n '
'/$1/' !{
#no match - put the current line in the hold buffer
x
# delete the old one, which is
# now in the pattern buffer
d
}
'/$1/' {
# a match - get last line
x
# print it
p
# get the original line back
x
# print it
p
# get the next line
n
# print it
p
# now add three dashes as a marker
a\
---
# now put this line into the hold buffer
x
}'
你可以用这个来显示在一个关键词附近的三行:
grep3 vt100 </etc/termcap
用h或H持有
“x”命令交换持有缓存和模式缓存,两个空间都发生改变,”h”命令将模式缓存拷贝到持有空间,而模式空间是不变的,下例使用持有命令完成上例的功能:
#!/bin/sh
# grep3 version b - another version using the hold commands
# if there is only one argument, exit
case $# in
1);;
*) echo "Usage: $0 pattern";exit;;
esac;
# again - I hope the argument doesn't contain a /
# use sed -n to disable printing
sed -n '
'/$1/' !{
# put the non-matching line in the hold buffer
h
}
'/$1/' {
# found a line that matches
# append it to the hold buffer
H
# the hold buffer contains 2 lines
# get the next line
n
# and add it to the hold buffer
H
# now print it back to the pattern space
x
# and print it.
p
# add the three hyphens as a marker
a\
---
}'
在持有buffer中保存多行
“H”命令允许你将几行结合起来放入持有缓存中,它的表现像”N”命令一样,将多行追加到缓存,在行之间有”\n”。你可以在持有缓存中保存多行,并在找到特定模式之后,将它们输出。
在下例中,一个文件用空格做为一行的第一个字符被视为是一个延续字符,文件/etc/termcap,/etc/printcap,makefile和邮件信息使用空格或tab来表示一个条目的继续。如果你想打印一个单词前的条目,你可以用下面的脚本,我使用”^I”表示tag字符:
#!/bin/sh
# print previous entry
sed -n '
/^[ ^I]/!{
# line does not start with a space or tab,
# does it have the pattern we are interested in?
'/$1/' {
# yes it does. print three dashes
i\
---
# get hold buffer, save current line
x
# now print what was in the hold buffer
p
# get the original line back
x
}
# store it in the hold buffer
h
}
# what about lines that start
# with a space or tab?
/^[ ^I]/ {
# append it to the hold buffer
H
}'
你还可以用”H”命令来扩展上下文grep,在这个例子中,程序会打印模式前的两行,而不是一行,限制两行的方法是用”s”命令来保持一个新行,并删除多余的行,我称它为grep4。
#!/bin/sh
# grep4: prints out 4 lines around pattern
# if there is only one argument, exit
case $# in
1);;
*) echo "Usage: $0 pattern";exit;;
esac;
sed -n '
'/$1/' !{
# does not match - add this line to the hold space
H
# bring it back into the pattern space
x
# Two lines would look like .*\n.*
# Three lines look like .*\n.*\n.*
# Delete extra lines - keep two
s/^.*\n\(.*\n.*\)$/\1/
# now put the two lines (at most) into
# the hold buffer again
x
}
'/$1/' {
# matches - append the current line
H
# get the next line
n
# append that one also
H
# bring it back, but keep the current line in
# the hold buffer. This is the line after the pattern,
# and we want to place it in hold in case the next line
# has the desired pattern
x
# print the 4 lines
p
# add the mark
a\
---
}'
你可以修改这个脚本,让它打印模式附近的任何行,如你所见,你必须记住什么是在持有空间,和什么是在模式空间。
使用g或G获得
除了交换持有缓存和模式空间,你可以用”g”命令将持有空间拷贝到模式空间,这会删除模式空间。如果你想追到模式空间,使用”G”命令。它会添加一个新行到模式空间,并将持有空间的内容拷贝到新行之后。
下例是一个”grep3”的另一个版本,它的功能和前一个一样,但实现不一样,它说明了sed有多种方法来解决一个问题。重要的是你理解你的问题,并记录下你的解法:
#!/bin/sh
# grep3 version c: use 'G' instead of H
# if there is only one argument, exit
case $# in
1);;
*) echo "Usage: $0 pattern";exit;;
esac;
# again - I hope the argument doesn't contain a /
sed -n '
'/$1/' !{
# put the non-matching line in the hold buffer
h
}
'/$1/' {
# found a line that matches
# add the next line to the pattern space
N
# exchange the previous line with the
# 2 in pattern space
x
# now add the two lines back
G
# and print it.
p
# add the three hyphens as a marker
a\
---
# remove first 2 lines
s/.*\n.*\n\(.*\)$/\1/
# and place in the hold buffer for next time
h
}'
“G”命令使得一行有两份拷贝比较容易,假设你想将第一个16进制数变为大写,而不想用我之前告诉你的脚本:
#!/bin/sh
# change the first hex number to upper case format
# uses sed twice
# used as a filter
# convert2uc <in >out
sed '
s/ /\
/' | \
sed ' {
y/abcdef/ABCDEF/
N
s/\n/ /
}'
下面是只需要一次sed调用的方法:
#!/bin/sh
# convert2uc version b
# change the first hex number to upper case format
# uses sed once
# used as a filter
# convert2uc <in >out
sed '
{
# remember the line
h
#change the current line to upper case
y/abcdef/ABCDEF/
# add the old line back
G
# Keep the first word of the first line,
# and second word of the second line
# with one humongeous regular expression
s/^\([^ ]*\) .*\n[^ ]* \(.*\)/\1 \2/
}'
Carl Henrik Lunde提供了一个更简单的方法:
#!/bin/sh
# convert2uc version b
# change the first hex number to upper case format
# uses sed once
# used as a filter
# convert2uc <in >out
sed '
{
# remember the line
h
#change the current line to upper case
y/abcdef/ABCDEF/
# add the old line back
G
# Keep the first word of the first line,
# and second word of the second line
# with one humongeous regular expression
s/ .* / / # delete all but the first and last word
}'
这个例子只将字母”a”到”f”变为大写,我选这个例子是为了在显示方便,你可很容易地将所有字母变为大写。
流程控制
如你所了解的一样,sed有它自己的编程语言。它是一个定制的简单语言。没有哪一个语言没有流程控制还能被称为完整。有三个sed命令可以做这件事,你可指定一个文本加一个冒号作为标签,标签后跟着命令,如果没有其它标签,分支会执行到脚本的最后。”t”命令用来测试条件,在我介绍”t”命令之前,我会给你看一个”b”命令的例子。
例子中记录段落,并且如果它包含某一模式(由参数指定),脚本就会被整段打印。
#!/bin/sh
sed -n '
# if an empty line, check the paragraph
/^$/ b para
# else add it to the hold buffer
H
# at end of file, check paragraph
$ b para
# now branch to end of script
b
# this is where a paragraph is checked for the pattern
:para
# return the entire paragraph
# into the pattern space
x
# look for the pattern, if there - print
/'$1'/ p
'
用”t”测试
你可以在一个模式找到后执行一个分支,你也许想在只有替换后执行一个分支,命令”t label”会在最后一个替换命令修改模式空间后执行分支。
这个命令的作用之一就是递归模式,个旧市你想删除括号内的空格,括号也许是嵌套的,却也许你想删除像"( ( ( ())) )"这样的字符串,下例:
sed 's/([ ^I]*)/g'
只能移除最内层的空格,你也许需要管道四次来删除每层的空格,你可能会用正则表达式:
sed 's/([ ^I()]*)/g'
但这又会把括号集合删除。”t”命令可以来解决这个问题
#!/bin/sh
sed '
:again
s/([ ^I]*)//g
t again
'
添加注释的另一种方式
如果你用的sed版本不支持注释,你可以用另一种方式来添加注释,用a命令对第0行操作:
#!/bin/sh
sed '
/begin/ {
0i\
This is a comment\
It can cover several lines\
It will work with any version of sed
}'
有着糟糕文档的”;”
还有一个没有在文档中很好解释的命令”;”,它可以用来接合几个sed命令到一行中,下面是我前面介绍过的grep4脚本,但没有注释和错误检查,它两个命令间有分号:
#!/bin/sh
sed -n '
'/$1/' !{;H;x;s/^.*\n\(.*\n.*\)$/\1/;x;}
'/$1/' {;H;n;H;x;p;a\
---
}'
天啊,简直是一堆乱码。我想我已经说明了我的观点。就我感觉,分号唯一有用的时候是在命令行环境下输入sed脚本。如果你在一个脚本中写sed,就应让它可读性高一些,我已经说过很多sed版本除了第一行外,不支持注释。你可以在你的脚本里写上注释,在使用时删掉它们,这样做并不困难。毕竟,你现在也成为一个sed大师了嘛。我不会再告诉你如果写一个将注释删掉的脚本了,因为如果我告诉你,这简直就是对你智力的侮辱。另外,一些操作系统不允许你用分号。所以如果你看到一个有分号的脚本,它不会在非Linux系统下正常运行,就将分号换成换行符。(只要你不在csh/tcsh下用。但那是另一个话题了)。
将正则表达式作为参数传递。
在前面的脚本中,我提到过如果你在传递参数时有一个斜杠就会有问题,事实上,可能是正则表达式引起的麻烦。一下像下面一样的脚本可能会有时候不能正常执行:
#!/bin/sh
sed 's/'"$1"'//g'
如果你传递的参数有”^.*[]^$”中的任何一个,你的脚本都会出错,如果有人在替换命令中使用了”/”,那么看起来就像是有四个分隔符,而不是三个。如果你参数中有”[”而没有”]”,会有语法错误提示。其中一个解决办法是在传递这种参数时在相应的字符前加上反斜杠。但是用户就必须知道哪个字符是特殊字符。
另一种解决办法是在脚本中这些字符的每一个前加上反斜杠:
#!/bin/sh
arg=`echo "$1" | sed 's:[]\[\^\$\.\*\/]:\\\\&:g'`
sed 's/'"$arg"'//g'
如果你想查找模式”^../”,脚本会在传递给sed之前将它转换成”\^\.\.\/”。
命令总结
如我前面所保证的一样,这里有一个不同命令的总结。第二列说明命令可不可以接受范围(2)或是一个位置(1)的限定。第四列说明命令会修改四个缓存中的哪个。一些命令会影响输出,另一些影响缓存。如果你记得模式空间是要输出的(除非使用了-n参数),这个表可以帮你了解这么多命令。
+---------------------------------------------------------+
|Command Address Modifications to |
| or Range Input Output Pattern Hold |
| Stream Stream Space Buffer |
+--------------------------------------------------------+
|= - - Y - - |
|a 1 - Y - - |
|b 2 - - - - |
|c 2 - Y - - |
|d 2 Y - Y - |
|D 2 Y - Y - |
|g 2 - - Y - |
|G 2 - - Y - |
|h 2 - - - Y |
|H 2 - - - Y |
|i 1 - Y - - |
|l 1 - Y - - |
|n 2 Y * - - |
|N 2 Y - Y - |
|p 2 - Y - - |
|P 2 - Y - - |
|q 1 - - - - |
|r 1 - Y - - |
|s 2 - - Y - |
|t 2 - - - - |
|w 2 - Y - - |
|x 2 - - Y Y |
|y 2 - - Y - |
+--------------------------------------------------------+
结论
我举例所用的脚本有可能会有更短的实现,但我选择这些脚本是为说明一些用法,所以我要使它清晰。希望你喜欢我的教程。