sed 详解

sed:Stream EDitor,即流编辑器,使用 regex 处理数据;个人认为 sed 更像是一门编程语言,而非编辑器,请待我慢慢道来。

sed 简介

sed 每次只从输入中读取一行到一个 buffer 中(在 buffer 中不存在换行符),而这个 buffer 还有一个特别的名字 - 模式空间(pattern space);然后,模式空间将依次被 script(即 sed 指令)操作,一条一条的顺序执行下去,就像流水线一样;模式空间被 script 操作完成后,最后被送往屏幕(使用 -i 选项将直接修改源文件);然后又紧接着读取输入的下一行,就这样重复,直到文件流的末尾,sed 就执行完毕了。

sed 除了模式空间外,还存在一个保持空间(hold space),也就是说:sed 存在两个 buffer 缓冲区,使用 h(Hold) 命令将模式空间复制至保持空间,使用 H 命令将模式空间追加至保持空间,使用 g(Get) 命令将保持空间复制至模式空间,使用 G 命令将保持空间追加至模式空间,还有一个 x 命令,用于交换模式空间和保持空间。

sed 还支持语句块(statement block),使用{}括起来即可,就像 shell 的函数一样;同时还有注释行,也是使用#;sed 也有 goto 语句,不过在 sed 中,它不叫做 goto,而是被叫做 branch 分支(git 也有分支概念),而 branch 的 target 是一个 label 标签;因此,我们可以在任何一个位置打 label,然后再使用 b 命令无条件跳转至 label 处,以此达到 goto、loop 的效果;有无条件跳转就当然有条件跳转了,条件跳转就如同在跳转前先使用 if 条件语句,如果条件为真才会跳转至 label 处;而这个条件就是上一个s///语句(正则替换)是否修改了模式空间,t 是修改了,T 是未修改。

这样看下来,是不是觉得 sed-script 和一个普通的脚本语言很相似?是的,我一直都认为这是一门像模像样的 script-language!

sed 详解

Usage:sed [OPTIONS...] {sed-script} [FILES...]

  • OPTIONS:命令行选项;
  • sed-script:脚本,使用 -e 从命令行读取,使用 -f 从文件中读取;
    • ADDRESS:定位,也叫作用行,表示仅将指定行应用 COMMAND;
    • COMMAND:指令,即定义如何对模式空间、保持空间进行操作;
  • FILES:输入文件,如果省略,表示从标准输入读取;

命令行选项

  • -e script:从命令行中读取 script,可以有多个 -e 选项;如果省略,则默认为第一个参数;
  • -f script-file:从文件中读取 script;
  • -r:使用扩展正则表达式,GNU;
  • -R:使用扩展正则表达式,POSIX(不推荐);
  • --posix:关闭所有 GNU 扩展(不推荐);
  • --follow-symlinks:始终跟随符号链接文件;
  • -n:禁止自动打印模式空间,除非使用 p/P 等显式打印命令;
  • -l N:定义模式空间初始大小,默认为 70 个字符;
  • -u:输入和输出都尽量不使用缓冲区;如,从输入文件读取最少量的数据、经常刷新输出缓冲区;
  • -s:将每个文件看作是一个单独的文件(在使用行号定位时有影响),默认将所有输入文件合并为一个输入流;
  • -i[SUFFIX]:直接编辑文件,如果指定了 SUFFIX(如:-i'.bak'),则将源文件备份为同名 .bak 文件;

ADDRESS

默认 sed 会将 COMMAND 应用于所有行,除非指定以下 ADDRESS 作用域:

  • number:指定行(有多个文件时行号会累加,使用 -s 不累加),最后一行为$
  • first,last:指定行范围;
  • first,+N:指定行范围(相对行数);
  • /regex/:所有匹配 regex 的行;
  • \cregexc:所有匹配 regex 的行,c 可以为任意字符;
  • /regex1/,/regex2/:所有匹配 regex1 ~ regex2 的行范围;
  • \cregex1c,\cregex2c:所有匹配 regex1 ~ regex2 的行范围,c 可以为任意字符;
  • first~step:每隔 step 行处理;如 sed -n '1~2 p' 将打印所有奇数行;
  • !:放置在 ADDRESS 后用于取反操作,表示不与之相匹配的行执行后面的 COMMAND;

COMMAND

  • {}:sed 语句块;
  • #:sed 注释行;
  • : label:在当前语句处打上标签,与 b、t、T 结合使用,类似 goto 语句;
  • b label:无条件跳转至 label 标签,如果标签不存在则跳转至脚本末尾;
  • t label:如果上一个s///修改了模式空间则跳转,如果标签不存在则跳转至脚本末尾;
  • T label:如果上一个s///未修改模式空间则跳转,如果标签不存在则跳转至脚本末尾(GNU);
  • q [code]:立即退出 sed-script,默认退出码为 0(若未指定 -n,则打印模式空间后退出);
  • Q [code]:立即退出 sed-script,默认退出码为 0;
  • c \string:使用文本”string”替换当前行,使用\n可写入多行;
  • i \string:在当前行前插入文本”string”,使用\n可插入多行;
  • a \string:在当前行后追加文本”string”,使用\n可追加多行;
  • r filename:在当前行后追加文件 filename 所有行;
  • R filename:在当前行后追加文件 filename 第一行(GNU);
  • w filename:将当前模式空间所有行写入文件 filename;
  • W filename:将当前模式空间第一行写入文件 filename(GNU);
  • h H:复制/追加模式空间保持空间
  • g G:复制/追加保持空间模式空间
  • x:交换模式空间保持空间
  • =:打印当前行号;
  • p:打印当前模式空间所有行;
  • P:打印当前模式空间第一行;
  • l:以 “视觉明确” 形式打印当前行(如显示 Tab 符、行尾符);
  • d:删除模式空间所有行,并开始下轮循环;
  • D:删除模式空间第一行,并开始下轮循环;
  • n N:读取/追加下一输入行至模式空间;
  • y/src/dst/:将 src 替换为 dst(非正则匹配);
  • s/regex/repl/:将首个被 regex 匹配的字符串替换为 repl 字符串,/可替换为其它字符如@;在 repl 中,可引用匹配的子串,&引用整个匹配串、\1~\9引用对应序号子串;
  • s/regex/repl/g:将全部被 regex 匹配的字符串替换为 repl 字符串;
  • s/regex/repl/i:将首个被 regex 匹配的字符串替换为 repl 字符串(忽略大小写匹配);
  • s/regex/repl/ig:将全部被 regex 匹配的字符串替换为 repl 字符串(忽略大小写匹配);

其它说明

  • sed 不支持 regex 非贪婪匹配,不过可以使用各种 sed 奇技淫巧来完成;
  • sed 支持多个 script,每个 script 前使用 -e 选项指定;也可以使用;分隔,而不是用 -e 选项;
  • 比如:sed -e 's/9/8/g' -e 's/8/7/g'sed 's/9/8/g; s/8/7/g'是等价的,个人比较喜欢后者。

sed 例子

基础用法

进阶用法

我现在有一个 C 语言源文件,它的内容如下(省略了其它无关紧要的部分):

我现在想将/* ... */注释行删除,用 sed 该怎么做?
因为它们在不同的行,想要删除它们需要使用多行匹配;

sed 脚本如下(script.sed):

测试一下:

sed 脚本解释如下:

但是,sed 并不支持非贪婪匹配,也就是说:*元字符会匹配尽可能多的字符;看例子:

我们主要分析这条 sed 命令s@^(.*)/\*.*\*/(.*)$@\1\2@g
第一个括号的.*匹配到了printf("end\n"); /* Comment-A */ printf("test\n");
第二个括号的.*什么都没匹配到,为空;
因此后面的\1\2实际就是引用的\1

不过,我想应该还是可以通过其他 sed 奇技淫巧实现”非贪婪匹配”,来正确的完成替换;
我们暂且不考虑这个东西,如果真的需要非贪婪匹配,那么请使用更强大的 Perl 正则。

sed 参考

Sed - An Introduction and Tutorial by Bruce Barnett
sed, a stream editor - GNU.org