第四章 编写sed脚本

编写sed脚本

sed脚本就是一组编辑命令的集合,运行在输入文件上实现批量文本修改。

手动编辑器 vs sed脚本

  • vi交互式编辑器:敲一条命令立刻看到效果,支持undo撤销,操作安全可控。
  • sed脚本:自动化批量编辑,没有撤销undo机制,写错脚本会产生非预期结果。

编写sed脚本三步(和第三章写正则的方法论完全一致)

  1. 先搞清楚你要做什么处理。
  2. 清晰描述完整处理流程。
  3. 反复测试脚本之后,再执行最终修改。

测试最佳实践:准备多份样本输入,观察输出结果是否符合预期。

sed三大基础原理(重中之重)

  1. 脚本里面所有编辑命令,会依次应用到每一行输入。
  2. 默认命令作用于全部行;只有行寻址可以限制命令生效的行。
  3. 原始输入文件不会被修改;sed修改的是行的副本,副本输出到标准输出(屏幕)。

后续第五章、第六章会讲解更多sed命令。本章重点理解脚本的运行逻辑,而不是死记全部命令。

在脚本中应用命令

非常容易踩坑:很多初学者误以为:sed执行完一条命令,把这条命令作用全部行,再执行下一条命令。 真实流程:读取一行 → 将脚本全部命令依次执行在这一行 → 输出 → 读取下一行。

也就是说:后面的命令,处理的是前面命令修改之后的模式空间内容,不是原始输入行。旧的原始行会被丢弃。

坑示例

脚本,本意:pig→cow;cow→horse

1
2
s/pig/cow/g
s/cow/horse/g

输入行:pig

  1. 第一条:pig替换成cow
  2. 第二条:把刚刚生成的cow又替换成horse
    输出结果:horse

对错取决于业务想不想要 “衍生出来的内容被后续命令再次处理”。 问题根源:前一条命令修改了文本,后一条命令继续处理修改后的结果。 解决方案:调换命令顺序

1
2
s/cow/horse/g
s/pig/cow/g

模式空间

模式空间(pattern space):sed内部的临时缓冲区、工作区。每次存放一行输入的副本,所有编辑命令全部在这里做修改。

特点:

  1. 每次读入一行文本的副本放入模式空间。原始文件保持不变。
  2. 脚本的每一条编辑命令,依次修改模式空间里面这份副本。
  3. 全部命令执行完毕,输出模式空间内容;清空,读取下一行。
  4. 处理超大文件也不怕,因为一次只加载一行,不会占满内存。
flowchart TD
    A["The UNIX System<br/>输入"] --> B["模式空间"]
    C["脚本<br/>s/Unix/UNIX/<br/>s/UNIX System/UNIX Operating System/"] -.-> B
    
    subgraph 模式空间
        B1["The Unix System"]
        B2["The UNIX System"]
        B3["The UNIX Operating System"]
        B1 -->|执行第1条替换| B2
        B2 -->|执行第2条替换| B3
    end

    B --> D["输出<br/>The UNIX Operating System"]

图4‑1 脚本命令改变模式空间内容

关键点:第二个替换匹配的是修改之后的行,不是原始输入。

补充:sed还有另外一块缓冲区叫保持空间 hold space,第六章高级sed命令讲解。

寻址上的全局透视

sed默认是全局模式,不加地址,命令对每一行生效。 vi/ex编辑器默认只作用当前行;sed正好反过来:默认全部行,需要写地址来限制范围。

sed命令可以指定:0个、1个、2个地址。地址可以是:行号、正则表达式(斜杠包裹/pattern/)。

地址写法 行为说明
无地址 命令对所有行生效
1个地址 只匹配这个地址的行执行命令;地址可以是行号1或者正则/Sebastopol/
地址1,地址2 范围:从匹配地址1的行开始,一直到匹配地址2的行(包含两端)
地址! 感叹号!,取反;命令作用于所有不匹配该地址的行

示例演示(d删除命令)

  1. 无地址,删除全部行,无输出
1
d
  1. 只删除第1行
1
1d
  1. 删除文件最后一行;$代表输入流最后一行,和正则行尾$不是同一个东西
1
$d
  1. 删除全部空行,正则作为地址
1
/^$/d
  1. 删除 .TS 到 .TE 之间全部行(tbl表格块)
1
/^\.TS/,/^\.TE/d
  1. 删除50行直到文件末尾
1
50,$d
  1. 混合行号+模式:删除第1行直到第一个空行(删除邮件头部)
1
1,/^$/d

重要细节:范围地址一旦匹配第一个地址,就持续应用命令,直到命中第二个地址;如果文件从头到尾没有匹配第二个地址,则会删除从第一个匹配之后的全部行。

地址+!取反示例

保留.TS~.TE块,其余全部删除(提取表格块)

1
/^\.TS/,/^\.TE/!d

翻译:不在这个范围里面的行,全部删除。

分组命令 { }

大括号{}实现两件事:

  1. 在同一个地址/行范围上,执行多条sed命令。
  2. 地址嵌套:在一个行范围内部,再增加一层过滤条件。

语法硬性规则:

  • 左大括号 { 必须在行末尾
  • 右大括号 } 必须单独占一行,后面不能有空格

示例1:在.TS和.TE表格块内部,删除块里面的空行(嵌套地址)

1
2
3
/^\.TS/,/^\.TE/{
    /^$/d
}

示例2:同一个范围,执行多条修改命令

1
2
3
4
5
/^\.TS/,/^\.TE/ {
    /.S/d
    s/^\.ps 10/.ps 8/
    s/^\.vs 12/.vs 10/
}

测试并保存输出

sed只会修改副本,原始文件不受影响。

1
2
sed -f sedscr testfile
# 输出打印屏幕,testfile文件本身没有改动!

想要保存结果,必须使用重定向>输出到新文件:

1
sed -f sedscr testfile > newfile

❗严重坑点:绝对不能重定向输出到输入源文件 > testfile,会直接清空破坏原文件!

测试工作流

  1. sed输出重定向到新文件 newfile
  2. 使用diff对比原始文件和输出文件,检查改动是否正确
1
diff testfile newfile
  1. 确认没问题,备份原文件,再覆盖原文件。

两个辅助shell脚本(原书提供)

testsed:用于测试sed脚本

把输出写入临时文件 tmp.文件名,方便人工检查;不会修改原始文件。

1
2
3
4
for x
do
sed -f sedscr $x > tmp.$x
done

for x 等价于 for x in "$@",$@ 代表脚本命令行上传入的所有参数。x :代表变量名字本身,$x :代表取出变量 x 里面存的值。
使用:写好sed脚本sedscr,执行testsed 文件名,检查tmp.*临时文件。如果脚本写错,修改sedscr,重新在原始文件上跑,不要去修改临时文件。

runsed:确认脚本没问题后,用来永久修改原始文件

内部逻辑:生成临时文件;校验临时文件不为空;自动备份原文件xxx.bak,再覆盖原文件。 使用前强烈建议先用testsed完成测试!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/sh
for x
do
echo "editing $x: \c"
if test "$x" = sedscr; then
    echo "not editing sedscript!"
elif test -s $x; then
    sed -f sedscr $x > /tmp/$x$$
    if test -s /tmp/$x$$
    then
        if cmp -s $x /tmp/$x$$
        then
            echo "file not changed: \c"
        else
            mv $x $x.bak # 备份原始文件
            cp /tmp/$x$$ $x
        fi
        echo "done"
    else
        echo "Sed produced an empty file\c"
        echo "-check your sedscript."
    fi
    rm -f /tmp/$x$$
else
    echo "original file is empty."
fi
done
echo "all done"

用法示例:处理ch0开头的一批文件

1
runsed ch0?

sed脚本的4种类型

1. 对同一文件的多重编辑

场景:一个文件做批量多处修改。 开发技巧:一条命令、一次测试;迭代叠加,不要一次性写一大堆命令再测试。

案例:把打印格式化文件转为troff格式,需求清单

  1. 将空行替换段落宏 .LP
  2. 删除以+开头的下划线行
  3. 删除每行开头前导空格
  4. 多个连续空格压缩成单个空格
  5. 处理句号后面的多空格,保留句号后面两个空格

完整脚本 sedscr

1
2
3
4
5
s/^$/.LP/
/^+  */d
s/^  *//
s/  */ /g
s/\.  */.  /g

运行测试

1
sed -f sedscr horsefeathers

开发思路:写一条 → 测试;再加下一条,逐步迭代。出现bug,就逐个注释命令定位问题。

2. 改变一组文件

场景:上百个文件批量做搜索替换。

编写替换列表脚本,先用testsed抽样测试,确认没问题,再用runsed批量修改。

示例片段

1
2
3
4
5
s/ON switch/START switch/g
s/ON button/START switch/g
s/STANDBY switch/STOP switch/g
s/STANDBY button/STOP switch/g
s/STANDBY/STOP/g

实战建议:先用grep扫描原始文本,摸清真实文本的各种情况;挑选有代表性测试样例,不要只拿一两个简单样例就直接跑全部文件。

3. 提取文件的内容

sed -n 参数:关闭默认输出;只打印p命令显式指定的行。 优势:可以提取行范围;grep只能匹配单行,不能直接提取一段连续多行。

示例A:提取troff宏定义

.deBL 开始,..结束,提取整个宏定义。

1
sed -n "/^\.deBL/,/^\.\.$/p" /usr/lib/macros/mmt

封装shell脚本 getmac,传入宏名字作为参数

1
2
3
#!/bin/sh
#getmac -- 获取mm宏 $1 的定义
sed -n "/^\.de$1/,/^\.\.$/p" /usr/lib/macros/mmt

调用:

1
getmac BL

扩展版本支持多个宏包(-ms / -mm /‑man),使用case分支。

示例B:生成文档提纲 do.outline

读取标题宏,替换、缩进输出文档大纲,不改动原文件,仅输出到屏幕。

1
2
3
4
5
6
sed -n '
s/^\.Se /CHAPTER /p
s/^\.Ah / A. /p
s/^\.Bh /  B. /p
s/"//g
' $*

4. 编辑工作转移(管道流式处理,不落地写文件)

场景:sed放在管道中间,处理数据直接交给下一个程序,不修改磁盘上原始文件,不生成中间文件。

示例:把双连字符--转换成troff长破折号\(em;排除3个及以上连续短横线(防止把绘制横线也转换)。

1
/---/!s/--/\\(em/g

地址! 解读:行如果匹配---,就不执行替换;其余全部行执行替换。

放到管道直接交给troff格式化,无中间文件:

1
sed '/---/!s/--/\\(em/g' file | troff

典型shell脚本format,封装sed、eqn、pic、troff整套排版流水线。

编写sed脚本提示

  1. 充分了解你的输入:使用grep提前探查原始文本,知道会出现哪些格式。
  2. 先小样本测试;采样测试;既要测试应该匹配的,也要测试不该匹配的边界情况。
  3. 逐步开发,每新增一条命令就要测试。
  4. 不必追求100%处理所有极端边角案例;极少出现的异常情况,可以留给人工处理。

本章核心速记卡片

  1. sed执行顺序:读一行 → 全部脚本命令依次跑在模式空间副本 →输出;前序修改会影响后续命令。
  2. 模式空间:每行副本的临时缓冲区;原始文件永远不会被自动修改。
  3. 寻址:0/1/2个地址;逗号表示范围;!取反;范围地址要警惕第二个模式永远匹配不到的风险。
  4. 分组{}:对同一个范围执行多条命令;语法严格,{在行尾,}独占一行。
  5. 测试流程:输出重定向新文件 → diff对比;testsed做测试;确认无误再runsed永久修改。禁止>直接定向输入文件。
  6. sed‑n:关闭默认输出,配合p打印需要的行,可以提取多行片段。
  7. 4类脚本模型:单文件多重编辑;批量多文件修改;内容提取;管道流式处理。