第六章高级sed命令

本章的高级sed命令分为三大类:

  1. 多行模式空间命令:N、D、P(大写)
  2. 保持空间缓冲区命令:H/h、G/g、x
  3. 流程控制(分支与条件)::标签、b无条件分支、t条件测试分支

sed默认正常执行流程: 读一行输入 → 放入模式空间,从上到下依次执行脚本所有命令 → 脚本执行完毕,输出模式空间内容,清空模式空间,读取下一行,回到脚本开头。 本章所有高级命令都会打破这套默认流程,这也是难理解的根源。

一、多行模式空间

sed默认一次处理一行;多行命令允许模式空间存放多行文本,实现跨行匹配。

小写基础命令和大写多行命令对照

基础单行命令 多行版本 作用
n N 读取下一行
d D 删除
p P 打印

1. 追加下一行:N(大写Next)

N:读取下一行输入,追加到当前模式空间末尾,两行之间嵌入换行符\n;不会输出原有内容,继续向下执行后续脚本。

重点区分:

  • n(小写next):输出当前模式空间,清空,读入下一行;不会产生多行模式空间
  • N(大写Next):不输出,直接把下一行拼进模式空间,产生多行,行之间用\n分隔。

多行模式空间中元字符行为:

  • ^:只匹配模式空间最开头,不匹配内部\n之后的行首
  • $:只匹配模式空间最末尾,不匹配内部\n之前的行尾
  • 内部换行符用\n进行匹配

示例场景:短语被拆分成两行

原始文本片段

1
2
3
Consult Section 3.1 in the Owner and Operator
Guide for a description of the tape drives
available on your system.

需求:跨行把Owner and Operator Guide替换成Installation Guide

基础脚本:匹配行尾是Operator的行,读入下一行,匹配换行符

1
2
3
4
/Operator$/ {
N
s/Owner and Operator\nGuide/Installation Guide/
}

坑:替换匹配了\n,但没有输出换行,行会拼接成超长一行。 sed的替换字符串中不能直接写\n插入换行,必须反斜杠转义后回车换行。

  • 第一行末尾 Installation Guide\ → 反斜杠,然后敲回车键,这样可以插入换行符
  • 第二行只有一个 /,是 sed 替换命令的结束分隔符

修复版本,保留换行:

1
2
3
4
5
/Operator$/ {
N
s/Owner and Operator \nGuide /Installation Guide\
/
}
重要避坑:$!N

如果文件最后一行匹配上地址,执行N,但已经没有下一行可读,sed直接退出,本行都不会输出! 安全写法:$!N,只有不是最后一行的时候,才执行N。含义:$代表最后一行;!取反;不是最后一行,才执行 N

1
2
3
4
/Operator$/ {
    $!N
    s/Owner and Operator \nGuide /Installation Guide\
}

另外一个坑:如果目标短语本身就在同一行,不要贸然执行N,否则会把下一行无关内容拉进模式空间,破坏文本。解决思路先做一次单行的替换;只有单行匹配失败,再去执行 N 读取下一行

1
2
3
4
5
6
7
# 优先处理:完整短语在同一行的情况
s/Owner and Operator Guide/Installation Guide/
# 只有行内出现Owner,但是本行没有完整短语,才读取下一行
/Owner/ {
    $!N
    s/Owner and Operator \nGuide /Installation Guide\
}

2. 多行删除 D(大写Delete)

  • d(小写):清空整个模式空间,读新行,跳回脚本开头。
  • D(大写):找到模式空间里「第一个\n换行符」,把换行符 以及它前面所有字符全部删掉 ;换行后面剩下的内容留在模式空间,不读新行,直接跳回脚本开头重新跑脚本。

案例:把连续多个空行压缩为最多一个空行。
错误写法(用d)

1
2
3
4
/^$/{
N
/^\n$/d
}

缺陷:偶数个连续空行会全部删掉,奇数才保留一行,效果不符合预期。

正确写法(D多行删除)

1
2
3
4
/^$/{
N
/^\n$/D
}

逻辑:遇到空行,N读下一行;如果两行都是空\n,D删掉第一行空行;回到脚本顶端继续判断剩下那行空行。最终多个连续空行只保留1行。

3. 多行打印 P(大写Print)

  • p:打印模式空间全部内容
  • P:只打印模式空间中第一个嵌入换行符\n之前的第一部分。

使用前提:一般配合sed -n(关闭默认输出),或者脚本流程走不到脚本末尾(不会自动输出)的时候使用。

经典组合循环:N → s替换 → P → D

  1. N把下一行读进模式空间,变成两行
  2. 做跨行替换
  3. P输出模式空间的第一行
  4. D删掉第一行,剩下第二行留在模式空间,回到脚本顶端

效果:第二行保留在缓冲区,可以继续参与后续所有规则处理,实现滚动两行窗口。

示例:跨行匹配UNIX\nSystem替换
测试文本

1
2
3
Here are examples of the UNIX
System. Where UNIX
System appears, it should be the UNIX Operating System.

sed脚本

1
2
3
4
5
6
7
8
/UNIX$/{
    N
    /\nSystem/{
    s// Operating &/
    P
    D
    }
}

&不是代表下一行,是代表本次正则匹配捕获到的那一串字符既/\nSystem/
s// Operating &/,如果s的第一个//中间什么都不写:复用最近一次成功匹配的正则表达式。上一条命令是 /\nSystem/,刚刚匹配成功,匹配到的文本就是 \nSystem。等价完整写法:s/\nSystem/ Operating &/

学习案例:Scribe字体标签 @fl(...)跨行转换

需求:把scribe标记@fl(内容)转troff粗体\fB内容\fR;标记可能在同一行多次出现,也可能括号跨两行。

原始简单版本缺陷:

1
2
# 错误,.*贪婪匹配,一行多个括号会一次性匹配到最后一个)
s/@fl(\(.*\))/\\fB\1\\fR/g

修正单行匹配,用[^)]*匹配除右括号以外字符(非贪婪效果)

1
s/@fl(\([^)]*\))/\\fB\1\\fR/g

但是上面只处理单行,处理不了跨两行的@fl(xxx\nxxx)。 使用 N + P + D 循环处理跨行场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
#第一步:优先处理全部在同一行的 @fl(...)
s/@fl(\([^)]*\))/\\fB\1\\fR/g

#匹配:行里面有 @fl( ,但是本行找不到结束的 )
/@fl(.*/{
    N                   # $!N书上省略,读入下一行到模式空间;现在模式空间两行,中间\n
    #捕获:开头@fl(,中间包含换行\n,直到遇到第一个 )
    s/@fl(\(.*\n[^)]*\))/\\fB \1\\fR/g
    P                   #打印模式空间第一个\n之前的第一行
    D                   #删除第一行;剩余部分留在模式空间,跳回脚本开头
}

逻辑:

  1. 先处理本行能完成的匹配
  2. 如果行内出现@fl(但是本行没有),执行N读下一行
  3. 跨行替换;P输出第一行;D删除第一行,剩下第二行回到脚本开头继续解析是否还存在标记。
  4. [^字符] 正则语法:脱字符^写在方括号里面第一位,代表取反:匹配【不是这个字符】的任意字符。

缺陷:只能最多跨两行,如果括号跨3行以上,这套脚本失效。

实战示例:转换Interleaf文档

Interleaf标记:<para>代表段落,后面跟空行;<Figure Begin>是图片二进制数据。
需求:

  1. 将<para>+后面空行替换troff段落宏.LP
  2. 把图片块导出到单独文件fig.interleaf,原文位置替换图片占位宏.FG ... .FE
  3. 删除多余空行

sed脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/<para>/{
    N
    c\
.LP
}
/<Figure Begin>/,/<Figure End>/{
    w fig.interleaf
    /<Figure End>/i\
.FG\
<insert figure here>\
.FE
    d
}
/^$/d

说明:

  1. 匹配<para>,N读取后面空行进入模式空间;c命令改写整个模式空间内容为.LP
  2. 范围匹配图片块,w把区间全部行写入外部文件;匹配到结束标记时i插入占位宏;d删除模式空间,不在原文输出图片二进制。

二、保持空间 hold space

sed有两块缓冲区:

  1. 模式空间(pattern space):正在处理的文本,日常sed命令操作的就是它
  2. 保持空间(hold space):临时备份存储区;普通s/d/p等命令不能直接操作保持空间,只能通过h H g G x互相拷贝交换
命令 含义
h 覆盖:把模式空间内容复制,覆盖重写保持空间原有内容
H 追加:模式空间内容追加到保持空间末尾;追加前自动加换行符\n
g 覆盖:保持空间复制,覆盖重写模式空间
G 追加:保持空间内容追加到模式空间末尾;追加前自动加换行符\n
x 交换:模式空间 和 保持空间两者完整互换内容

大写H/G:追加;小写h/g:覆盖重写;x直接交换两块缓冲区。

案例1:调换成对行顺序

输入文件

1
2
3
4
5
6
1
2
11
22
111
222

需求:把1开头行放到2开头行后面输出

1
2
3
4
5
6
7
/1/{
h
d
}
/2/{
G
}

逻辑:

  1. 匹配1开头行:h复制到保持空间;d删除模式空间,不输出,回到脚本开头读下一行
  2. 读到2开头行,G把保持空间的1开头行追加到模式空间后面;脚本走到末尾自动输出两行。

漏洞:如果1开头的后面没有紧跟着2开头,那一行会永久消失,没有输出。

案例2:只修改行中一部分文本(局部大写转换)

需求:句子find the Match statement → find the MATCH statement;只把中间语句名大写,其余文本原样保留。 思路:

  1. h:把完整原始行备份进保持空间
  2. 模式空间只提取中间语句名字
  3. y做大小写转换(y会转换整个模式空间)
  4. G把原始完整行追加回来
  5. 替换重组,把大写后的名字放回原句子中

脚本:

1
2
3
4
5
6
7
/the .*statement/{
h
s/.*the \(.*\)statement.*/\1/
y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/
G
s/\(.*\)\n\(.*the \).*\(statement.*\)/\2\1\3/
}

案例3:收集文本块,增加HTML段落标签<p></p>

输入:段落由多行组成,段落之间用空行分隔。
需求:遇到空行的时候,把之前收集的所有行用<p>...</p>包裹输出。

脚本主体

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#非空行:追加进保持空间,d不输出
/^$/!{
    H
    d
}
#遇到空行,处理保持空间中积攒的段落
/^$/ {
    x
    s/^\n/<p>/
    s/$/<\/p>/
    G
}

流程拆解:

  1. 读到非空行:H追加到保持空间,d删除模式空间,回到开头读下一行。文本全部积攒在hold space,完全不输出。
  2. 读到空行:
    • x交换模式空间、保持空间;此时模式空间是积攒的段落文本,保持空间存那个空行
    • s/^\n/<p>/:H命令会在内容开头自带换行,替换开头换行符为<p>
    • s/$/<\/p>/:模式空间末尾加上</p>
    • G把保持空间存的原始空行追加回来;脚本末尾自动输出带标签段落。

漏洞:文件最后一段末尾没有空行的话,最后一段永远不会输出! 修复,专门匹配文件最后一行$:

1
2
3
4
5
6
${
/^$/! {
    H
    s/.*//
    }
}

逻辑:匹配文件最后一行,如果不为空,H存入保持空间;把模式空间清空变成空行,就会触发上面空行的处理逻辑输出段落。

案例4:索引条目脚本 index.edit

核心用途:正则元字符[ ] \ * .需要做两层转义:一层用于sed匹配模式;一层用于替换字符串。 实现技巧:

  1. h原始条目存入保持空间
  2. 模式空间做模式侧元字符转义
  3. x交换;模式空间做替换侧元字符转义
  4. 再次交换,拼接完整sed替换脚本输出。

核心知识点:保持空间用来保存两份不同处理版本的同一行,交替交换修改。

正则与替换侧元字符转义表

特殊字符 正则匹配侧(s/左边/) 替换字符串侧(s/右边/)
\ 转义符,自身要写 \\ 转义符,自身要写 \\
. 匹配任意字符 → 转义 \. 普通字符,不用转义
* 前项重复 → 转义 \* 普通字符,不用转义
[ ] 字符集 → 转义 \[ \] 普通字符,不用转义
^ 行首锚点 → \^ 普通字符,不用转义
$ 行尾锚点 → \$ 普通字符,不用转义
& 普通字符,不用转义 整个匹配内容 → 转义 \&
\( \) 捕获组(存入 \1~\9) 普通字符,不用转义
\1~\9 反向引用(匹配重复文本) 引用第 n 个捕获组内容
\n 换行符(需配 N) 换行符

匹配侧是正则世界,元字符都要转义;替换侧是字面世界,只有 \、&、\1~\9 特殊,其余全是普通字符。

三、高级流控制:标签、b分支、t测试

sed默认从上往下顺序执行脚本。使用标签、b、t可以实现跳转、循环、条件分支。

语法

  1. 标签定义 :label
1
:mylabel   #冒号开头;冒号和标签之间不能空格;标签最多7字符

标签行末尾空格会被当成标签名字一部分!

  1. 无条件分支 b [label]
1
[地址]b [标签名]
  • 如果写标签:跳转到对应标签行继续往下执行
  • 不写标签:直接跳到脚本末尾,结束当前行所有处理,不再执行后面任何命令

利用b实现跳过部分脚本逻辑:

1
2
3
4
5
# .ES ~ .EE之间的行,直接跳到脚本末尾,后面所有替换都不执行
/^\.ES/,/^\.EE/b
s/^"/``/
s/"$/''/
……#后面一堆替换全部被跳过

b可以写循环:

1
2
3
4
5
:top
command1
command2
/pattern/b top   #匹配pattern,跳回top标签,重复执行command1 command2
command3

条件测试分支 t [label]

t:只有本行有任意s替换执行成功,才发生跳转;否则继续向下执行。

1
[地址]t [标签]
  • 无标签:替换成功就跳到脚本末尾。

典型场景:多条件互斥匹配,类似case语句,匹配成功就不再尝试后续规则。 示例:匹配.Rh 0行,优先3个引号参数,其次2个,其次1个。一旦某一条s替换成功,t直接跳出,不再尝试后面的替换。

1
2
3
4
5
6
7
/\.Rh 0/{
s/"\(.*\)" "\(.*\)" "\(.*\)"/"\1" "\2" "\3"/
t
s/"\(.*\)" "\(.*\)"/"\1" "\2"/
t
s/"\(.*\)"/"\1"/
}

综合改进:支持跨多行的Scribe字体转换(b+t+标签循环)

原来脚本只能跨两行;用标签循环,不断N读入行,可以处理跨3行甚至更多行括号。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#改变Scribe字体的脚本,支持多行
:begin
/@fl(\([^)]*\))/{
s//\\fB\1\\fR/g
b begin
}
/@fl(.*/{
N
s/@fl(\([^)]*\n[^)]*\))/\\fB\1\\fR/g
t again
b begin
}
: again
P
D
}

逻辑拆解

  1. :begin循环起点
  2. 只要本行能匹配完整@fl(...)做替换,b begin跳回开头,继续检测本行剩余其他标记(一行多个标记)
  3. 如果发现@fl(但是本行没有),执行N读取下一行,尝试跨行替换
  4. t again:跨行替换成功,跳到again标签执行P、D输出第一行;
  5. 如果替换失败(括号还在更下一行),b begin回到循环开头,继续N读下一行;
  6. :again:P打印第一行,D删除第一行,剩余内容回到脚本开头继续解析。

完整综合示例 phrase shell脚本

模拟grep,搜索可能跨行的短语,使用:N h G x b D几乎全部高级sed命令。 逻辑要点:

  1. 优先判断短语是否完整在当前一行;匹配就b跳到脚本末尾直接输出。
  2. 不匹配:N读下一行,h保存两行完整副本进保持空间。
  3. 删掉模式空间第一行,只看第二行,如果第二行命中关键词,直接输出。
  4. 恢复保持空间原始两行,把换行替换成空格,把两行拼接成一整行文本,测试是否跨行匹配短语。
  5. 匹配成功,恢复原始换行版本输出;失败:D删除第一行,保留第二行,回到脚本开头继续循环。

本章全部核心知识点速记(复习清单)

1)多行模式空间

  1. N:追加下一行到模式空间,生成多行;注意最后一行$!N的坑;多行中^ $只针对整个模式空间首尾,内部换行用\n匹配。
  2. D:删除模式空间第一行,不读新行,回到脚本开头;d清空全部模式空间读新行。压缩多空行用D。
  3. P:打印模式空间第一行;经典循环组合:N → s → P → D实现两行滚动窗口。

2)保持空间 hold space

  • 两套缓冲区:模式空间(主操作)、保持空间(临时备份)
  • h/H:模式空间→保持空间(覆盖/追加)
  • g/G:保持空间→模式空间(覆盖/追加)
  • x:交换两块缓冲区。
  • 用途:保存原始文本副本,只修改部分内容;逐行收集文本块。
  • 坑:H/G追加时会自动插入换行符。

3)流程控制

  1. :tag定义标签;b label无条件跳转,不带标签跳到脚本结尾。可实现循环,跳过部分脚本。
  2. t label条件跳转:只要有任意s替换成功才跳转,适合case式互斥匹配。
  3. 三者组合可以写循环,处理跨任意多行的匹配。

sed高级脚本阅读困难的根源: D会回到脚本开头,不会读新行;b/t跳转;两块缓冲区互相拷贝;模式空间可以存放多行。