sed 多行模式、分支及循环

2024-07-13 09:03:47 浏览数 (3)

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 即是标签。

1 人点赞