本文的介绍基于 GNU sed ,它对 POSIX sed 进行了一些扩展。
Sed 是一个流编辑器。它是基于行的,按顺序对每一行执行命令,然后,将其结果写入标准输出 (stdout)。
sed 维护着两个数据缓存区:模式空间和保留空间。两者均被初始化为空。
sed 对输入的每一行运行一次如下所述的执行周期:首先,sed 从输入流中读入一行,并删除行末的换行符,将此行的内容放入模式空间。然后,脚本里的命令被执行;可以对每一个命令指定地址(地址相当于一种条件,只有条件被满足,才会执行紧跟其后的命令。当到达脚本的结尾,模式空间的内容(如果之前行末的换行符被删除,此时会被加回来)被写入到输出流(除非使用了选项'-n')。然后,对下一行开始下一个执行周期。
除非使用了命令'D',否则,在两次执行周期之间,模式空间的内容是被删除的。相反,保留空间的内容在两次执行周期之间是被保持的。
sed的基本用法 :sed [选项]... {脚本} [输入文件]...
选项 | 说明 |
---|---|
-n, --quiet, --silent | 默认情况下,sed在每个执行周期结束时打印出模式空间,此选项禁用此功能 |
-e 脚本, --expression=脚本 | 添加“脚本”到程序的运行列表 |
-f 脚本文件, --file=脚本文件 | 添加“脚本文件”的内容到程序的运行列表 |
-i[扩展名], --in-place[=扩展名] | 直接修改文件(如果指定扩展名就备份文件) |
--posix | 关闭所有 GNU 扩展 |
-r, --regexp-extended | 在脚本中使用扩展正则表达式 |
-s, --separate | 将所有输入文件看成是独立的输入流,而非单个长输入流 |
如果没有 -e, --expression, -f 或 --file 选项,那么第一个非选项参数被视为sed脚本。其他非选项参数被视为输入文件,如果没有输入文件,或者输入文件是'-',那么程序将从标准输入读取数据。
命令 | 说明 |
---|---|
q | 退出sed,此命令不接受地址范围 |
d | 删除模式空间,立即进入下一个执行周期 |
D | 删除模式空间的内容直到遇到第一个换行符,若模式空间非空,则对剩余内容重新运行一次执行周期(不读入新行),否则,进入下一个执行周期 |
p | 打印模式空间,常和选项"-n"联合使用 |
P | 打印模式空间的内容直到遇到第一个换行符 |
n | 若没有用'-n'选项,则打印模式空间的内容,然后将下一行读入并替换掉模式空间,若输入已结束则直接退出sed |
N | 添加一个换行符到模式空间,然后将下一行附加到模式空间,若输入已结束则直接退出sed |
s/// | 对模式空间的内容执行替换操作 |
g | 将保留空间的内容覆盖到模式空间 |
G | 添加一个换行符到模式空间,然后将保留空间的内容附加到模式空间 |
h | 将模式空间的内容覆盖到保留空间 |
H | 添加一个换行符到保留空间,然后将模式空间的内容附加到保留空间 |
x | 交换保留空间和模式空间的内容 |
= | 打印行号(带一个换行符),在 GNU 扩展下接受地址范围 |
r FILENAME | 在当前执行周期结束时将文件的内容插入到输出流 |
w FILENAME | 将模式空间的内容写入到文件中 |
: LABEL | 指定一个标签。不接受任何地址 |
b LABEL | 无条件跳转到标签 LABEL。若 LABEL 省略,则开始下一个执行周期 |
t LABEL | 若成功完成了一次s/// 命令,或者是最后一个t 命令,则跳转到标签 LABEL。若 LABEL 省略,则开始下一个执行周期 |
z | 清空模式空间。在模式空间有多字节编码的情况下,此命令比 's/.*//' 更有效。(GNU 扩展) |
sed 可以对每一个编辑命令指定一个操作地址或地址范围。地址可以是以下形式:
地址形式 | 说明 |
---|---|
正整数 | 指定行号。sed 会对所有输入文件的行进行连续计数,除非使用了'-s'或'-i'选项 |
起始行号~步长 | 行号满足公式“起始行号+N×步长”(N为非负整数)的行被匹配 |
$ | 匹配最后一个输入文件的最后一行。如果指定了'-s'或'-i'选项,就匹配每个输入文件的最后一行 |
/REGEXP/ | 选择匹配正则表达式 REGEXP 的所有行 |
\%REGEXP% | '%'可换成任何单个字符。作用同上,不过,允许使用除'/'之外的其它字符作为正则表达式的分隔符 |
/REGEXP/I | 'I'标志表示匹配正则表达式时忽略大小写 |
地址范围由起始地址和结束地址加一个逗号(,)分隔所组成。地址范围所指定的行,从匹配起始地址的行开始,一直到匹配结束地址的行为止(包含起始行和结束行)。如果没有行能够匹配结束地址,那么就一直指定到输入文件的末尾。
如果结束地址是一个正则表达式,那么会从起始行的下一行开始寻找匹配结束地址的行。
如果结束地址是一个不大于起始地址的行号,那么就只有起始行被指定。
GNU sed 也支持以下几种特殊形式的地址范围:
特殊地址范围形式 | 说明 |
---|---|
0,/REGEXP/ | sed 会尝试从第一行开始匹配REGEXP,而'1,/REGEXP/'是从第二行开始匹配REGEXP |
addr1,+N | 匹配地址addr1和它下面的N行 |
addr1,~N | 匹配地址addr1和它下面的行,直到行号是N的倍数 |
地址(或地址范围)位于编辑命令的左边。如果在它们中间加一个感叹号(!),就表示对地址(或地址范围)指定行之外的其它行执行编辑命令。
sed 正则表达式中使用的特殊字符:
字符 | 说明 |
---|---|
^ | 与行首匹配 |
$ | 与行尾匹配 |
. | 与任一个字符匹配,包括换行符 |
* | 与前一个字符的零或多个出现匹配 |
+ | 与前一个字符的一或多个出现匹配(GNU 扩展) |
\? | 与前一个字符的零或一个出现匹配(GNU 扩展) |
{I} | 与前一个字符的I个出现匹配,I是0~255的整数 |
{I,J} | 与前一个字符的I~J个出现匹配,I是0~255的整数 |
{I,} | 与前一个字符的I个或多于I个出现匹配,I是0~255的整数 |
[字符列表] | 与'字符列表'之内的单个字符匹配,可用"-"来指定字符范围 |
[^字符列表] | 与非'字符列表'之内的单个字符匹配 |
(REGEXP) | 对匹配正则表达式REGEXP的部分分组,用于后向引用 |
\DIGIT | 引用分组号为DIGIT的子串 |
sed 正则表达式中的特殊字符列表:
字符列表 | 说明 |
---|---|
[:alnum:] | 字母数字 [a-zA-Z0-9] |
[:alpha:] | 字母 [a-zA-Z] |
[:blank:] | 空格或制表符 |
[:cntrl:] | 任何控制字符 |
[:digit:] | 数字 [0-9] |
[:graph:] | 任何可视字符(无空格) |
[:lower:] | 小写 [a-z] |
[:print:] | 非控制字符 |
[:punct:] | 标点字符 |
[:space:] | 空格或制表符 |
[:upper:] | 大写 [A-Z] |
[:xdigit:] | 十六进制数字 [0-9a-fA-F] |
sed 正则表达式中常用的转义字符:
转义字符 | 说明 |
---|---|
\w | 匹配任何 "word" 字符,即字母、数字和下划线 |
\W | 匹配 "non-word"字符 |
\b | 匹配单词边界,即 "word" 字符和 "non-word" 字符之间的位置 |
\B | 匹配除单词边界以外的任何地方,即两个 "word" 字符之间,或者两个 "non-word" 字符之间 |
\' | 匹配模式空间的结尾 |
` | 匹配模式空间的开始 |
下面是几个示例:
正则表达式 | 描述 |
---|---|
. | 将与包含至少一个字符的字符串匹配 |
.. | 将与包含至少两个字符的字符串匹配 |
.** | 将与任何字符串匹配,包括空串 |
^# | 将与以 '#' 开始的任何字符串匹配 |
^$ | 将与所有空行匹配 |
}$ | 将与以 '}'(无空格)结束的任何行匹配 |
} **$ | 将与以 '}' 后面跟有零或多个空格结束的任何行匹配 |
\$ | 将与以反斜杠(\)结尾的字符串匹配 |
\$ | 将与包含美元符号的字符串匹配 |
^[abc] | 将与以 'a'、'b' 或 'c' 开始的任何行匹配 |
[^[:space:]]+ | 将与一个或多个非空格和制表符的字符组成的字符串匹配,通常匹配一个词语 |
^(.*)\n\1$ | 将与由换行符分隔的两个相同子串组成的字符串匹配 |
如果只要打印 C 源文件中的 main() 函数,可输入:
1 | $ sed -n -e '/main[[:space:]]*(/,/^}/p' sourcefile.c | more |
该命令有两个规则表达式 /main[[:space:]]*(/
和 /^}/
,以及一个命令 'p'。第一个规则表达式将与后面依次跟有任意数量的空格或制表键以及开始圆括号的字符串 "main" 匹配。这应该与一般 ANSI C main() 声明的开始匹配。
在这个特别的规则表达式中,出现了 [:space:]
字符类。这只是一个特殊的关键字,它告诉 sed 与 TAB 或空格匹配。如果愿意的话,可以不输入 [:space:]
,而输入 '[',然后是空格字母,然后是 Ctrl-V,然后再输入制表键字母和 ']' 。Ctrl-V 告诉 bash 要插入“真正”的制表键,而不是执行命令扩展。使用 [:space:]
字符类(特别是在脚本中)会更清楚。
好,现在看一下第二个 regexp。/^}/
将与任何出现在新行行首的 '}' 字符匹配。如果代码的格式很好,那么这将与 main() 函数的结束花括号匹配。如果格式不好,则不会正确匹配。
因为是处于 '-n' 安静方式,所以 'p' 命令还是完成其惯有任务,即明确告诉 sed 打印该行。试着对 C 源文件运行该命令,它应该输出整个 main() { } 块,包括开始的 "main()" 和结束的 '}'。
s///
替换命令的完整语法应该是 s/REGEXP/REPLACEMENT/FLAGS
关于 s///
命令的一个妙处是 '/' 分隔符有许多替换选项。如果正在执行字符串替换,并且正则表达式或替换字符串中有许多斜杠,则可以通过在 's' 之后指定一个不同的字符来更改分隔符。例如,下例将把所有出现的 /usr/local 替换成 /usr:
1 | $ sed -e 's:/usr/local:/usr:g' mylist.txt |
在该例中,使用冒号作为分隔符。如果需要在正则表达式中指定分隔符字符,可以在它前面加入反斜杠(\)进行转义。
GNU 扩展下,REPLACEMENT 可以包含以下几个特殊的标记:
标记 | 说明 |
---|---|
\L | 将 replacement 转换为小写,直到遇到'\U'或者'\E' |
\l | 将下一个字符转换为小写 |
\U | 将 replacement 转换为大写,直到遇到'\L'或者'\E' |
\u | 将下一个字符转换为大写 |
\E | 终止由'\U'或'\L'开始的大小写转换 |
在替换命令中,'FLAGS' 是可选的。常用的 FLAGS 有以下几个:
FLAGS | 说明 |
---|---|
g | 全局替换 |
NUMBER | 只替换第 NUMBER 个匹配 |
p | 替换操作完成后打印新的模式空间内容 |
i 或 I | 匹配 REGEXP 时忽略大小写 |
w FILENAME | 替换完成后,将结果写入到文件中 |
我们已经看到如何执行简单甚至有些复杂的直接替换,但是 sed 还可以做更多的事。实际上可以引用匹配正则表达式的部分或全部,并使用这些部分来构造替换字符串。作为示例,假设您正在回复一条消息。下例将在每一行前面加上短语 "ralph said: ":
1 | $ sed -e 's/.*/ralph said: &/' origmsg.txt |
输出如下:
1 2 3 4 | ralph said: Hiya Jim, ralph said: ralph said: I sure like this sed stuff! ralph said: |
该例的替换字符串中使用了 '&' 字符,该字符告诉 sed 插入匹配正则表达式的整个部分。因此,可以将与 '.*' 匹配的任何内容(行中的零或多个字符的最大组或整行)插入到替换字符串中的任何位置,甚至多次插入。
s///
命令还允许我们在规则表达式中定义区域,然后可以在替换字符串中引用这些特定区域。作为示例,假设有一个包含以下文本的文件:
1 2 3 4 | foo bar oni eeny meeny miny larry curly moe jimmy the weasel |
现在假设要编写一个 sed 脚本,该脚本将把 "eeny meeny miny" 替换成 "Victor eeny-meeny Von miny" 等等。要这样做,首先要编写一个由空格分隔并与三个字符串匹配的规则表达式。
1 | '.* .* .*'
|
现在,将在其中每个感兴趣的区域两边插入带反斜杠的圆括号来定义区域:
1 | '\(.*\) \(.*\) \(.*\)'
|
除了要定义三个可在替换字符串中引用的逻辑区域以外,该规则表达式的工作原理将与第一个规则表达式相同。下面是最终脚本:
1 | $ sed -e 's/\(.*\) \(.*\) \(.*\)/Victor \1-\2 Von \3/' myfile.txt |
如您所见,通过输入 '\x'(其中,x 是从 1 开始的区域号)来引用每个由圆括号定界的区域。输出如下:
1 2 3 4 | Victor foo-bar Von oni Victor eeny-meeny Von miny Victor larry-curly Von moe Victor jimmy-the Von weasel |
在开始创建更复杂的 sed 脚本时,需要有输入多个命令的能力。有几种方法这样做。首先,可以在命令之间使用分号。例如,以下命令系列使用 '=' 命令和 'p' 命令,'=' 命令告诉 sed 打印行号,'p' 命令明确告诉 sed 打印该行(因为处于 '-n' 模式)。
1 | $ sed -n -e '=;p' myfile.txt |
无论什么时候指定了两个或更多命令,都按顺序将每个命令应用到文件的每一行。在上例中,首先将 '=' 命令应用到第 1 行,然后应用 'p' 命令。接着,sed 继续处理第 2 行,并重复该过程。虽然分号很方便,但是在某些场合下,它不能正常工作。另一种替换方法是使用两个 -e 选项来指定两个不同的命令:
1 | $ sed -n -e '=' -e 'p' myfile.txt |
然而,在使用更为复杂的附加和插入命令时,甚至多个 '-e' 选项也不能帮我们的忙。对于复杂的多行脚本,最好的方法是将命令放入一个单独的文件中。然后,用 -f 选项引用该脚本文件:
1 | $ sed -n -f mycommands.sed myfile.txt
|
这种方法虽然可能不太方便,但总是管用。
有时,可能要指定应用到一个地址的多个命令。这在执行许多 s///
以变换源文件中的字和语法时特别方便。要对一个地址执行多个命令,可在文件中输入 sed 命令,然后使用 '{}' 字符将这些命令分组,如下所示:
1 2 3 4 5 | 1,20{ s/[Ll]inux/GNU\/Linux/g s/samba/Samba/g s/posix/POSIX/g } |
上例将把三个替换命令应用到第 1 行到第 20 行(包括这两行)。还可以使用规则表达式地址或者二者的组合:
1 2 3 4 5 6 | 1,/^END/{ s/[Ll]inux/GNU\/Linux/g s/samba/Samba/g s/posix/POSIX/g p } |
该例将把 '{ }' 之间的所有命令应用到从第1行开始,到以字母"END"开始的行结束(如果在源文件中没发现"END",则到文件结束)的所有行。
既然在单独的文件中编写 sed 脚本,我们可以利用附加、插入和更改行命令。这些命令将在当前行之后插入一行,在当前行之前插入一行,或者替换模式空间中的当前行。它们也可以用来将多行插入到输出。插入行命令用法如下:
1 2 | i\
This line will be inserted before each line
|
如果不为该命令指定地址,那么它将应用到每一行,并产生如下的输出:
1 2 3 4 5 6 7 8 | This line will be inserted before each line line 1 here This line will be inserted before each line line 2 here This line will be inserted before each line line 3 here This line will be inserted before each line line 4 here |
如果要在当前行之前插入多行,可以通过在前一行之后附加一个反斜杠来添加附加行,如下所示:
1 2 3 4 5 | i\ insert this line\ and this one\ and this one\ and, uh, this one too. |
附加命令的用法与之类似,但是它将把一行或多行插入到模式空间中的当前行之后。其用法如下:
1 2 | a\
insert this line after each line. Thanks!
|
另一方面,更改行命令将替换模式空间中的当前行,其用法如下:
1 2 | c\ You're history, original line! Muhahaha! |
因为附加、插入和更改行命令需要在多行输入,所以将把它们输入到一个文本 sed 脚本中,然后通过使用 '-f' 选项告诉 sed 执行它们。使用其它方法将命令传递给 sed 会出现问题。
本作品由 Yysfire 创作,采用进行许可。转载时请在显著位置标明本文永久链接:
http://yysfire.github.io/linux/sed-usage-summary.html