刚入行那会儿,我第一次在线上服务器处理一个快上G的Nginx日志,用vim打开之后光标转了半天圈,鼠标挪都挪不动,最后只能强行kill掉进程。组里的老同事过来看了一眼,在命令行敲了一行sed -n '10,20p' app.log,几秒钟就把日志里想看的部分捞了出来。那一刻我才真正理解,shell三剑客这三个词不是教程里的概念,而是每个靠命令行吃饭的人绕不开的日常工具。所谓三剑客,就是grep、sed、awk这三个命令的组合,其中sed扮演的是"编辑"这个角色。
这篇就围绕sed展开,从它的底层机制讲到高频用法,再延伸到真实场景和坑点,尽量把"能用"和"为什么这么用"一起讲清楚。适合刚开始接触shell脚本的入门读者,也适合已经用过一些sed命令、但对执行流程和边界理解得不够透的朋友。
1. 先解决定位问题:三剑客凭什么分工
1.1 一把武器干一件事,组合起来干活
很多初学者会陷入一个误区:拿到文本处理需求,不知道该用grep还是sed还是awk,于是挨个试一遍。其实三剑客的分工相当明确,用一句话概括就是:grep负责找行,sed负责改行,awk负责算列。
| 工具 | 核心定位 | 典型场景 | 一个命令就能看懂 |
|---|---|---|---|
| grep | 查找与过滤 | 从日志里搜出包含 error 的行 | grep "error" app.log |
| sed | 编辑与替换 | 把配置里所有 8080 端口改成 9090 | sed -i 's/8080/9090/g' app.conf |
| awk | 格式化与计算 | 统计某一列的总和、按条件输出列 | awk '{sum+=$1} END{print sum}' data.txt |
实际干活的时候,这三个命令也经常串成管道一起用。典型场景是:先用grep把嫌疑行筛出来,再用sed做去重、替换、删字段之类的处理,最后用awk汇总计算。每一步都只做自己最擅长的那件事,组合起来就是一套完整的文本处理流水线。
1.2 流式处理到底是什么意思
sed的全称是 Stream Editor,流编辑器。理解"流"这个字,就抓住了它的核心。
想象一条生产线,文件的内容像是传送带上的纸箱,一个接一个地经过。sed每次都只处理眼前的这一个箱子:读一行,放进内存,按你给出的命令处理,处理完输出,然后立刻清空,继续读下一行。它不会把整个文件一次性加载到内存里等着你慢慢操作,也不会像 vim 那样维护一个巨大的编辑器缓冲区。
这个设计带来两个很实际的好处:
- 内存占用基本恒定。处理一个 1G 的文件和处理一个 1M 的文件,
sed的内存消耗不会差太多,因为它只关心当前这一行长什么样。 - 可以在管道里实时吞吐。
tail -f app.log | sed 's/ERROR/错误/g'这种用法,sed会实时处理冒出来的每一行,而不需要等文件写完。
1.3 学 sed 的正确节奏
sed的命令格式核心就一句话:sed [选项] '范围+动作' 文件。范围是说"处理哪些行",动作是说"对这些行做什么"。
范围可以是行号、正则表达式、或者两者的组合;动作则是最常用的几种:打印、删除、替换、插入。你只要把"范围"和"动作"这两块拼图搞清楚,大部分需求都能直接写出来。选项里最需要记牢的是三个:-n(安静模式,取消默认输出)、-i(原地修改文件)、-E(使用扩展正则)。
后面的章节就按照"机制—命令—正则—实战—踩坑"这个路径展开,这样一层层下来,比零散记命令要扎实得多。
2. 模式空间与执行顺序:sed 背后的流水线机制
2.1 模式空间:每一行都有一个"工位"
讲sed绕不开一个概念:模式空间(pattern space)。
这是sed内部一块临时的内存缓冲区。每一行文本读进来之后,会被放进模式空间里,然后你指定的命令全部在这个空间内执行。执行完之后,如果没加-n,模式空间里的内容默认会被输出到标准输出,接着模式空间被清空,再读入下一行。
你可以把模式空间理解成"工位"。传送带送来一个纸箱(一行文本),工位上的工人(sed 命令)在台面上操作这个箱子,操作完把箱子推走,台面清空,等下一个箱子送进来。它不会攒着几十个箱子一起处理,也不会把箱子拆开摊到整个车间里。
理解了这一点,很多奇怪现象就有了解释。比如你写sed 'p' file,每一行会打印两遍——因为默认输出一遍,加上p命令的显式打印又一遍。这不是 bug,而是"模式空间内容输出一次 + 命令又输出一次"的叠加结果。
2.2 一行命令在 sed 内部是如何流转的
拿最基础的sed -n '3p' file举例,它的完整执行流程是这样的:
sed读取文件第一行,放进模式空间。- 检查地址
3是否匹配当前行。第 1 行不匹配,跳过p命令。 - 因为加了
-n,默认输出被抑制,所以第 1 行不会打印出来。 - 清空模式空间,读入第 2 行,同样不匹配,不输出。
- 读入第 3 行,匹配地址
3,执行p命令,把这一行打印到标准输出。 - 继续往下读第 4 行、第 5 行,直到文件末尾。
这个流程看起来简单,但它是理解sed一切行为的地基。比如你问"为什么sed -n '/error/p' app.log等价于grep error app.log",答案就在这个流程里:sed一行行扫过来,凡是匹配/error/的行就执行一次打印,其他行什么也不输出。这不就是按条件筛选行吗?
2.3 地址范围:从第几行到哪几行,规则就这几条
地址是sed的"减速带",决定命令在哪些行上生效。常用的表达方式就下面这几种:
| 地址写法 | 含义 |
|---|---|
3 | 第 3 行 |
1,5 | 第 1 行到第 5 行 |
$ | 最后一行 |
1~2 | 从第 1 行开始,每 2 行取 1 行(也就是奇数行) |
/pattern/ | 所有匹配正则 pattern 的行 |
/p1/,/p2/ | 从匹配 p1 的行开始,到匹配 p2 的行结束,这个区间内的所有行 |
3,+2 | 第 3 行以及往下 2 行(即第 3 到 5 行) |
其中区间匹配/p1/,/p2/是流式判断的。sed每读到一行,先看当前状态:如果此刻不在区间内,就检查这一行是否匹配起始条件 p1;一旦匹配,就从这一行开始进入区间,之后每一行都算命中,直到某一行匹配了 p2,区间结束。
我经常用这个特性来处理日志。比如sed -n '/2024-06-01 10:00:00/,/2024-06-01 10:30:00/p' app.log,就能精确截取某个时间段内的日志,不需要借助其他工具。再比如sed '/^kernel/,/^$/d' dmesg.txt,会删除从 kernel 开头到第一个空行之间的内容,在清理内核启动信息时很顺手。
2.4 多条命令怎么组合,版本差异要注意
一个sed表达式里经常需要做不止一件事。比如你想在第 20 到 30 行之间,先把foo替换成bar,再把空行删掉,有两种规范的写法。
第一种是用-e把多个表达式串起来:
sed -e '20,30s/foo/bar/g' -e '20,30/^$/d' file第二种是用花括号把地址范围内的多个命令打包:
sed -n '20,30{ s/foo/bar/g; /^$/d; p; }' file花括号写法的可读性更好,但注意地址和左花括号之间要有空格,命令之间用分号分隔。这里有一个不同平台上的坑:GNU sed 和 BSD sed(比如 macOS 自带的那个)对花括号命令块里空格和分号的要求不完全一致,在一些 BSD 实现里分号后面必须跟空格,否则解析就出错。为了避免折腾,我建议写比较复杂的花括号块时,直接用多个-e,或者把命令写进一个脚本文件,再用sed -f script.sed file来执行。这样可读性高,也不会遇到平台差异。
3. 高频动作逐个拆解:p、d、s、a/i/c
3.1 p 打印:小命令但最容易栽在 -n 上
p的作用是"把模式空间的内容打印出来"。单独用p一般没什么意义,因为它会让每行打印两遍。它的正确用法是配合-n,变成"只打印你想打印的":
sed -n '10p' file # 打印第 10 行 sed -n '20,30p' file # 打印第 20 到 30 行 sed -n '/error/p' app.log # 相当于 grep error我在教新人sed的时候,发现最容易翻车的点就是忘记-n。比如想只看匹配行,写了sed '/error/p' app.log,结果屏幕上每一行都出现了,匹配行还会出现两遍。原因前面已经说过:默认输出一次,p命令再输出一次。所以请养成一个习惯:用p之前,先问自己一句"要不要关掉默认输出",要的话就带上-n。
3.2 d 删除:默认不影响原文件
d命令删除的是模式空间里的内容。删除之后,这一行后续不会再被输出,也不会再被执行其他命令。
sed '1,10d' file # 输出删除前 10 行之后的内容 sed -i '/^$/d' file # 去掉所有空行(-i 表示写回原文件) sed -i '/^#/d' app.conf # 删除所有以 # 开头的注释行很多新手看到d就以为原文件被删了,其实不然。sed默认是把处理结果输出到屏幕,原文件一动不动。除非你加了-i,但那是另一个话题,后面专门讲。d的用处非常广,比如sed '10,$d' file可以只保留前 10 行,相当于用一个命令完成了head -10的效果。它和head、tail的不同在于,d是基于地址规则的,你可以精确控制"删哪里"。
3.3 s 替换:三剑客里最常用的"改"
s是sed里戏份最重的命令,它的格式是:
sed 's/正则/替换文本/标志位' file比如sed 's/8080/9090/' app.conf,会把每一行第一次出现的8080换成9090,但只换每行的第一处。要全局替换,得加g标志:
sed 's/8080/9090/g' app.conf常用标志位有这些:
| 标志位 | 作用 |
|---|---|
g | 全局替换,作用于行内所有匹配位置 |
i | 忽略大小写 |
p | 替换成功后打印这一行,常配合-n |
w file | 把替换成功后的行写入指定文件 |
| (无) | 只替换每行第一次匹配 |
s命令里一个非常实用的技巧是更换定界符。如果你要替换的内容本身包含/,比如路径,那正则里的斜杠就会和定界符撞车。比如想替换/usr/local/bin,写成sed 's/\/usr\/local\/bin/\/opt\/bin/g'会非常痛苦。解决办法是换一个定界符,跟在s后面的第一个字符就是新的定界符:
sed 's#/usr/local/bin#/opt/bin#g' file sed 's|/usr/local/bin|/opt/bin|g' file只要内容里没有#或|,这样写就清爽很多。我实际处理路径类替换时,几乎都会用#当定界符。
3.4 a/i/c:行插入、追加和整行替换
除了改内容,sed更擅长的是改结构。a是在匹配行后面追加一行,i是在匹配行前面插入一行,c是把匹配到的整行替换成新内容。
sed '/^\[server\]/a bind 0.0.0.0:8080' nginx.conf sed '3i # this line inserted by sed' app.conf sed '/^old config/c new config line' app.conf追加多行内容时,用\n换行:
sed '/^\[server\]/a port=8080\nhost=0.0.0.0' app.conf一个常见的坑是 BSD 的sed对a的写法要求更严格,有时候需要写成a\再加换行的形式,GNU sed 则宽松一些。跨平台写脚本时,这部分尤其容易踩雷,建议先在小文件上测一遍。
c命令的替换是整行级别的,它会把匹配到的整行文本直接换掉。比如配置里有多个timeout行,你只想改其中某些,用c配合地址就能精准控制:sed '/^timeout=30/c timeout=60' app.conf,只会把timeout=30那行整行替换成timeout=60。
3.5 y、r、w:三个低频但有价值的命令
这三兄弟使用频率不如前几个,但特定场景下意外地好用。
y是逐字符的映射转换,类似于tr命令。它的定界符格式和s一样,两边的字符数量必须相等:
sed 'y/abc/xyz/' file # 把 a 换成 x,b 换成 y,c 换成 zr可以把另一个文件的内容读取进来,插入到匹配行之后。做 HTML 模板拼接时就很方便:
sed '/<\/body>/r footer.html' index.htmlw命令则正好相反,把匹配到的行写入另一个文件:
sed -n '/ERROR/w error.lines' app.log这条命令和grep ERROR app.log > error.lines的效果一样,但好处是可以直接在sed的流水线里继续做别的处理,不用开一个额外的重定向。
4. 正则进阶:括号、花括号和定界符的那些事
4.1 基础正则与扩展正则:少打反斜杠的方式
sed默认使用的是基础正则表达式(BRE),它和你在grep -E、awk、Perl 里常用的扩展正则(ERE)写法上有明显的差异。最大的区别在于:BRE 里(、)、{、}、+、?、|这些符号默认是普通字符,想表达"分组、重复次数、或"等含义,必须加反斜杠转义。
| 元字符 | 基础正则(BRE)写法 | 扩展正则(ERE)写法 |
|---|---|---|
| 分组 | \( \) | ( ) |
| 重复次数 | \{m,n\} | {m,n} |
| 一次或多次 | \+ | + |
| 零次或一次 | \? | ? |
| 或 | | | ` |
一个实际例子:匹配连续 3 到 5 位数字,BRE 要写成[0-9]\{3,5\},ERE 就是[0-9]{3,5}。哪种好看,一目了然。
所以我强烈建议:写sed脚本时默认加上-E选项,使用扩展正则。一个额外的坑是,macOS 自带的 BSD sed 只认-E,不认 GNU sed 的-r。好在这几年 GNU sed 也兼容了-E,所以你的选择完全可以统一成-E,在绝大多数 Linux 和 macOS 上都不会出问题。
4.2 脱字符和行尾符的边界:从单行到多行的观念转变
在常规的逐行处理中,^匹配一行的开头,$匹配一行的结尾。比如sed 's/^/ /' file给每行开头加两个空格,sed 's/$/;/' file给每行末尾加个分号,这些都很好理解。
但sed里还有一类多行命令,比如N,它会把下一行也读进模式空间,跟当前行拼在一起。这时候模式空间里就不止一行内容了,^只能匹配整个模式空间的第一行的开头,$也只能匹配整个模式空间的末尾,中间夹着的那些原始行边界不再视为行首行尾。
这个特性坑过不少人。比如你N之后想用s/^/>> /给每一行都加前缀,结果发现只有第一行被加了前缀,第二行纹丝不动,因为^匹配不到中间的"行开头"。原因就是在多行模式空间里,^只认整个缓冲区的起点。遇到这种情况,要么放弃N改用其他方案,要么对\n做显式替换:sed 's/\n/>> /g'这种思路。知道这个边界在哪,至少排查问题时能少走弯路。
4.3 分组引用与 &:不只是替换这么简单
s命令里有两个特殊的"引用"符号。&代表整个正则匹配到的内容,而\1、\2这类代表第 1、第 2 个括号分组捕获到的内容。
&的使用场景是"在原匹配的基础上做包装"。比如把所有数字加方括号:
sed -E 's/[0-9]+/[&]/g' data.txt它会先把每一串数字匹配出来,然后用[&]把匹配内容包进方括号。另一个例子是给年份加后缀:
sed -E 's/2024/&年/g' file结果就是所有2024变成2024年。
分组引用则更适合"打乱重组"的操作。比如交换一行里的前两个单词:
sed -E 's/([^ ]+) ([^ ]+)/\2 \1/' names.txt再比如手机号脱敏,保留前 3 位和后 4 位,中间打星号:
sed -E 's/(1[3-9][0-9])[0-9]{4}([0-9]{4})/\1****\2/g' phones.txt这里(1[3-9][0-9])捕获前三位,([0-9]{4})捕获后四位,中间四位直接替换成****。
再强调一个细节:如果替换文本里需要出现字面意义的&,必须写成\&,否则sed会把&当成整个匹配去展开。这个细节容易在生成一些包含&的代码时踩坑。
5. 实战案例:脱敏、批量改配置和管道解析
5.1 原地修改与备份,别拿线上文件开玩笑
sed默认输出到屏幕,原文件不变。如果想真正修改原文件,需要加-i选项。它的含义是"in-place",原地写回。
sed -i 's/8080/9090/g' app.properties一条命令下去,文件里的端口就全变了。但-i有个安全隐患:万一正则写错了,文件就被改坏了,而且没有撤销的机会。所以我几乎每次用-i都会顺带备份:
sed -i.bak 's/8080/9090/g' app.properties这个写法会生成一个app.properties.bak的备份文件,修改后再比对确认无误,再把备份删掉。成本极低,收益极高。
这里还要提一个跨平台差异。macOS 自带的 BSD sed 对-i的参数要求更严格,如果你想不备份直接改,必须写成sed -i '' 's/8080/9090/g' file,空字符串表示"不想要备份后缀"。同样的写法放在 GNU sed 上,行为不一致,容易出问题。我的建议是:如果你在写跨平台脚本,与其纠结两种 sed 的差异,不如直接避开-i,用重定向的方式:
sed 's/8080/9090/g' app.properties > app.tmp && mv app.tmp app.properties这样在任何平台的 shell 里行为都是一致的。
5.2 日志脱敏:IP 和手机号这样打码
线上日志里经常混着 IP、手机号、身份证号之类的敏感信息,发布前需要脱敏。这种批量替换正是sed的强项。
IP 地址打码:
sed -E 's/([0-9]{1,3}\.){3}[0-9]{1,3}/x.x.x.x/g' access.log这个正则会匹配192.168.1.10这类 IPv4 地址,替换成x.x.x.x。还能进一步做到保留前两位、后两位打码,这就看你需要什么粒度的脱敏了。
手机号脱敏,保留前三位和后四位:
sed -E 's/(1[3-9][0-9])[0-9]{4}([0-9]{4})/\1****\2/g' users.txt如果日志文件太大,脱敏完之后你还要统计一下改动范围,可以配合-n和p标志,只输出发生替换的行:
sed -nE 's/([0-9]{1,3}\.){3}[0-9]{1,3}/x.x.x.x/gp' access.log | wc -l这里g是全局替换,p是每替换成功一行打印一次,加上-n取消默认输出,最终管道统计出的行数就是被脱敏过的行数。
5.3 for 循环批量处理:一切还可以再自动化一步
sed单文件处理只是基础,实际工作中经常要批量处理一堆同类型文件。比如把/opt/config/下所有.conf文件里的注释行删掉、把127.0.0.1替换成0.0.0.0:
for f in /opt/config/*.conf; do sed -i.bak '/^#/d; s/127.0.0.1/0.0.0.0/g' "$f" done一个需要注意的坑:文件名可能包含空格。所以for循环里引用文件变量一定要加双引号,写"$f"而不是$f。另一个细节是,如果目录里一个.conf文件都没有,通配符*.conf会原样保留成一个不存在的路径,循环就会进入一次空转。想避免这种情况,在脚本开头加shopt -s nullglob可以解决,它会让没有匹配的通配符直接变成空列表。
批量修改这种事,sed -i每个文件都会重写一遍,效率足够,但请务必在跑之前先拿一两个文件做测试,确认正则的效果符合预期。
5.4 从管道输出里挑行:很多时候根本不用写脚本
sed最常见的隐藏技能,其实是处理其他命令的标准输出。比如ps aux只看表头之后的前两行:
ps aux | sed -n '1,3p'再比如netstat -an的输出前三行是标题和统计信息,直接跳过:
netstat -an | sed '1,3d'在adb shell场景里也很常见。比如列出所有安装包,只想找某个应用:
adb shell pm list packages | sed -n '/com.tencent/p'这里的思路是,凡是场景是"我只想要某几行""我想给输出加个前缀""我想把输出的某些字段打码",都可以在管道末尾挂一个sed搞定,完全不需要写一个完整的脚本文件。
有一个小习惯我一直推荐:当你想从命令输出里取某几行时,sed -n '2,5p'比head -5 | tail -4的组合更直观,因为它只表达一个动作:"取 2 到 5 行",不需要在两个命令之间来回换算行号。
6. 我不想你再踩的坑:sed 命令实战翻车现场
6.1 忘记 -n 造成的重复输出
前面反复强调过-n的问题,因为它是我见过最多新人翻车的点。举一个具体例子:你想把日志里匹配ERROR的行单独打印出来,于是写了sed '/ERROR/p' app.log。结果终端哗哗地输出所有行,而且每一行 ERROR 还出现了两次。表面上看是"输出重复了",本质上是"默认输出 + p 命令"叠加了。
这类问题的排查思路很简单:你先问自己到底想要什么效果。如果你想要的是"只输出匹配行",那就该加-n;如果你想要的是"输出所有行,但匹配行额外再看一遍",那不加-n也没毛病。为了避免下意识犯错,建议把sed -n '/ERROR/p'这种组合当成一个固定搭配来记。
6.2 定界符冲突:替换路径时尤其常见
假设你想把文本里的http://www.example.com替换为空,如果直接用/当定界符,表达式会碎成一地:
sed 's/http://www.example.com//' file # 报错,或者结果奇怪因为第一个/出现在s后面当定界符,第二个/是http://里的斜杠,第三个/是结束定界符。整个表达式的结构直接被内容里的斜杠干扰了。
解决办法就是换定界符。我会优先用#或者|:
sed 's#http://www.example.com##g' file sed 's|http://www.example.com||g' file关键是记住:s后面的第一个字符就是定界符,之后所有同字符再次出现的位置都会被当成定界符解析。所以选定界符的原则是"内容里大概率不会出现的字符"。
6.3 转义的层级问题:Shell 一层,sed 一层
在 shell 里写sed命令,转义是两层叠加的:shell 解释一次,sed再解释一次。如果你把正则表达式用双引号包起来,shell 会先处理掉一波反斜杠,最后到sed手里的正则可能已经不是你想的那个样子了。
举个匹配反斜杠的例子。Windows 风格路径C:\tmp里的反斜杠,在正则里需要写成\\才能匹配一个真正的反斜杠。如果你用双引号写:
sed "s/C:\\tmp/\/data/g" fileshell 会先把\\变成单个\,于是sed收到的是C:\tmp,在正则里\t会被解释成制表符,完全不是你想要的效果。
我的铁律是:凡是sed表达式,一律用单引号包起来,除非你明确需要 shell 做变量展开。比如sed "s/$old/$new/g"这种需要变量插值的场景才用双引号。单引号内的一切内容都会原样传给sed,至少省掉一层心智负担。
6.4 不敢 -i:先测试再动手的铁律
-i用好了是神器,用坏了是事故。最常见的翻车方式是:正则写的没问题,但忘了文件里还有些特殊行也被误匹配了,然后-i一下,几百行数据悄悄变掉。
我个人的工作流是:
head -5 file | sed 's/要替换的/替换成/g'先用head取几行样本跑一遍,确认输出效果。然后正式执行时带备份后缀-i.bak:
sed -i.bak 's/要替换的/替换成/g' file diff file.bak file # 确认改动内容符合预期最后再决定要不要把.bak删掉。这套流程虽然多两步,但在大规模批量处理文件时,多这两步节省的排查时间是按小时算的。
6.5 非 ASCII 字符与 locale 问题
sed处理 UTF-8 中文时,大多数场景下是正常的,但如果当前环境的 locale 设置不正确,可能会出现"Invalid or incomplete multibyte or wide character"的错误,或者正则匹配发生诡异的偏移。
我之前在处理一份中英混排的文本时,就遇到过sed -E 's/[a-z]+//'把后面的中文字符也误删的情况。排查后发现是终端会话的LANG没设对,系统使用了不合适的字符集去解释输入流。解决办法是把环境变量指到 UTF-8:
export LANG=C.UTF-8如果你的文本以中文为主,而且正则逻辑很复杂,我的建议是别硬用sed,直接考虑 Perl:perl -pi -e 's/模式/替换/g' file。Perl 对 Unicode 的支持比传统的sed要强得多,复杂字符操作会更省心。
7. 关于 sed,我已经养成的几个使用习惯
前面把原理、命令和坑都讲得差不多了,最后分享几个我在实际使用中沉淀下来的习惯,谈不上标准答案,但确实帮我省了很多事。
第一,我给自己定了一条硬规矩:涉及数据改动的sed -i,前面一定先有测试步骤,要么是head看样本,要么是-i.bak留备份。线上环境改坏了文件,再牛的补救手段都不如一开始留条退路。
第二,简单命令我直接写一行,复杂一点的命令我会写成脚本文件,再用sed -f script.sed file来跑。这样做的好处是命令里的空格、分号、转义字符都是固定的,不会因为换了终端、换了 shell 环境就出幺蛾子。脚本文件里还能加注释,过两个月回来看,还能大致明白当时想干什么。
第三,我对工具的选择有一个非常明确的分工:单纯找行用grep,改行批处理用sed,按列算数、生成报表用awk。如果某个需求的正则复杂到让我开始怀疑人生,那我果断切到perl -pi,不为难自己,也不为难sed。工具本来就是为需求服务的,没有哪个命令是必须死磕到底的。
最后说一个很实用的判断标准:如果你能把"取行、删行、替换、插入"这四类操作在 3 秒内用sed写出来,你的命令行熟练度已经超过大多数人了。再往上进阶,把模式空间、保留空间和多行处理的机制吃透,你会发现自己不仅能更快地写出命令,还能在你写之前就大概预判出sed会输出什么。这个预判能力,才是真正"懂 sed"的标志。