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