正则表达式工具:sed

作者:jicanmeng

时间:2015年09月14日


  1. s命令表示替换
  2. i命令表示插入(insert)
  3. a命令表示追加(append)
  4. c命令表示修改(change)
  5. 打印命令:p, =, l,P
  6. 符号;用于隔开多个命令
  7. {}表示命令打包(command grouping)
  8. 从文件读取sed命令
  9. y命令表示替换
  10. 文件读写命令:r和w。
  11. 读取下行数据,继续执行命令,而不用回到命令的最开始地方执行:n和N。
  12. d命令表示删除(delete)
  13. 跳转命令:b和t
  14. &命令表示替换匹配的正则表达式
  15. 几个例子

关于sed有两个重要的概念,pattern space 和 hold buffer。每次sed从文件中读取一行后,都会放到pattern space中。hold buffer可以看做是一个临时的暂存区。

sed的工作原理如下:

while(input stream中有剩余的内容) {
    从input stream读入下一行;
    去除行尾的换行符'\n',然后放到pattern space中;
    对pattern space中的这一行文本执行script中的内容;
    对pattern space中的内容添加换行符'\n',再输出到output stream中。
}
每次循环称为一个cycle。

sed命令的格式如下:
sed [OPTION] {script} [input-file]

常用的options如下:
-n
--quiet
--silent
      suppress automatic printing of pattern space
      执行完script中的命令后,不会将pattern space中的内容输出到output stream中。这个选项常常和p命令一起用。
-e SCRIPT
--expression=SCRIPT
      add the script to the commands to be executed
      指定执行某个script。可以有多个-e选项同时使用。
-f script-file
--file=script-file
      add the contents of script-file to the commands to be executed
      指定执行某个文件中的script。
-r
--regexp-extended
      use extended regular expressions in the script.
      sed默认使用基本的正则表达式。使用-r选项会使用扩展的正则表达式。
-i[SUFFIX]
--in-place[=SUFFIX]
    直接修改输入文件。如果指定后缀,例如-i.bak,那么会首先创建一个.bak文件,然后再修改输入文件。

script的格式如下:
[address[,address]]command

address可以直接使用数字表示,表示对第几行执行命令。例如2d表示删除第2行的内容,2,7d表示删除2到7行的内容。 address也可以使用正则表达式的形式表示,表示对符合要求的某行执行命令。例如/abcd/d表示将包含有abcd字符的行删除,/abcd/,/defg/d表示将从包含abcd的行开始,一直到包含defg的行结束,全部删除。(这里特别需要注意的是,使用两个正则表达式表示范围时,第一个正则表达式表示打开功能,第二个正则表达式表示关闭功能。对于本例来说,如果文件中第3行包含abcd,第5行包含defg,第9行包含abcd,然后一直到文件末尾都不包含defg,那么会删除第3行到第5行的内容和第9行到文件末尾的内容。)

符号!放在某个address后面,表示对于这个address,不执行对应的命令。

符号$表示最后一行。

符号;用于分隔开多个command。等价于使用多个-e选项。

符号{}用于组合多个命令,各个命令之间使用;隔开。

s命令表示替换

s命令的格式如下:s/pattern/replacement/flags

常见的flag有四种:

  1. flag为数字:
    1. 只把每一行中的第一个my替换为your:
    2. sed "s/my/your/" pets.txt
      或者sed "s/my/your/1" pets.txt
    3. 只把每一行中的第二个my替换为your:
    4. sed "s/my/your/2" pets.txt
  2. flag为g:
    1. 把每一行中的所有my替换为your:
    2. sed "s/my/your/g" pets.txt
    3. 把每一行中的第3个到最后一个my替换为your:
    4. sed "s/my/your/3g" pets.txt
    5. 只把第三行中的所有my替换为your:
    6. sed "3s/my/your/g" pets.txt
    7. 把第三行到第六行中的所有my替换为your:
    8. sed "3,6s/my/your/g" pets.txt
  3. flag为p:
    1. 查找包含test字符串的行,并把本行中的第一个test字符串修改为abc,然后显示出来:
    2. sed -n "s/test/abc/p" pets.txt
      或者sed -n "s/test/abc/1p" pets.txt
    3. 查找包含test字符串的行,并把本行中的第二个test字符串修改为abc,然后显示出来:
    4. sed -n "s/test/abc/2p" pets.txt
  4. flag为w:
    1. 查找包含test字符串的行,并把本行中的第一个test字符串修改为abc,然后保存到result.txt文件中:
    2. sed -n "s/test/abc/w result.txt" pets.txt
      或者sed -n "s/test/abc/1w result.txt" pets.txt
    3. 查找包含test字符串的行,并把本行中的第二个test字符串修改为abc,然后保存到result.txt文件中:
    4. sed -n "s/test/abc/2w result.txt" pets.txt

从上面可以看出来,s命令使用正斜线/来分隔各个字段。如果pattern或replacement中也出现了正斜线/,那么必须要使用反斜线进行转义。例如:将/etc/passwd文件中的/bin/bash替换为/bin/csh:
sed 's/\/bin\/bash/\/bin\/csh/' /etc/passwd
这样看起来就非常不方便。为了解决这个问题,sed命令允许使用其它字符来作为s命令的字段分隔符,例如!,%都可以。info sed中时这样描述的:

The `/' characters may be uniformly replaced by any other single character within any given `s' command. The `/' character (or whatever other character is used in its stead) can appear in the REGEXP or REPLACEMENT only if it is preceded by a `\' character.
上面这个例子就可以写为:
sed 's!/bin/bash!/bin/csh!' /etc/passwd
这样看起来就清晰多了。

i命令表示插入(insert)

  1. 在第1行前面插入一行:
  2. echo "Test Line 1" | sed '1i hello,world!'

    使用上面的命令,会在第一行前面添加一行,其中hello,world从第一列开始。但是如果我希望hello,world不从第一列开始呢?那么就需要特殊符号\了,而且在插入多行时非常方便。
  3. 在第一行前面插入一行:
  4. echo "Test Line 1" | sed '1i\ hello,world!'
  5. 在第一行前面插入两行:
  6. echo "Test Line 1" | sed '1i\
     hello,world!\
    hello,programmer!'
    [jicanmeng@andy tmp]$ echo "Test Line 1" | sed '1i\ hello,world!'
                             hello,world!
                            Test Line 1
                    [jicanmeng@andy tmp]$ echo "Test Line 1" | sed '1i\
                    >   hello,world\
                    >  hello,programmer!'
                      hello,world!
                     hello,programmer!
                    Test Line 1
                    [jicanmeng@andy tmp]$ 
    从上图可以看出,要插入多行文本时,需要对新文本的末尾都使用反斜线,直到你要插入的文本的最后一行。

a命令表示追加(append)

  1. 在最后一行后面追加一行:
  2. sed "$ a This is my monkey, my monkey's name is wukong" pets.txt

    和命令i一样,a命令也会使用特殊符号\。这里就不列举例子了。

c命令表示修改(change)

  1. 修改第2行:
  2. sed "2 c This is my monkey, my monkey's name is wukong" pets.txt
  3. 修改包含fish的行:
  4. sed "/fish/c This is my monkey, my monkey's name is wukong" pets.txt

    和命令i和命令a一样,c命令也会使用特殊符号\。这里就不列举例子了。

p命令会打印pattern space的内容,=命令会打印当前正在处理的数据流的行号,l命令也会打印pattern space的内容,和p命令不同的是,l命令同时还把一些特殊字符打印了出来。P命令只会打印pattern space中的第一行。

  1. 打印包含fish的行:
  2. sed -n '/fish/p' pets.txt
  3. 打印从第一个包含dog的行到第一个包含fish的行的中间所有行:
  4. sed -n '/dog/,/fish/p' pets.txt
  5. 打印第1行到第一个包含fish的行的中间所有行:
  6. sed -n '1,/fish/p' pets.txt
  7. 打印文件内容,并显示行号:
  8. sed '=' pets.txt
  9. 打印包含fish的行,并显示行号:
  10. sed -n '/fish/{=;p}' pets.txt
  11. 显示pets.txt文件的内容,包括特殊字符:
  12. sed -n 'l' pets.txt

符号;用于隔开多个命令

  1. 把第1行到第3行中的my替换为your,第3行到最后一行中的This替换为That:
  2. sed '1,3s/my/your/g; 3,$s/This/That/g' pets.txt
    等价于sed -e '1,3s/my/your/g' -e '3,$s/This/That/g' my.txt

{}表示命令打包(command grouping)

  1. 对3行到第6行,寻找包含This的行,找到就删除:
  2. sed '3,6 {/This/d}' pets.txt
  3. 对3行到第6行,匹配/This/成功后,再匹配/fish/,成功后执行d命令:
  4. sed '3,6 {/This/{/fish/d}}' pets.txt
  5. 从第一行到最后一行,如果匹配到This,则删除之;如果前面有空格,则去除空格:
  6. sed '1,${/This/d;s/^ *//g}' pets.txt

从文件读取sed命令

[jicanmeng@andy tmp]$ cat script1
                    s/brown/green/
                    s/fox/elephant/
                    s/dog/cat/
                [jicanmeng@andy tmp]$ cat data1
                    The quick brown fox jumps over the lazy dog.
                [jicanmeng@andy tmp]$ sed -f script1 data1
                    The quick green elephant jumps over the lazy cat.
                [jicanmeng@andy tmp]$ 

y命令表示替换

y命令是唯一可以处理单个字符的sed编辑器命令。命令格式如下:
[address]y/inchars/outchars/
转换命令会进行inchars和outchars值的一对一映射。inchars中的第一个字符会被转换为outchars中的第一个字符,第二个字符会被转换为outchars中的第二个字符。这个映射会一直持续到处理完指定字符。如果inchars和outchars的长度不同,则sed编辑器会产生一条错误信息。

  1. 将所有行中的1转换为4,2转换为5,3转换为6:
  2. sed 'y/123/456' pets.txt

y命令是一个全局命令,也就是说,它会自动替换一行中出现所有的inchars中的字符,你无法指定只替换某个字符,也无法指定只替换某字符第几次出现的地方。

文件读写命令:r和w

w命令表示写到文件中,r表示从文件读取数据。info sed文档中对r命令的描述是:

Queue the contents of FILENAME to be read and inserted into the output stream at the end of the current cycle, or when the next input line is read. Note that if FILENAME cannot be read, it is treated as if it were an empty file, without any error indication.

  1. 将前两行保存到一个文件中:
  2. sed '1,2w test.txt' pets.txt
  3. 将test.txt文件的内容插入到第2行后面:
  4. sed '2r test.txt' pets.txt
  5. 将包含fox的行删除,替换为test.txt文件的内容:
  6. [jicanmeng@andy tmp]$ cat script1
                                s/brown/green/
                                s/fox/elephant/
                                s/dog/cat/
                            [jicanmeng@andy tmp]$ cat data1
                                The quick brown fox jumps over the lazy dog.
                            [jicanmeng@andy tmp]$ sed '/fox/{
                            > r data1
                            > d}' script1
                            s/brown/green/
                            The quick brown fox jumps over the lazy dog.
                            s/dog/cat/
                            [jicanmeng@andy tmp]$ 

两点需要注意:1. 对于r命令:在本次cycle中所有命令执行完毕后,下次cycle从data1文件中读取数据,data1文件的数据读取完毕后,再继续从script1文件读取数据。2. 对于r命令,不要和其它的命令写在一行,因为文件名时可以包含一些特殊字符的,写在一行往往会出现读取的文件不存在的问题导致出错。

读取下行数据,继续执行命令,而不用回到命令的最开始地方执行:n和N。

通常sed编辑器会在本行上执行完所有的命令,才会读取数据流的下一行,进行下次的命令执行过程。n命令改变了这个流程。它会直接读取下一行,放到pattern space中,继续执行本次循环中n命令后面的命令。info sed文档中对n命令的描述是:

If auto-print is not disabled, print the pattern space, then, regardless, replace the pattern space with the next line of input.

N命令也会读取下一行,但不是替换pattern space的内容,而是附加到pattern space中,然后继续执行N命令后面的命令。info sed文档中对N命令的描述是:

Add a newline to the pattern space, then append the next line of input to the pattern space. If there is no more input then `sed' exits without processing any more commands.

  1. 将短语System Administrator替换为Desktop User:
  2. [jicanmeng@andy tmp]$ cat data3
                                The first meeting of the Linux System
                                Administrator's group will be held on Tuesday.
                                All System Administrator should attend this meeting.
                                Thank you for your attentance.
                            [jicanmeng@andy tmp]$ sed 'N; s/System\(.\)Administrator/Desktop\1User/' data3
                                The first meeting of the Linux Desktop
                                User's group will be held on Tuesday.
                                All Desktop User should attend this meeting.
                                Thank you for your attentance.
                            [jicanmeng@andy tmp]$ sed '{
                            > N
                            > s/System\nAdministrator/Desktop\nUser/
                            > s/System Administrator/Desktop User/}' data3
                                The first meeting of the Linux Desktop
                                User's group will be held on Tuesday.
                                All Desktop User should attend this meeting.
                                Thank you for your attentance.
                            [jicanmeng@andy tmp]$ 
    这个例子非常典型。使用一条命令就可以代替下面的三条命令。由此也可以看出,在正则表达式中,.符号也可以代表换行符\n。我们再通过一个例子看一看n和N的用法:
    [jicanmeng@andy tmp]$ cat one.txt
                            I install linux on my computer
                            hello world
                            I like programming.
                            [jicanmeng@andy tmp]$ sed '=;n;=;=;=' one.txt
                                1
                                I install linux on my computer
                                2
                                2
                                2
                                hello world
                                3
                                I like programming
                            [jicanmeng@andy tmp]$ sed -n '=;n;=;=;=' one.txt
                                1
                                2
                                2
                                2
                                3
                            [jicanmeng@andy tmp]$ sed '=;N;=;=;=' one.txt
                                1
                                2
                                2
                                2
                                I install linux on my computer
                                hello world
                                3
                                I like programming
                            [jicanmeng@andy tmp]$ 
    可以看出,当下一行没有数据时,无论是n还是N,都不会继续执行后面的命令了,而是直接打印pattern space,结束本次循环了。

d命令表示删除(delete)

和n命令和N命令相同,d命令和D命令也改变了sed的命令执行过程。d命令删除pattern space,然后从头开始执行命令,即开始下次循环。D命令仅仅删除pattern space的第一行,如果pattern space中还有数据,就不读下一行,而是从头再进行一次命令循环;如果pattern space中没有数据了,就读取下一行,进行一次命令循环。

info sed文档中对D命令的描述是:

Delete text in the pattern space up to the first newline. If any text is left, restart cycle with the resultant pattern space (without reading a new line of input), otherwise start a normal new cycle.

  1. 删除包含fish的行:
  2. sed '/fish/d' pets.txt
  3. 删除第2行:
  4. sed '2d' pets.txt
  5. 删除第2行到最后一行:
  6. sed '2,$d' pets.txt
再看一个D命令的例子:
[jicanmeng@andy tmp]$ cat one.txt
                        I install linux on my computer
                        hello world
                        I like programming.
                        [jicanmeng@andy tmp]$ sed '=;=;N;D;=;=;=' one.txt
                            1
                            1
                            2
                            2
                            3
                            3
                            I like programming
                        [jicanmeng@andy tmp]$ 

跳转命令:b和t

b命令表示跳转,格式如下: [address]b [label]
address参数决定了哪行或哪些行的数据会触发跳转命令。label参数定义了要跳转到的位置。如果没有加label参数,跳转命令就会跳转到脚本的末尾。

  1. 例1:
  2. [jicanmeng@andy tmp]$ echo "This, is, a, test, to, remove, commas." | sed -n '{
                            > :start
                            > s/,//1p
                            > b start
                            > }'
                                This is, a, test, to, remove, commas.
                                This is a, test, to, remove, commas.
                                This is a test, to, remove, commas.
                                This is a test to, remove, commas.
                                This is a test to remove, commas.
                                This is a test to remove commas.
                                ^C
                            [jicanmeng@andy tmp]$ 
    这个脚本有个问题。它永远都不会停下,因为总会跳转到start。我们可以用t命令来解决这个问题。测试(test)命令t会基于替换命令的输出跳转到一个标签。格式如下: [address]b [label]

    如果替换命令成功匹配,就会跳转到label。和b命令一样,在没有指定标签的情况下,如果测试成功,sed会跳转到脚本的末尾。
  3. 例2:
  4. [jicanmeng@andy tmp]$ echo "This, is, a, test, to, remove, commas." | sed -n '{
                            > :start
                            > s/,//1p
                            > t start
                            > }'
                                This is, a, test, to, remove, commas.
                                This is a, test, to, remove, commas.
                                This is a test, to, remove, commas.
                                This is a test to, remove, commas.
                                This is a test to remove, commas.
                                This is a test to remove commas.
                            [jicanmeng@andy tmp]$ 
  5. 例3:
  6. [jicanmeng@andy tmp]$ echo "1234567" | sed '{
                            > :start
                            > s/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/
                            > t start
                            > }'
                                1,234,567
                            [jicanmeng@andy tmp]$ 
    这个例子非常重要,它体现了正则表达式匹配中的"贪心法"原则。

&命令表示替换匹配的正则表达式

  1. 看一个例子:
  2. [jicanmeng@andy tmp]$ echo "The cat sleeps in his hat." | sed 's/.cat/"&"/g'
                                The "cat" sleeps in his "hat".
                            [jicanmeng@andy tmp]$ 

几个例子

  1. 加倍行间距:
  2. sed 'G' test.txt
  3. 加倍行间距,但最后一行不用加倍:
  4. sed '$!G' test.txt
  5. 加倍行间距,但如果两行之间已经有空白行,就不用加倍了:
  6. sed '/^$/d; $!G' test.txt
  7. 给文件中的行编号:
  8. sed '=' test.txt
  9. 给文件中的行编号,并且行号和行内容在同一行:
  10. sed '=' test.txt | sed 'N; s/\n/ /'
  11. 打印末尾的10行:
  12. [jicanmeng@andy tmp]$ sed '{
                            > :start
                            > $q
                            > N
                            > 11,$D
                            > b start
                            > }' /etc/passwd
                            [jicanmeng@andy tmp]$ 
    这个例子很好,注意看11,$D,如果文件内容小于10行,这句就不起作用。
  13. 删除文件中连续的空白行,保证两行之间只有一个空白行即可:
  14. sed '/./,/^$/!d' test.txt 这个例子也非常妙,区间是/.//^$/,区间的开始地址匹配会匹配任何含有至少一个字符的行,区间的结束地址会匹配一个空行。在这个区间内的行不会被删除,其余的行都被删除了。
  15. 删除开头的空白行:
  16. sed '/./,$!d' test.txt
  17. 打印结尾的空白行:
  18. [jicanmeng@andy tmp]$ sed '{
                            > :start
                            > /^\n*$/{$d; N; b start}
                            > }' test.txt
                            [jicanmeng@andy tmp]$ 
  19. 将本目录和所有子目录下的文件中的aabb字符串替换为ccdd:
  20. sed -i 's/aabb/ccdd/g' `grep -rl aabb .`

    其中的-l选项表示只显示匹配到的文件名。

参考资料

  1. 第十二章、正规表示法与文件格式化处理:
    http://vbird.dic.ksu.edu.tw/linux_basic/0330regularex.php#sed
  2. coolshell:sed 简明教程:
    http://coolshell.cn/articles/9104.html
  3. World's best introduction to sed:
    http://www.catonmat.net/blog/worlds-best-introduction-to-sed/
  4. Famous Sed One-Liners Explained:
    http://www.catonmat.net/blog/sed-one-liners-explained-part-one/
    http://www.catonmat.net/blog/sed-one-liners-explained-part-two/
    http://www.catonmat.net/blog/sed-one-liners-explained-part-three/
    http://www.catonmat.net/blog/sed-book/
  5. sed 批量替换多个文件:
    http://blog.csdn.net/lingphper/article/details/52767077