Sed 介绍和教程

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加上替换。即是每个替换命令只关心一行,而不管附近几行。如果可以有限制在特定的一些行内操作,那该多有用呀。一些有用的限制可以是:

  1. 通过行号指定某行。
  2. 通过行号指过行的区间。
  3. 所有包含某一模式的行。
  4. 从文件开始到某一正则的所有行。
  5. 从某一正则到文件结尾的所有行。
  6. 在两个正则之间的所有行。

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. 从输入文件读取一行,将它放入模式空间。如果当前文件到达文件结束处,还有其它文件要读,将当前文件关闭,将下一个文件打开,将新的文件的第一行读入模式空间。
  2. 文件行加1。打开一个新文件不会重置文件行数。
  3. 检查每一个sed命令,如果在命令前有一个限制,并且在模式空间的当前行符合这一限制,命令就会被执行,一些命令,如”n”或”d”会导致sed返回第1步,”q”命令会导致sed中止,否则下一条命令继续执行。
  4. 在所有命令被检查执行后,除非使用了”-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/

#   print

       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      -       |

+--------------------------------------------------------+

结论

    我举例所用的脚本有可能会有更短的实现,但我选择这些脚本是为说明一些用法,所以我要使它清晰。希望你喜欢我的教程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值