sed 默认每次只处理一行数据,除非使用 H、G 或者 N 等命令创建多行模式,每行之间用换行符分开。本篇将解释适用于多行模式的 sed 命令。在处理多行模式时,要记住 ^ 只匹配该模式的开头,即最开始一行的开头,且 $ 只匹配该模式的结尾,即最后一行的结尾。
1. 读取下一行数据并附加到模式空间(命令 N)
就像 H 和 G 一样,大写的命令只会追加内容而不是替换内容。命令 N 从输入文件中读取下一行并追加到模式空间,而不是替换模式空间。 小写命令 n 打印当前模式空间的内容,并清空模式空间,从输入文件中读取下一行到模式空间,然后继续执行后面的命令。大写命令 N 不会打印模式空间内容,也不会清除模式空间内容,而是在当前模式空间内容后加上换行符 n,并且从输入文件中读取下一行数据,追加到模式空间中,然后继续执行后面的命令。 在下面的例子中,以冒号分隔,打印员工名称和职位。示例文本 empnametitle.txt 的内容如下:
代码语言:javascript复制John Doe
CEO
Jason Smith
IT Manager
Raj Reddy
Sysadmin
Anand Ram
Developer
Jane Miller
Sales Manager
在这个文件中,每个员工的姓名和职位位于连续的两行内。下面的命令在同一行上打印以冒号分割的员工名称和职位。
代码语言:javascript复制#sed -e '{N;s/n/:/}' empnametitle.txt
John Doe:CEO
Jason Smith:IT Manager
Raj Reddy:Sysadmin
Anand Ram:Developer
Jane Miller:Sales Manager
#
本例中:
- N 追加换行符 n 到当前模式空间(员工名称)的最后,然后从输入文件读取下一行数据并追加进来。因此,当前模式空间内容变为“员工名称n员工职位”。
- s/n/:/ 把换行符 n 替换为冒号,作为员工名称和员工职位的分隔符。
下面的例子在每行文本行首添加行号。示例文件 employee.txt 文件内容如下:
代码语言:javascript复制101,John Doe,CEO
102,Jason Smith,IT Manager
103,Raj Reddy,Sysadmin
104,Anand Ram,Developer
105,Jane Miller,Sales Manager
本例演示在打印 employee.txt 文件内容的同时,以文本方式显示每行的行号:
代码语言:javascript复制#sed -e '=' employee.txt | sed '{N;s/n/ /}'
1 101,John Doe,CEO
2 102,Jason Smith,IT Manager
3 103,Raj Reddy,Sysadmin
4 104,Anand Ram,Developer
5 105,Jane Miller,Sales Manager
#
命令 = 在每个原始行的前面添加一行行号:
代码语言:javascript复制#sed -e '=' employee.txt
1
101,John Doe,CEO
2
102,Jason Smith,IT Manager
3
103,Raj Reddy,Sysadmin
4
104,Anand Ram,Developer
5
105,Jane Miller,Sales Manager
#
命令 N 在当前模式空间后面加上 n(当前模式空间内容为行号),然后读取下一行,并追加到模式空间中。因此,模式空间内容变为“行号n原始内容”。然后用 s/n/ / 把换行符 n 替换成空格。
2. 打印多行模式中的第一行(命令 P)
大写的 D、P 功能和小写的 d、p 非常相似,但它们在多行模式中有特殊的功能。小写的命令 p 打印模式空间的内容,大写的 P 也打印模式空间内容,直到它遇到换行符 n。
下面的例子将打印示例文本 empnametitle.txt 中所有管理者的名称:
代码语言:javascript复制#sed -n -e 'N' -e '/Manager/P' empnametitle.txt
Jason Smith
Jane Miller
#
- N 追加换行符 n 到当前模式空间(员工名称)的最后,然后从输入文件读取下一行数据并追加进来。因此,当前模式空间内容变为“员工名称n员工职位”。
- /Manager/P 打印匹配 Manager 的行,并且只打印员工名称。
3. 删除多行模式中的第一行(命令 D)
小写命令 d 会删除模式空间内容,然后读取下一条记录到模式空间,并忽略其后的命令,从头开始下一次循环。大写命令 D,既不会读取下一条记录,也不会完全清空模式空间(除非模式空间内只有一行)。它只会:
- 删除模式空间的部分内容,直到遇到换行符 n。
- 忽略后续命令,在当前模式空间中从头开始执行命令。
假设有下面内容的文件 empnametitle-with-commnet.txt,每个员工的职位都用 @ 包含起来作为注释。需要注意的是,有些注释是跨行的,如 @Information Technology officer@ 就跨了两行。
代码语言:javascript复制John Doe
CEO @Chief Executive Officer@
Jason Smith
IT Manager @Infromation Technology
Officer@
Raj Reddy
Sysadmin @System Administrator@
Anand Ram
Developer @Senior
Programmer@
Jane Miller
Sales Manager @Sales
Manager@
现在的目标是去掉文件里的注释:
代码语言:javascript复制#sed -e '/@/{N;/@.*@/{s/@.*@//;P;D}}' empnametitle-with-commnet.txt
John Doe
CEO
Jason Smith
IT Manager
Raj Reddy
Sysadmin
Anand Ram
Developer
Jane Miller
Sales Manager
#
注意这里不能加 -n 选项,因为 P 命令打印的只有职位(因为只有职位带注释):
代码语言:javascript复制#sed -n -e '/@/{N;/@.*@/{s/@.*@//;P;D}}' empnametitle-with-commnet.txt
CEO
IT Manager
Sysadmin
Developer
Sales Manager
#
也可把上述命令写到 sed 脚本中然后执行:
创建内容如下的脚本文件 D-upper.sed
代码语言:javascript复制#!/bin/sed -f
/@/{
N
/@.*@/{s/@.*@//;P;D}
}
修改脚本文件的模式为可执行
代码语言:javascript复制chmod u x D-upper.sed
执行脚本
代码语言:javascript复制#./D-upper.sed empnametitle-with-commnet.txt
John Doe
CEO
Jason Smith
IT Manager
Raj Reddy
Sysadmin
Anand Ram
Developer
Jane Miller
Sales Manager
#
本例中:
- /@/{ 是外传循环。sed 搜索包含 @ 符号的任意行,如果找到,就执行后面的命令;如果没有找到,则读取下一行。
- N 从输入文件读取下一行,并追加到模式空间。
- /@.*@/ 在模式空间中搜索匹配 /@.*@/ 的模式,即以 @ 开头和结尾的任何内容。
- s/@.*@//;P;D 这个中的替换命令把整个注释替换为空(相当于删除)。P 打印模式空间中的第一行,然后 D 删除模式空间中的第一行。然后从头开始执行命令,即不读取下一条记录,又返回到 /@/ 处执行命令。
完整的执行流程如下表所示。
循环次数 | 模式空间 | 操作 |
---|---|---|
1 | John Doe 空 | 打印 John Doe => |
2 | CEO @Chief Executive Officer@ CEO @Chief Executive Officer@nJason Smith CEO nJason Smith CEO nJason Smith Jason Smith 空 | N => s/@.*@// => P CEO => D => 打印 Jason Smith => |
3 | IT Manager @Infromation Technology IT Manager @Infromation TechnologynOfficer@ IT Manager IT Manager 空 | N => s/@.*@// => P IT Manager => D => |
4 | Raj Reddy 空 | 打印 Raj Reddy => |
5 | Sysadmin @System Administrator@ Sysadmin @System Administrator@nAnand Ram Sysadmin nAnand Ram Sysadmin nAnand Ram 空 | N => s/@.*@// => P Sysadmin => D => |
6 | Developer @Senior Developer @SeniornProgrammer@ Developer Developer 空 | N => s/@.*@// => P Developer => D => |
7 | Jane Miller 空 | 打印 Jane Miller => |
8 | Sales Manager @Sales Sales Manager @SalesnManager@ Sales Manager Sales Manager 空 | N => s/@.*@// => P Sales Manager => D => |
对于这个例子而言,可以有以下更简单的写法:
代码语言:javascript复制#sed -e '/@/{N;s/@.*@//}' empnametitle-with-commnet.txt
John Doe
CEO
Jason Smith
IT Manager
Raj Reddy
Sysadmin
Anand Ram
Developer
Jane Miller
Sales Manager
#
因为注释固定占一行或两行,所以匹配到 @ 就执行 N,取下一行数据到模式空间,然后整体替换掉注释内容即可。这种写法与上面的 P D 写法输出相同,但执行逻辑不同。
完整的执行流程如下表所示。
循环次数 | 模式空间 | 操作 |
---|---|---|
1 | John Doe 空 | 打印 John Doe => |
2 | CEO @Chief Executive Officer@ CEO @Chief Executive Officer@nJason Smith CEO nJason Smith 空 | N => s/@.*@// => 打印 CEO nJason Smith => |
3 | IT Manager @Infromation Technology IT Manager @Infromation TechnologynOfficer@ IT Manager 空 | N => s/@.*@// => 打印 IT Manager => |
4 | Raj Reddy 空 | 打印 Raj Reddy => |
5 | Sysadmin @System Administrator@ Sysadmin @System Administrator@nAnand Ram Sysadmin nAnand Ram 空 | N => s/@.*@// => 打印 Sysadmin nAnand Ram => |
6 | Developer @Senior Developer @SeniornProgrammer@ Developer 空 | N => s/@.*@// => 打印 Developer => |
7 | Jane Miller 空 | 打印 Jane Miller => |
8 | Sales Manager @Sales Sales Manager @SalesnManager@ Sales Manager 空 | N => s/@.*@// => 打印 Sales Manager => |
4. 循环和分支(命令 b 和 :label 标签)
使用标签和分支命令 b,可以改变 sed 的执行流程:
- :label 定义一个标签。
- b lable 执行该标签后面的命令。sed 会跳转到该标签,然后执行后面的命令。
- 命令 b 后面可以不跟任何标签,这种情况下,它会直接跳到 sed 脚本的结尾。
下面例子将把 empnametitle.txt 文件中的员工名称和职位合并到一行内,字段之间以冒号 : 分隔,并且在管理者的名称前面加上一个星号 *。sed 脚本文件 label.sed 内容如下:
代码语言:javascript复制#!/bin/sed -nf
h;n;H;x
s/n/:/
/Manager/!b end
s/^/*/
:end
p
这个脚本中,鉴于之前的例子,已经知道 h;n;H;x 和 s/n/:/ 的作用了。下面是关于分支的操作:
- /Manager/!b end 的作用是如果行内不包含关键字 Manager,则跳转到 end 标签(可以任意设置想要的标签名称)。因此,只有匹配 Manager 的员工名称签名,才会执行 s/^/*/ 即在行首加星号 *。
- :end 即是标签。
给这个脚本加上可执行权限,然后执行:
代码语言:javascript复制#chmod u x label.sed
#./label.sed empnametitle.txt
John Doe:CEO
*Jason Smith:IT Manager
Raj Reddy:Sysadmin
Anand Ram:Developer
*Jane Miller:Sales Manager
#
h;n;H;x 可以用一个 N 替代,这样就不用使用保持空间了。而且这个例子只是为了说明命令 b 如何运行,但它不是必须的,等效的简单写法如下:
代码语言:javascript复制#sed -n -e 'N;s/n/:/g;/Manager/s/^/*/g;p;' empnametitle.txt
John Doe:CEO
*Jason Smith:IT Manager
Raj Reddy:Sysadmin
Anand Ram:Developer
*Jane Miller:Sales Manager
#
5. 使用命令 t 进行循环
命令 t 的作用是,如果前面的命令执行成功,那么就跳转到 t 指定的标签处,继续往下执行后续命令。否则,仍然继续正常的执行流程。 下面例子将把 empnametitle.txt 文件中的员工名称和职位合并到一行内,字段之间以冒号 : 分隔,并且在管理者的名称前面加上三个星号 *。其实只需把上个例子中的替换命令改为 s/^/***/ 即可达到该目的,这里仅仅是为了解释命令 t 是如何运行的。
创建脚本文件 label-t.sed,内容如下:
代码语言:javascript复制#!/bin/sed -nf
h;n;H;x
s/n/:/
:repeat
/Manager/s/^/*/
/***/! t repeat
p
然后给脚本文件加上可执行权限并执行:
代码语言:javascript复制#chmod u x label-t.sed
#./label-t.sed empnametitle.txt
John Doe:CEO
***Jason Smith:IT Manager
Raj Reddy:Sysadmin
Anand Ram:Developer
***Jane Miller:Sales Manager
#
本例中的下面代码执行循环:
代码语言:javascript复制:repeat
/Manager/s/^/*/
/***/! t repeat
- /Manager/s/^/*/ 的作用是如果匹配到 Manager,在行首加上星号 *
- /***/!t repeat 的作用是如果没有匹配到三个连续的星号 *,并且前面一行的替换命令成功执行了,则跳转到名为 repeat 的标签处。
- :repeat 即是标签。