sed & awk 第二版学习(五)—— 高级 sed 命令

2024-09-20 08:48:06 浏览数 (2)

高级命令分成三个组:

  1. 处理多行模式空间(N、D、P)。
  2. 采用保持空间来保存模式空间的内容并使它可用于后续的命令(H、h、G、g、x)。
  3. 编写使用分支和条件指令的脚本来更改控制流(:、b、t)。

高级命令改变执行或控制的流程顺序。sed 脚本中正常的控制流为:一行被读入模式空间并用脚本中的每个命令逐个应用于那一行;当到达脚本底部时,输出这一行并且清空模式空间;然后新行被读入模式空间,并且控制被转移回脚本顶端。

一、多行模式空间

模式匹配是面向行的。像 grep 这样的程序尝试在单个输入行上匹配一个模式,这就使它很难匹配一个在行尾处开始,并在下一行的开始处结束的短语或句子。sed 能查看模式空间的多个行,这就允许模式扩展到多行上。

1. 追加下一行

多行下一行(N)命令通过读取新的行,并将它添加到模式空间的现有内容之后来创建多行模式空间。模式空间最初的内容和新的输入之间用换行符分隔。在模式空间中嵌入的换行符可以利用转移序列“n”来匹配。在多行模式空间中,元字符“^”匹配模式空间中的第一个字符,而不匹配换行符后面的字符。同样,“$”只匹配模式空间中最后的换行符,而不匹配任何嵌入的换行符。

下面的例子是将“Owner and Operator Guide”换成“Installation Guide”。示例文件 sample 内容如下:

代码语言:javascript复制
Consult Section 3.1 in the Owner and Operator
Guide for a description of the tape drives
available on your system.

脚本如下:

代码语言:javascript复制
/Operator$/{N;s/Owner and OperatornGuide/Installation Guide/g}

执行结果:

代码语言:javascript复制
$ sed -e '/Operator$/{N;s/Owner and OperatornGuide /Installation Guiden/g}' sample
Consult Section 3.1 in the Installation Guide
for a description of the tape drives
available on your system.

本例中知道行在哪里被拆分成两行,因此知道在哪里指定嵌入的换行符。如果“Owner and Operator Guide”在不同的位置被分成多行呢?扩展的测试文件 sample 内容如下:

代码语言:javascript复制
Consult Section 3.1 in the Owner and Operator
Guide for a description of the tape drives
available on your system.

Look in the Owner and Operator Guide shipped with your system.

Two manuals are provided including the Owner and
Operator Guide and the User Guide.

The Owner and Operator Guide is shipped with your system.

修改后的脚本文件 sedscr 内容如下:

代码语言:javascript复制
s/Owner and Operator Guide/Installation Guide/
/Owner/{
N
s/ *n/ /
s/Owner and Operator Guide */Installation Guide
/
}

执行结果:

代码语言:javascript复制
$ sed -f sedscr sample
Consult Section 3.1 in the Installation Guide
for a description of the tape drives
available on your system.

Look in the Installation Guide shipped with your system.

Two manuals are provided including the Installation Guide
and the User Guide.

The Installation Guide is shipped with your system.

第一行脚本替换不换行的文本“Owner and Operator Guide”,如果没有这一行替换,执行结果如下:

代码语言:javascript复制
$ sed -f sedscr sample
Consult Section 3.1 in the Installation Guide
for a description of the tape drives
available on your system.

Look in the Installation Guide
shipped with your system. 
Two manuals are provided including the Installation Guide
and the User Guide.

The Owner and Operator Guide is shipped with your system.

和之前的结果比较,有两个明显的问题:一是第二段原来是一行,现在在 shipped 前多个一个换行符,而整段后面的换行符没有了;第二个是最后一句没有按预期进行替换。第一个问题的原因是这一行匹配“Owner”,因此将下一行(空行)追加到模式空间。替换命令删除嵌入的换行符,结果就是原来的空行受到影响消失了。第二个问题的原因是,当最后一行匹配“Owner”,执行 N 时,没有另外的输入行被读取,所以 sed 没有执行后续的替换命令。

为了修正第二个问题,按如下方式使用 N 命令应该是安全的:

代码语言:javascript复制
$!N

但这样又会多引入一次第一个问题。最好的解决方案就是当这个模式能在一行上匹配时避免读取下一行。这就是第一条指令尝试匹配所有出现在一行上的文本字符串的原因。

2. 多行删除

多行删除命令(D)删除模式空间中直到第一个嵌入换行符的这部分内容。它不会导致读入新的输入行,而是返回到脚本顶端,将这些指令应用于模式空间剩余的内容。下面的例子查找一系列空行并输出单个空行,分别使用删除命令(d)和多行删除命令(D)实现,以示区别。测试文件 sample 内容如下:

代码语言:javascript复制
This line is followed by 1 blank line.

This line is followed by 2 blank lines.


This line is followed by 3 blank lines.

This line is followed by 4 blank lines.


This is the end.

使用删除命令(d)的执行结果如下:

代码语言:javascript复制
$ sed '/^$/{N;/^n$/d}' sample
This line is followed by 1 blank line.

This line is followed by 2 blank lines.
This line is followed by 3 blank lines.

This line is followed by 4 blank lines.
This is the end.

当遇到一个空行时,下一行就追加到模式空间中,然后尝试匹配嵌入的换行符。注意定位元字符“^”和“$”分别匹配模式空间的开始处和结束处。当有偶数个空行时,所有的空行都会被删除;当有奇数个空行时,有一行被保留下来。这是因为删除命令(d)清除的是整个模式空间。一旦遇到第一个空行,就读入下一行,然后两行都被删除。如果遇到第三个空行,并且下一行不为空,那么删除命令就不会被执行,因此空行被输出。

如果使用多上删除命令(D),就能得到想要的结果:

代码语言:javascript复制
$ sed '/^$/{N;/^n$/D}' sample
This line is followed by 1 blank line.

This line is followed by 2 blank lines.

This line is followed by 3 blank lines.

This line is followed by 4 blank lines.

This is the end.

多行删除命令完成工作的原因是,当遇到两个空行时,D 命令只删除两个空行中的第一个。下次遍历该脚本时,这个空行将导致下一行被读入模式空间。如果那行不为空,那么两行都输出,因此确保输出一个空行。换句话说,当模式空间中有两个空行时,只有第一个空行被删除。当一个空行后面跟有文本时,模式空间可以正常输出。

3. 多行打印

多行打印(P)命令输出多行模式空间的第一部分,直到第一个嵌入的换行符为止,通常与 -n 选项联合使用。P 命令经常出现在 N 命令之后和 D 命令之前,这三个命令能建立一个输入/输出循环,用来维护两行的模式空间,但是一次只输出一行。这个循环的目的是只输出模式空间的第一行,然后返回到脚本的顶端将所有命令应用于模式空间的第二行。没有这个循环,当执行脚本中的最后一个命令时,模式空间中的这两行都将被输出。

下面看一个例子,如果发现:

代码语言:javascript复制
UNIX
System

则将它变成:

代码语言:javascript复制
UNIX Operating
System

示例文件 sample 内容如下:

代码语言:javascript复制
Here are examples of the UNIX
System. Where UNIX
System appears, it should be the UNIX
Operating System.

脚本如下:

代码语言:javascript复制
/UNIX$/{N;s/nSystem/ Operating &/;P;D}

执行结果:

代码语言:javascript复制
$ sed '/UNIX$/{N;s/nSystem/ Operating &/;P;D}' sample
Here are examples of the UNIX Operating 
System. Where UNIX Operating 
System appears, it should be the UNIX
Operating System.

完整的执行流程如下表所示。

循环

模式空间内容

命令

输出

1

Here are examples of the UNIX

N

Here are examples of the UNIXnSystem. Where UNIX

s/nSystem/ Operating &/

Here are examples of the UNIX Operating nSystem. Where UNIX

P

Here are examples of the UNIX Operating

Here are examples of the UNIX Operating nSystem. Where UNIX

D

2

System. Where UNIX

N

System. Where UNIXnSystem appears, it should be the UNIX

s/nSystem/ Operating &/

System. Where UNIX Operating nSystem appears, it should be the UNIX

P

System. Where UNIX Operating

System. Where UNIX Operating nSystem appears, it should be the UNIX

D

3

System appears, it should be the UNIX

N

System appears, it should be the UNIXnOperating System.

s/nSystem/ Operating &/

System appears, it should be the UNIXnOperating System.

P

System appears, it should be the UNIX

System appears, it should be the UNIXnOperating System.

D

4

Operating System.

(最后一行默认输出) Operating System.

第一行匹配模式 UNIX$ 后,N 命令将一个新的输入行追加到模式空间的当前行。在替换命令应用于多行模式空间之后,模式空间的第一部分被 P 命令输出,然后被 D 命令删除。这意味着当前行被输出并且新的行成为当前行。D 命令阻止脚本到达底部,如果没有 D 命令,默认脚本到达底部,输出两行并清除模式空间的内容。D 命令保护了模式空间的第二部分,并将控制转移到脚本顶端,在顶端所有的编辑命令都可以被应用于一行,从而开始下一循环。

替换命令匹配“nSystem”,并且用“ Operating nSystem”取代它。保留换行符很重要,否则模式空间中就只有一行。注意 P 命令和 D 命令的顺序。

循环进行到最后一行时,不匹配模式 UNIX$,因此不执行后面的一系列命令,而是默认输出此行。

二、保持空间

模式空间是容纳当前输入行的缓冲区。还有一个称为保持空间(hold space)的预留(set-aside)缓冲区。模式空间的内容可以复制到保持空间,保持空间的内容也可以复制到模式空间。有一组命令用于在保持空间和模式空间之间移动数据。保持空间用于临时存储,单独的命令不能寻址保持空间或者更改它的内容。保持空间最常见的用途是,当改变模式空间中的原始内容时,用于保留当前输入行的副本。影响模式空间的命令如下表所示。

命令

缩写

功能

Hold

h或H

将模式空间的内容复制或追加到保持空间

Get

g或G

将保持空间的内容复制或追加到模式空间

Exchange

x

交换保持空间和模式空间的内容

这些命令中的每一条都可以利用一个地址来指定一行或行范围。hole(h、H)命令将数据移至保持空间,而 get(g、G)命令将保持空间的数据移回到模式空间。同一命令的大小写字母的功能差别是,小写字母命令改写目的缓冲区的内容,而大写字母追加缓冲区的现有内容。

hold 命令用模式空间的内容取代保持空间的内容。get 命令用保持空间的内容取代模式空间的内容。Hold 命令在保持空间的内容之后放置一个换行符,后面跟随模式空间的内容。即使保持空间是空的,换行符也被追加到保持空间中。Get 命令在模式空间的内容之后放置一个换行符,后面跟随保持空间的内容。

交换命令只是交换两个缓存区的内容。

1. 交换奇偶行

下面看一个使用保持空间的例子。样本文件 sample 内容如下:

代码语言:javascript复制
1
2
11
22
111
222

目标是颠倒以 1 开始的行和以 2 开始的行的顺序。执行结果如下:

代码语言:javascript复制
$ sed '/1/{h;d;};/2/{G}' sample
2
1
22
11
222
111

匹配“1”的任何行都被复制到保持空间并且从模式空间中删除。控制转移到脚本的顶端并且不打印那一行。当读取下一行时,它匹配模式“2”,因此将已经复制到保持空间的行追加到模式空间之后,然后到达脚本底端,两行都被打印出来。

在 h 命令后面跟 d 命令是一种常见的搭配。没有 d 命令,控制将一直进行到脚本底部,并且默认模式空间的内容将被输出。

这个脚本使用硬编码,逻辑性很差。如果一行匹配第一个指令并且下一行匹配第二个指令失败,那么第一行就不会被输出。更为通用的交换奇偶行的 sed 命令如下:

代码语言:javascript复制
sed -n '$!{h;n;G};p' sample

执行的流程是:

  1. 将当前输入行复制到保持空间。
  2. 取下一个输入行到模式空间,然后将保持空间的内容,追加到模式空间中。
  3. 打印模式空间,开始下一循环。

$! 的意思是最后一行不操作保持空间,这样如果是奇数行时最后一行也会正常输出。

2. 单词转大写

需求是将模式所匹配的内容转为大写,类似于下面的样子:

代码语言:javascript复制
s/find the Match statement/find the MATCH statement/g

转换命令可以进行小写字母到大写字母的转换,但它将转换应用于整个行。使用保持空间可以完成以上任务,因为可以用保持空间存储输入行的备份而将需要转换的部分独立出来,然后在模式空间进行转换。样本文件 sample 内容如下:

代码语言:javascript复制
find the Match statement
Consult the Get statement.
using the Read statement to retrieve data

脚本文件 sedscr 内容如下:

代码语言:javascript复制
# 将语句的名字变成大写形式
/the .* statement/{
h
s/.*the (.*) statement.*/1/
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
G
s/(.*)n(.*the ).*( statement.*)/213/
}

脚本执行结果如下:

代码语言:javascript复制
$ sed -f sedscr sample
find the MATCH statement
Consult the GET statement.
using the READ statement to retrieve data

地址将过程限制在“the .* statement”的行上。执行的流程是: 1. 用 h 命令将当前输入行复制到保持空间,之后模式空间和保持空间的内容完全相同。如第一行:

代码语言:javascript复制
模式空间:find the Match statement
保持空间:find the Match statement

2. 用 s 命令从行中提取语句的名字,并且用它来取代整个行:

代码语言:javascript复制
模式空间:Match
保持空间:find the Match statement

3. 用 y 命令将每个小写字母转成大写字母:

代码语言:javascript复制
模式空间:MATCH
保持空间:find the Match statement

4. 用 G 命令将保持空间中的行追加到模式空间:

代码语言:javascript复制
模式空间:MATCHnfind the Match statement
保持空间:find the Match statement

5. 用 s 命令匹配模式空间的三个不同部分:1)嵌入的换行符之前的所有的字符;2)从嵌入的换行符开始直到后面跟有一个空格的“the”,且包括 the 在内的所有字符;3)以空格并且后面跟有“statement”开始直到模式空间结尾处所有的字符。当语句的名字出现在原始行中时,被匹配而不被保存(没在括号中)。这个命令的替换部分回调被保存的部分,并按不同的顺序重新组合它们,在“the”和“statement”之间放置大写的单词:

代码语言:javascript复制
模式空间:find the MATCH statement
保持空间:find the Match statement

从这个脚本可以看出,灵活地使用保持空间对于隔离和操作输入行的某部分内容很有用。

三、高级的流控制命令

分支(b)和测试(t)命令将脚本中的控制转移到包含特殊标签的行。如果没有指定标签,则将控制转移到脚本的结尾处。分支命令用于无条件转移,测试命令用于有条件转移,它只有当替换命令改变当前行时才会执行。

标签是任意长度的字符序列,它本身占据一行并以冒号开始:

代码语言:javascript复制
:mylabel

在冒号和标签之间不允许有空格。当在分支命令或测试命令中指定标签时,在命令和标签之间允许有空格,但不要在标签后面插入空格:

代码语言:javascript复制
b mylabel

1. 分支命令

b 命令用于在脚本中将控制权转移到另一行,语法如下:

代码语言:javascript复制
[address]b[label]

label 是可选的,如果没有给出 label,控制就被转移到脚本的结尾处。如果有 label,就继续执行标签后面的行。

分支命令可用于将一组命令作为一个过程来执行,这个过程可以从脚本的主体中重复调用。它也可以用于避免执行某个基于一个模式匹配的过程,例如:

代码语言:javascript复制
/^.ES/,/^.EE/b
s/^"/``/
s/"$/''/
s/"? /''? /g
.
.
.
s/\(em\^"/\(em``/g
s/"\(em/''\(em/g
s/\(em"/\(em``/g
s/@DQ@/"/g

当匹配以 .ES 和 .EE 作为行头的一个段时,不执行后面的一系列替换。因为没有为 b 命令提供标签,所以跳转到脚本的结尾。

通过使用 ! 并组合一组命令可以实现类似的效果。在应用中对分支命令使用 ! 的优点是,可以更容易地指定要避免的多个情况。! 符号可以应用于单个命令,也可以应用于紧随其后的包围在大括号中的一组命令。分支赋予了脚本几乎不受限的跳转控制能力。

下面的脚本使用分支命令创建循环:

代码语言:javascript复制
:top
command1
command2
/pattern/b top
command3

一旦读取一个输入行,command1 和 command2 就会应用于那一行;然后,如果模式空间的内容与指定的模式匹配,那么控制就被转移到跟在标签“top”后面的行,这意味着 command1 和 command2 将被再次执行。只有当模式不匹配时才执行 command3。所有这三个命令都会被执行,尽管前两个命令可以多次执行。

现在将脚本修改为:

代码语言:javascript复制
command1
/pattern/b end
command2
:end
command3

它首先执行 command1,然后如果模式匹配,控制权就转移到跟在标签“end”后面的行,这意味着跳过了 command2。在所有情况下,都会执行 command1 和 command3。

下面的脚本只执行 command2 或 command3 中的一个,但不会两者都执行:

代码语言:javascript复制
command1
/pattern/b dothree
command2
b
:dothree
command3

脚本中有两个分支命令。第一个分支命令在模式匹配时将控制转移到 command3。如果模式不匹配,则执行 command2。跟在 command2 后面的分支命令将控制转移到脚本结尾处,绕过了command3。第一个分支命令执行的条件是匹配模式,第二个命令执行没有条件。

2. 测试命令

如果在当前匹配地址的行上进行了成功的替换,那么 t 命令就转到标签或脚本结尾处,因此它隐含了一个条件分支。t 命令语法如下:

代码语言:javascript复制
[address]t[label]

如果没有给出标签 label,控制被转移到脚本结尾处。如果提供了标签 label,那么就转移到标签后面的行继续执行。

来看下面的示例脚本:

代码语言:javascript复制
/.Rh 0/{
s/"(.*)" "(.*)" "(.*)"/"1" "2" "3"/
t
s/"(.*)" "(.*)"/"1" "2"/
t
s/"(.*)"/"1"/
}

这个脚本尝试连续地匹配三个参数、两个参数、一个参数的每种情况,并且在成功进行替换时,避免执行进一步的替换。一旦一个替换被执行,其后的 t 命令就使控制到达脚本的末尾。如果在 .Rh 行上有三个参数,那么第一个替换命令之后的 t 命令为真,sed 继续执行下一个输入行。如果少于三个参数,就不会执行第一个替换,t 命令为假。则将尝试执行下一个替换命令。这个过程将一直重复到所有的可能性都用完为止。

t 命令提供功能类似于 C 语言或 shell 中 case 语句的功能,即测试每种情况并且当一种情况为真时,退出结构。如果上面的脚本是一个更大的脚本中的一部分,可以使用标签(如形象地命名为“break”)直接转移到分组命令的末尾,然后继续执行其它一些命令。

代码语言:javascript复制
/.Rh 0/{
s/"(.*)" "(.*)" "(.*)"/"1" "2" "3"/
t break
.
.
.
}
:break
more commands

四、查找跨行短语

本节介绍一个名为 phrase 的 shell 脚本,用于寻找一系列出现在两行上的多个单词,用到了几乎所有的 sed 的高级结构。与 grep 一样,该程序的一个必不可少的部分是只打印匹配模式的行,但它不是使用 -n 选项来抑制默认的行输出,而是构建了一个输入/输出循环,用来控制何时输出或不输出。

这个脚本被设计为可以接受命令行参数。第一个参数是搜索模式,所有其它参数都被解释为文件名。整个脚本如下:

代码语言:javascript复制
#! /bin/bash
# phrase -- 查找跨行单词
# $1 = 查找的字符串; 剩余参数 = 文件名
search=$1
shift
for file
do
sed '
/'"$search"'/b
N
h
s/.*n//
/'"$search"'/b
g
s/ *n/ /
/'"$search"'/{
g
b
}
g
D' $file
done

名为 search 的 shell 变量被指定为命令行上的第一个参数,它表示搜索模式。这里用一对双引号把变量括起来,然后再用单引号括住它。注意 sed 脚本本身被包围在单引号中,这可以防止对 shell 特殊的字符被解释。在单引号对中的双引号序列确保被包围的参数首先被 shell 求值,然后再由 sed 对 sed 脚本进行处理。

之后的 shift 用于改变位置参数的位置。‌具体来说,shift 命令会将所有的位置参数向左移动一个位置,即第一个参数 1 会被丢弃,原来的第二个参数 2 变成第一个参数,原来的第三个参数

for file 是一个 for 循环的开始。在这里,for 循环后面直接跟了变量名 file,而没有显式地列出要迭代的值列表。这种用法在 shell 脚本中意味着循环将遍历位置参数(即 1、2、

for 循环体中是作用于文件的 sed 脚本。该 sed 脚本在三个不同的点尝试匹配搜索字符串,每一个都标有用于查找搜索模式的地址。脚本的第一行寻找在一行出现搜索模式的行:

代码语言:javascript复制
/'"$search"'/b

如果搜索模式匹配这一行,那么不带标签的分支命令就将控制转移到脚本的底部并打印该行。使用 sed 的正常控制流,下一个输入行被读入模式空间,并且控制返回到脚本的顶端。每次尝试匹配模式时,都可以用相同的方式使用分支。

如果一个输入行不匹配这个模式,则开始下一个过程来创建多行模式空间。新行本身可能匹配这一搜索字符串。这里的策略是,如果第二行匹配模式,则输出第二行。

代码语言:javascript复制
N
h
s/.*n//
/'"$search"'/b

N 命令将下一个输入行追加到模式空间。h 命令把两行的模式空间复制到保持空间,之所以这样做是为了保护原始内容的完整,因为下面的动作将更改模式空间。在寻找模式之前,使用替换命令删除嵌入的换行符前面的行以及该嵌入的换行符,目的是只尝试匹配第二行。如果成功,那么不带标签的分支命令就将控制转移到脚本的底部并打印该行。否则,再尝试跨越两行进行匹配:

代码语言:javascript复制
g
s/ *n/ /
/'"$search"'/{
g
b
}

这个的 g 命令从保持空间获取原始的两行的一个备份,并改写模式空间中处理过的行。替换命令用一个空格取代嵌入的换行符和它前面的任意空格。然后尝试匹配搜索模式。如果匹配成功,再次使用 g 命令从保持空间(保护了换行符和其前面的空格)中得到副本,并用不带标签的 b 命令转到脚本底部以打印它。

只有当模式不匹配时才执行脚本的最后一部分:

代码语言:javascript复制
g
D

这里的 g 命令从保持空间获取保持换行符的副本。D 命令删除模式空间中的第一行并且将控制转移回脚本的顶端。这里只删除模式空间的第一部分,而不是清空它,因为在读取另一个输入行之后,有可能要进行跨两行的匹配。

下面是程序在样本文件上运行的结果:

代码语言:javascript复制
$ phrase "the procedure is followed" sect3
If a pattern is followed by a f(CW!fP, then the procedure
is followed for all lines that do not match the pattern.
so that the procedure is followed only if there is no match.

(这个脚本貌似不错,逻辑也很清晰。但是,最初的需求是要找出所有匹配某个字符串的所有行,包括跨行的匹配。如果以满足这个需求来衡量,该脚本存在很大的缺陷,甚至可以说根本不成立。)

假设 sect3 文件的内容如下:

代码语言:javascript复制
aaa
bbb aaa bbb           
123
aaa  
bbb aaa
      bbb
123
aaa bbb aaa     
     bbb
123

希望找出所有包含“aaa bbb”的行,包括跨行匹配,预期的结果是:

代码语言:javascript复制
aaa
bbb aaa bbb           
aaa  
bbb aaa
      bbb
aaa bbb aaa     
     bbb

注意:行尾或行头可能有0个1个或多个 [ t] 的情况,如果统一替换为一个空格后,能匹配字符串,就输出行。

运行该脚本的结果为:

代码语言:javascript复制
$ phrase "aaa bbb" sect3 
bbb aaa bbb
aaa 
bbb aaa
aaa bbb aaa
123

不但缺少了匹配的行(Omissions,遗漏),还多输出了不匹配的行(False alarms,假报警)。下表所示的执行流程揭示了输出错误的原因。

循环次数

模式空间

保持空间

操作

1

aaa aaanbbb aaa bbb aaanbbb aaa bbb bbb aaa bbb 空

aaanbbb aaa bbb aaanbbb aaa bbb aaanbbb aaa bbb

N h s/.*n// (匹配)b;跳到末尾;输出 bbb aaa bbb

2

123 123naaa 123naaa Aaa 123naaa 123 aaa 123naaa aaa

aaanbbb aaa bbb aaanbbb aaa bbb 123naaa 123naaa 123naaa 123naaa 123naaa 123naaa

N h s/.*n// g s/ *n/ / g D

3

aaa aaanbbb aaa aaanbbb aaa bbb aaa aaanbbb aaa aaa bbb aaa aaanbbb aaa 空

123naaa 123naaa aaanbbb aaa aaanbbb aaa aaanbbb aaa aaanbbb aaa aaanbbb aaa aaanbbb aaa

N h s/.*n// g s/ *n/ / (匹配)g b;跳到末尾;输出 aaanbbb aaa

4

bbb bbbn123 bbbn123 123 bbbn123 bbb 123 bbbn123 123

aaanbbb aaa aaanbbb aaa bbbn123 bbbn123 bbbn123 bbbn123 bbbn123 bbbn123

N h s/.*n// g s/ *n/ / g D

5

123 123naaa bbb aaa 123naaa bbb aaa aaa bbb aaa 空

bbbn123 bbbn123 123naaa bbb aaa 123naaa bbb aaa 123naaa bbb aaa

N h s/.*n// (匹配)b;跳到末尾;输出 aaa bbb aaa

6

bbb bbbn123 bbbn123 123 bbbn123 bbb 123 bbbn123 123

123naaa bbb aaa 123naaa bbb aaa bbbn123 bbbn123 bbbn123 bbbn123 bbbn123 bbbn123

N h s/.*n// g s/ *n/ / g D

7

123

bbbn123

N 已到最后一行,命令失败,输出 123,清空模式空间和保持空间

用 sed 实现这个功能非常复杂,用 awk 会相对容易些:

代码语言:javascript复制
search="aaa bbb" 
awk '  
{  
    # 检查当前行是否包含搜索字符串
    if ($0 ~ /'"$search"'/) { 
        cur = $0
        pre = prev_line
        gsub(/'"$search"'/,"",$0)
        sub(/[ t] $/, "", prev_line);      # 删除前一行尾部空格
        sub(/^[ t] /, "", $0);             # 删除当前行头部空格
        if (prev_line " " $0 ~ /'"$search"'/ && !seen[NR-1]) {
            print NR-1"t"pre
            seen[NR-1] = 1
        }
        prev_line = cur  
        print NR"t"cur  # 输出当前行 
        seen[NR] = 1		
        next       # 跳过剩余的代码,继续处理下一行  
    }  
	
    # 如果这不是第一行,则检查当前行与前一行拼接后是否包含搜索字符串 
    cur = $0	
    pre = prev_line
    gsub(/'"$search"'/,"",prev_line)
    sub(/[ t] $/, "", prev_line);         # 删除前一行尾部空格
    sub(/^[ t] /, "", $0);                # 删除当前行头部空格
 
    if (NR > 1 && prev_line " " $0 ~ /'"$search"'/) {  
        # 如果之前没有输出过前一行,则输出它  
        if (!seen[NR-1]) {  
            print NR-1"t"pre  
            seen[NR-1] = 1  # 标记前一行已输出  
        }  
        # 总是输出当前行(因为它与前一行的组合包含了搜索字符串)  
        print NR"t"cur
        seen[NR] = 1
    }  
  
    # 保存当前行以便与下一行进行比较  
    prev_line = cur  
}  
  
# 处理最后一行(但在这个例子中,我们不需要特别的逻辑,因为我们已经检查了所有行)  
END {  
    # 注意:在这个例子中,我们不需要在END块中做任何特别的事情  
    # 因为我们已经处理了每一行  
}  
' sect3

执行结果:

代码语言:javascript复制
1	aaa
2	bbb aaa bbb		   
4	aaa  
5	bbb aaa
6		  bbb
8	aaa bbb aaa 	
9		 bbb

0 人点赞