1. sed 是什么
sed(Stream Editor,流编辑器)是一个非交互式的文本处理工具.它从标准输入或文件中逐行读入文本,按照事先写好的"脚本"对每一行进行处理,然后把结果输出到标准输出.
如果用 Word 或 vim 编辑文件,是手动选中文本 → 按删除键 → 输入新内容--这是交互式编辑.
sed 则是:把编辑指令提前写在一张小纸条上,sed 拿着纸条自动对文件的每一行执行相同操作--一行接一行,不问任何问题.
1.1 它和文本编辑器有什么不同
| 交互式编辑器(vim / nano / VS Code) | sed(流编辑器) | |
|---|---|---|
| 操作方式 | 手动光标定位,逐个字符编辑 | 写脚本,自动化批量处理 |
| 文件 | 打开 → 编辑 → 保存 | 读入 stdin/文件 → 处理 → 输出到 stdout |
| 是否原样修改 | 是(除非不保存) | 默认不改原文件(除非加 -i) |
| 适合场景 | 写代码,改配置,精细调整 | 批量替换,格式化,提取,自动化处理 |
一句话总结 sed 的定位:当需要对文本做"批量规则化"的处理时--比如把 100 个文件中的 http:// 全部替换为 https://--打开每个文件逐一修改是荒谬的.sed 就是为这种事而生的.
2. 执行模型:sed 如何工作
sed 的执行模型是理解它的最大门槛.不是语法难,而是它的工作方式和直觉想的可能不一样.
2.1 模式空间(Pattern Space)--sed 的工作台
sed 内部维护着一块内存区域叫模式空间.它的工作流程是一个严格的循环:
|
关键理解:sed 脚本(比如 s/old/new/)不是对文件整体执行一次--而是对每一行都独立执行一遍完整脚本.脚本是"每条生产线上的操作规范",不是"对整个仓库的管理指令".
2.2 一个例子走完完整生命周期
假设 test.txt 有三行,执行 sed 's/apple/orange/' test.txt:
|
2.3 默认行为:自动打印(-n 关闭它)
sed 在每行脚本执行完后,会自动把模式空间的内容打印到 stdout.这就是为什么不写任何"打印"命令也能看到输出--它是自动的.
-n 选项关闭这个自动行为.关闭后,只有显式使用 p 命令的行才会输出:
|
默认自动打印是个好设计
大多数时候想看到处理后的完整结果(改过的行 + 没改的行原样保留),所以自动打印让常见操作变得简洁.只有当想"只提取某些行"时才需要 -n,这就变成了类似 grep 的行为.
3. 脚本语法
sed 脚本的基本结构是:
|
地址是可选的--不写地址意味着所有行都执行该命令.
3.1 地址(Address)--对哪些行操作
| 地址类型 | 写法 | 含义 |
|---|---|---|
| 行号 | 3 |
第 3 行 |
| 行号范围 | 3,8 |
第 3 到第 8 行 |
| 正则匹配 | /pattern/ |
包含 pattern 的行 |
| 正则范围 | /start/,/end/ |
从匹配 start 的行到匹配 end 的行 |
| 最后一行 | $ |
文件的最后一行 |
| 取反 | /pattern/! |
不匹配 pattern 的行 |
3.2 命令(Command)--做什么
| 命令 | 含义 | 使用频率 |
|---|---|---|
s/old/new/ |
替换(substitute)--把 old 换成 new | ⭐⭐⭐⭐⭐ |
d |
删除(delete)--删掉这一行 | ⭐⭐⭐⭐ |
p |
打印(print)--输出模式空间(通常配合 -n) | ⭐⭐⭐⭐ |
a text |
在这行之后追加一行 text(append) | ⭐⭐⭐ |
i text |
在这行之前插入一行 text(insert) | ⭐⭐⭐ |
c text |
把匹配行替换为 text(change) | ⭐⭐ |
y/abc/xyz/ |
逐字符翻译(类似 tr 命令) | ⭐⭐ |
= |
打印当前行号 | ⭐ |
q |
退出(quit)--处理完当前行后停止 | ⭐ |
3.3 多个命令如何组织
三种方式,按场景选用:
|
4. s 命令:替换(最核心)
s 命令占据了 sed 使用的 80%.它的语法是:
|
4.1 基本替换
|
4.2 分隔符不一定是 /
当匹配模式或替换文本中本身包含大量 / 时(比如文件路径),可以换成其他分隔符.sed 使用 s 之后的第一个字符作为分隔符:
|
4.3 标志位(Flags)
| 标志 | 含义 | 示例 |
|---|---|---|
g |
global -- 替换行内所有匹配,不只是第一个 | s/foo/bar/g |
数字 |
只替换第 N 次匹配 | s/foo/bar/2 只替换第 2 个 |
p |
print -- 发生替换时打印这一行(配合 -n) | sed -n 's/foo/bar/p' |
i |
匹配时ignore 大小写 | s/hello/HI/i 匹配 Hello,HELLO 等 |
|
4.4 捕获组与反向引用
用小括号 \( \) 捕获匹配的部分,用 \1 \2 ... \9 引用:
|
sed 默认使用 BRE--捕获组要加反斜杠
默认 sed 中 \( \) 才是捕获组,+ ? { } 也都需要反斜杠.这和 Python/JS 中写的正则很不一样.
如果觉得反斜杠烦,macOS 和现代 Linux 用 sed -E 启用 ERE(扩展正则),然后 ( ) + ? { } 就都不需要反斜杠了:
|
反向引用类比手机输入法的"识别粘贴":(regex) 就好像在文本中"圈出"一块内容贴在 sed 的剪贴板(\1),可以在替换文本中重新贴出来--换位置,加前缀/后缀,改变格式,全由决定.
5. d 命令:删除行
d 把模式空间中的当前行删掉--实际上就是跳过打印步骤,直接进入下一行的处理.
|
6. p 命令:打印行(配合 -n)
p 用于显式打印模式空间的内容.单独用它会导致每行打印两次(自动打印一次 + p 打印一次),所以p 总是配合 -n 使用:
|
7. a / i / c:插入与替换行
这三个命令不操作行内容,而是操作行本身--在行前,行后插入新行,或把整行替换掉.
| 命令 | 含义 | 位置 |
|---|---|---|
a text |
append(追加) | 在匹配行之后加一行 |
i text |
insert(插入) | 在匹配行之前加一行 |
c text |
change(替换) | 用 text 替换整行 |
|
macOS 上的 sed 差异
macOS 的 sed(BSD 版本)要求 a i c 后的反斜杠必须有,且反斜杠后必须换行或紧跟一个转义的换行符.
如果嫌麻烦:用 sed -i '' '...' 并始终在 a / i / c 后写 \ 再接内容.或者更省心--安装 GNU sed:brew install gnu-sed,之后用 gsed 替代 sed,行为和 Linux 一致.
8. 地址进阶
8.1 行号地址
|
8.2 模式地址(正则)
|
8.3 范围地址
用两个地址(逗号分隔)定义一个区间:从第一个地址匹配的行开始,到第二个地址匹配的行结束(含两端).
|
如果文件中有多个 BEGIN...END 块,/BEGIN/,/END/ 会作用于每一个块.它是"遇到 BEGIN 就开启模式,遇到 END 就关闭,再遇到 BEGIN 又开启".这不是全局的一次性范围,而是一种触发器.
8.4 取反地址
在地址后加 ! 表示"不是这些行":
|
9. 模式空间与保持空间
这是 sed 中最进阶的概念--也是让从"会用 sed 做简单替换"升级到"能用 sed 做复杂文本变换"的分水岭.
9.1 直觉理解
sed 有两个内存缓冲区:
|
想象在流水线上装配产品.模式空间是工作台--每个零件(每行)依次放上来,处理完就送走.
保持空间是工具架--可以把某个零件先暂存上去,等后面的某个零件来了,再从架子上拿下来和它拼接.没有这个架子,只能处理"手头这一件",有了它,就能让行与行之间发生关联.
9.2 操作命令
| 命令 | 模式空间 → 保持空间 | 保持空间 → 模式空间 |
|---|---|---|
| 复制(覆盖) | h -- 把模式空间复制到保持空间 |
g -- 把保持空间复制到模式空间 |
| 追加(拼接) | H -- 把模式空间追加到保持空间末尾(前面加一个换行) |
G -- 把保持空间追加到模式空间末尾(前面加一个换行) |
| 交换 | x -- 交换模式空间和保持空间的内容 |
记忆技巧:小写 = 覆盖,大写 = 追加;h = 往 hold 塞,g = 从 hold 取(get).
9.3 实战:两行变一行
假设有一个文件,每两行是一组数据(第一行是名字,第二行是分数),想把它们合并成一行:
|
思路:读到奇数行(名字)时,把它存进保持空间.读到偶数行(分数)时,把保持空间的内容取出来,和当前行拼在一起:
|
分步走一遍(以第 1–2 行为例):
- 第 1 行 "Alice" 进入模式空间 →
h复制到保持空间 → 保持空间 = "Alice" - 自动打印被 -n 抑制,清空模式空间
- 第 2 行 "95" 进入模式空间 →
H把模式空间追加到保持空间 → 保持空间 = "Alice\n95" g把保持空间复制到模式空间 → 模式空间 = "Alice\n95"s/\n/, /把换行变成逗号 → "Alice, 95" → 打印
10. -i:原地修改文件
默认情况下 sed 把结果输出到 stdout,原文件不变.-i 让它直接写入原文件:
|
macOS 上 -i 需要提供备份后缀
macOS 的 sed 要求 -i 后面必须跟一个参数(备份文件的后缀).如果不需要备份,传空字符串:
|
最佳实践:先用不带 -i 的命令预览结果,确认无误后再加上 -i;或者先用 -i '.bak' 留个备份,确认没问题再删 .bak.
11. 常用配方(Recipes)
以下是最常遇到的 sed 用法,可以直接复制修改:
|
快速回顾
- sed = 流编辑器:非交互式,逐行读入 → 执行脚本 → 打印 → 清空 → 下一行
- 模式空间:sed 的工作台--每行文本轮流进入,被处理,被打印,被下一行覆盖
- 脚本结构:[地址] 命令.地址决定"对哪些行",命令决定"做什么"
- s 命令:核心中的核心.加
g替换全部,加i忽略大小写,用\1 \2引用捕获组 - BRE vs ERE:默认 BRE 需要反斜杠;用
sed -E切换到 ERE,和 Python/JS 正则一致 - d/p/a/i/c:
d删除整行,p打印(配合-n),a/i/c插入/替换整行 - 保持空间:第二个缓冲区--通过
h H g G x让行与行之间产生关联,实现跨行操作 - -i 原地修改:macOS 上必须写
sed -i '' '...';建议先预览确认后再加 -i - 定位:sed 的强项是重复性,规则化的文本批量处理--一次性的手动编辑不值得写 sed
动手练习
- 地址练习:创建一个 10 行的文本文件,练习
sed '3d',sed '2,5d',sed '$d',理解地址的三种写法 - g 标志:用
echo "one one one" | sed 's/one/two/'和sed 's/one/two/g'对比,理解g标志的作用 - 捕获组:写一条 sed 命令把 "2024-06-16" 变成 "16.06.2024"(用捕获组)
- 提取模式:找一份真实的配置文件(如
~/.zshrc),用sed -n '/^#/!p'只显示非注释行,然后用sed -n '/^#/p'只显示注释行 - 删除空行:用
sed '/^$/d'删除空行,对比删除前后的效果 - -n + p:用
seq 1 20 | sed -n '5,10p'体会-n+p的"提取"模式与 grep/head/tail 的异同 - 保持空间:用保持空间命令实现把文件所有行倒序输出(参考思路:
sed '1!G;h;$!d')--不需要完全理解,但动手跑一遍,观察效果