[转]Sed 介绍和教程

原文:http://quweiprotoss.blog.163.com/blog/static/40882883201011296102255/

 

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

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

结论

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

More References

This concludes my tutorial on sed. Other of my Unix shell tutorials can be found here. Other shell tutorials can be found at Heiner's SHELLdorado and Chris F. A. Johnson's Unix Shell Page
The Wikipedia Entry on SED
SED one-liners

posted @ 2011-12-05 23:23  Scan.  阅读(24663)  评论(2编辑  收藏  举报