1. 为什么 sed 值得你花时间搞透
刚接触 Linux 那会儿,我对 sed 的态度就是“能用就行”——替换个字符串、删个空行,基本靠搜命令拼凑。直到有一次处理一批日志文件,几千行内容需要按规则批量改写,用交互式编辑器一个个改根本不现实,我才真正意识到:sed 不是“可选技能”,而是文本处理的基本功。它的全称是 Stream Editor,流编辑器,核心工作模式是逐行读取输入、按规则处理、输出结果,整个过程不需要打开文件、不需要人工干预,特别适合批量化、自动化的场景。
这篇文章面向的是已经会用 Linux 基本命令、但 sed 还停留在“复制粘贴别人命令”阶段的同学。我会从 sed 的工作模型讲起,把地址定址、模式空间、常用命令、正则配合这些核心概念拆开揉碎,再配上大量可以直接抄作业的实操案例。看完之后,你至少能做到:看到一条 sed 命令能读懂它在干什么,遇到文本批处理需求能自己写出命令,而不是每次都去搜索引擎碰运气。
sed 的价值在于它把“编辑”这件事从交互式变成了程序化。你用 vim 改一个文件,改完就完了;但你用 sed 写一条命令,它可以套用到一万个文件上。这种从“手工”到“自动化”的跨越,才是 sed 真正值得投入时间的原因。而且 sed 的语法一旦掌握,配合 grep、awk、find 这些工具,能组合出非常强大的文本处理流水线,这在日志分析、配置管理、数据清洗等场景里几乎是刚需。
2. sed 的工作模型与核心概念拆解
2.1 逐行读取:sed 到底是怎么“流”起来的
理解 sed 的第一步,是搞清楚它的执行模型。sed 处理文件的方式和 vim 完全不同:vim 是把整个文件加载到内存里让你交互编辑,而 sed 是逐行读取、逐行处理、逐行输出。具体流程是这样的:sed 从输入源(文件或管道)读取一行内容,存入一个叫“模式空间”(Pattern Space)的临时缓冲区,然后按照你给的命令对模式空间里的内容进行操作,操作完成后默认把模式空间的内容输出到标准输出,接着读取下一行,重复这个过程,直到所有行处理完毕。
这个模型解释了很多 sed 的行为特征。比如为什么 sed 默认不会修改原文件?因为它只是把处理结果输出到标准输出,原文件根本没动。比如为什么 sed 处理大文件时内存占用很低?因为它一次只持有一行内容,不需要把整个文件加载进来。再比如为什么 sed 的某些命令(比如n、N、D)看起来行为很“怪”?因为它们本质上是在控制这个逐行读取的流程,改变模式空间的填充和输出时机。
模式空间之外还有一个概念叫“保持空间”(Hold Space),可以理解为一个辅助缓冲区。模式空间是当前正在处理的那行内容,保持空间则像一块草稿纸,你可以把模式空间的内容暂存进去,也可以从里面取出来。sed 提供了一组命令在两者之间搬运数据:h(模式空间覆盖到保持空间)、H(追加到保持空间)、g(保持空间覆盖到模式空间)、G(追加到模式空间)、x(交换两者)。这些命令单独看很抽象,但在处理需要跨行操作的场景时非常有用,后面会结合案例详细讲。
2.2 地址定址:告诉 sed “对哪些行动手”
sed 的命令格式是[地址]命令[参数],地址部分决定了命令作用于哪些行。如果不写地址,命令就作用于每一行。地址的写法非常灵活,常见的有以下几种:
- 行号:
sed -n '3p' file只打印第 3 行。sed -n '2,5p' file打印第 2 到第 5 行。 - 行号加步长:
sed -n '1~3p' file从第 1 行开始,每隔 3 行打印一次,即打印 1、4、7、10……行。这个在按固定间隔抽样时很好用。 - 正则匹配:
sed -n '/error/p' file打印所有包含 error 的行。sed -n '/^#/p' file打印所有以 # 开头的行(常用于提取配置文件里的注释行)。 - 范围匹配:
sed -n '/start/,/end/p' file打印从包含 start 的行到包含 end 的行之间的所有内容(含首尾)。这个在处理有明确起止标记的文本块时特别方便。 - 混合地址:
sed -n '5,/end/p' file从第 5 行开始,到第一个包含 end 的行结束。 - 取反:
sed -n '/error/!p' file打印所有不包含 error 的行。感叹号就是取反操作。
地址部分还有一个容易踩坑的地方:范围匹配是“贪婪”的。比如/start/,/end/会匹配从第一个 start 到第一个 end,然后继续找下一个 start 到下一个 end,以此类推。如果文件里 start 和 end 的配对关系比较复杂,范围匹配的结果可能和你预期的不一样。这时候要么用更精确的正则,要么用后面会讲到的保持空间来做状态判断。
2.3 常用命令速览:p、d、s、a、i、c、y
sed 的命令很多,但日常用得最频繁的就那么几个。先过一遍,后面再逐个展开:
| 命令 | 作用 | 典型用法 |
|---|---|---|
p | 打印模式空间内容 | sed -n '3p' file |
d | 删除模式空间内容,不输出 | sed '3d' file |
s | 替换 | sed 's/old/new/' file |
a | 在匹配行后追加文本 | sed '/pattern/a\new line' file |
i | 在匹配行前插入文本 | sed '/pattern/i\new line' file |
c | 替换整行 | sed '/pattern/c\replaced line' file |
y | 字符逐一替换 | sed 'y/abc/xyz/' file |
n | 读取下一行到模式空间 | sed 'n;s/old/new/' file |
N | 追加下一行到模式空间 | sed 'N;s/\n/ /' file |
h/H/g/G/x | 模式空间与保持空间互操作 | 跨行处理场景 |
这里要特别提醒一个新手最容易困惑的点:p命令默认会重复输出。因为 sed 处理完一行后本来就会自动输出一次,如果你再加一个p,那行就会被打印两次。所以p几乎总是和-n选项搭配使用,-n的作用是关闭自动输出,这样只有你显式用p的行才会被打印。这个组合sed -n '...p'是 sed 最常见的用法之一,务必记牢。
3. 替换命令 s 的深度解析与实战参数
3.1 s 命令的基本结构与替换标志位
s是 sed 里使用频率最高的命令,没有之一。基本格式是s/正则/替换内容/标志。分隔符默认是斜杠,但如果你的正则或替换内容里包含斜杠,可以换成其他字符,比如s|/usr/local|/opt|或者s#/old#/new#,这样就不用反斜杠转义了,可读性会好很多。
标志位决定了替换的行为方式,常用的有四个:
g(global):默认情况下,s只替换每行中第一个匹配项。加上g之后,替换该行中所有匹配项。这是最常用的标志,很多人写 sed 替换时忘了加g,结果发现只替换了一处,排查半天才发现是这个原因。p(print):打印发生替换的行。通常和-n搭配,只输出被替换过的行,方便确认哪些行被改动了。i(ignore case):替换时忽略大小写。比如s/error/ERROR/gi会把 error、Error、ERROR 都替换成 ERROR。- 数字:指定替换第几个匹配项。比如
s/old/new/2只替换每行中第二个 old。
这几个标志可以组合使用,比如s/old/new/gi表示全局替换且忽略大小写。但要注意g和数字不能同时用,因为g本身就是“所有”的意思,和“第 N 个”是矛盾的。
3.2 反向引用与分组:让替换更灵活
正则表达式里的括号()用于分组,在 sed 的替换部分可以用\1、\2等反向引用来引用分组匹配到的内容。这个功能在重组文本格式时非常有用。
举个实际场景:假设你有一批日期格式是2024-01-15,想改成15/01/2024。命令可以这样写:
sed -E 's/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/' file这里-E是启用扩展正则表达式,这样括号和花括号就不用反斜杠转义了。三个分组分别捕获年、月、日,替换部分用\3\/\2\/\1重新排列顺序。注意替换部分里的斜杠需要转义,因为斜杠是分隔符。如果觉得转义麻烦,可以换分隔符:
sed -E 's|([0-9]{4})-([0-9]{2})-([0-9]{2})|\3/\2/\1|' file这样清爽多了。反向引用最多支持\1到\9,也就是九个分组。如果分组超过九个,就需要用更高级的工具(比如 perl 或 awk)来处理了。
还有一个实用技巧:&代表整个匹配内容。比如你想给所有数字加上方括号,可以写sed 's/[0-9]\+/[&]/g'。&在这里就代表被匹配到的数字本身。这个在给匹配内容加前后缀时特别方便,不用再写一遍正则。
3.3 替换命令的常见坑与规避方法
坑一:特殊字符未转义。替换内容里如果包含&、\、分隔符本身,都需要特别注意。&在替换部分有特殊含义(代表整个匹配),如果你就是想插入一个普通的&符号,需要写成\&。反斜杠本身要写成\\。这些在批量处理配置文件路径时经常遇到。
坑二:贪婪匹配导致替换范围过大。正则默认是贪婪的,.*会尽可能多地匹配字符。比如s/".*"/"new"/会把从第一个引号到最后一个引号之间的所有内容都替换掉,而不是你预期的每一对引号。这时候需要用非贪婪匹配(sed 的扩展正则支持.*?,但基础正则不支持),或者用更精确的字符类[^"]*来限制匹配范围。
坑三:只替换了第一个匹配。前面提过,忘了加g是最常见的原因。但还有一种情况:如果你的正则里用了^或$锚定,那每行自然只有一个匹配位置,加不加g效果一样。
坑四:替换内容里的反斜杠被吞掉。在 shell 里,sed 命令本身可能被 shell 先解析一遍,反斜杠的层数容易搞混。一个实用的排查方法是:先用echo把命令打印出来看看 shell 解析后的结果,确认反斜杠数量对不对。
实操心得:写复杂的 sed 替换命令时,先用
echo "测试文本" | sed '你的命令'在小样本上验证,确认无误后再套用到实际文件。这个习惯能帮你省下大量排查时间。
4. 删除、插入、追加与多行操作实战
4.1 d 命令:精准删除行
d命令删除模式空间的内容,并且不输出。配合地址使用,可以精准删除特定行。常见用法:
- 删除空行:
sed '/^$/d' file - 删除注释行:
sed '/^#/d' file - 删除行尾空白:
sed 's/[[:space:]]*$//' file(这个其实是替换,不是删除行) - 删除指定范围:
sed '5,10d' file删除第 5 到第 10 行 - 删除匹配行及其后一行:
sed '/pattern/{N;d}' file,这里用N把下一行也读进来,然后一起删掉
删除操作在清理数据时特别常用。比如处理 CSV 文件时,删除表头、删除空行、删除不符合格式的行,都可以用d命令组合完成。
4.2 a、i、c:插入与替换整行
a(append)在匹配行之后追加内容,i(insert)在匹配行之前插入内容,c(change)用新内容替换整行。这三个命令的语法在 GNU sed 和 BSD sed(比如 macOS 自带的)里略有差异,GNU sed 支持a\后面直接跟文本,也支持a text的写法;BSD sed 则要求在a\后面换行再写文本。跨平台脚本里要特别注意这个差异。
实际案例:给配置文件里所有[section]标题行后面加一个空行:
sed '/^\[.*\]$/a\\' config.ini这里a\\表示追加一个空行。注意 GNU sed 里a\后面的内容如果为空,就是插入空行。
再比如,给脚本文件批量添加 shebang 行(如果还没有的话):
sed '1i\#!/bin/bash' script.sh1i表示在第一行之前插入。这个在批量生成脚本模板时很好用。
c命令用得相对少一些,因为它会替换整行。比如把所有包含TODO的行替换成# 待处理:
sed '/TODO/c\# 待处理' file4.3 多行模式:N、D、P 的配合使用
sed 默认一次处理一行,但有些场景需要跨行操作,比如把两行合并成一行、处理跨行的 XML 标签等。这时候就需要用到多行模式命令:
N:读取下一行并追加到模式空间,两行之间用换行符\n分隔。D:删除模式空间中第一个换行符之前的内容,然后重新开始循环(不读取新行)。P:打印模式空间中第一个换行符之前的内容。
一个经典案例:把文件中所有以反斜杠结尾的行和下一行合并:
sed ':a; /\\$/{N; s/\\\n//; ba}' file这条命令稍微复杂,拆解一下::a定义一个标签 a;/\\$/匹配以反斜杠结尾的行;N读入下一行;s/\\\n//把反斜杠和换行符替换掉(即合并两行);ba跳回标签 a 继续判断。这样就能把连续多行以反斜杠结尾的内容全部合并成一行。
另一个常见需求:把段落内的多行合并成一行,段落之间用空行分隔。这需要判断空行作为段落边界,用保持空间来暂存内容。这类操作已经接近 sed 的能力边界了,实际工作中如果遇到复杂的跨行处理,我会优先考虑 awk 或 perl,sed 更适合行内操作和简单的跨行合并。
注意事项:多行模式命令容易写出死循环,尤其是
D和b标签配合时。建议先在测试文件上运行,确认行为符合预期后再用于生产数据。另外,N在读取到最后一行时如果没有下一行可读,GNU sed 会直接退出,BSD sed 的行为可能不同,跨平台时要留意。
5. 保持空间与高级技巧
5.1 h/H/g/G/x:模式空间与保持空间的搬运
保持空间是 sed 里最容易被忽视但也最强大的特性。它本质上是一块额外的缓冲区,初始为空。你可以把当前行暂存进去,处理完其他行后再取出来。五个命令的作用如下:
h:把模式空间内容覆盖到保持空间H:把模式空间内容追加到保持空间(带换行符)g:把保持空间内容覆盖到模式空间G:把保持空间内容追加到模式空间(带换行符)x:交换模式空间和保持空间的内容
一个经典应用:倒序输出文件。思路是逐行读取,每读一行就把它追加到保持空间的前面,最后统一输出。命令如下:
sed -n '1!G; h; $p' file拆解:1!G表示除了第一行之外,都把保持空间内容追加到模式空间后面;h把当前模式空间覆盖到保持空间;$p在最后一行时打印。这样每处理一行,保持空间里就多一行,且新行在前,最终实现倒序。
另一个实用场景:给文件每行加行号。虽然nl命令或cat -n更简单,但用 sed 实现能帮你理解保持空间的用法:
sed = file | sed 'N;s/\n/\t/'这里第一个 sed 的=命令输出行号(单独一行),第二个 sed 用N把行号和内容合并到一行,再把换行符替换成制表符。
5.2 分支与循环:b、t、T 命令
sed 提供了一套简单的流程控制命令,让脚本可以根据条件跳转:
b label:无条件跳转到标签t label:如果自上次读取新行以来有成功的替换,则跳转到标签T label:与t相反,如果没有成功的替换则跳转
这些命令配合使用,可以实现循环逻辑。比如前面提到的合并反斜杠结尾行的例子就用了b做循环。再比如,把每行中的多个连续空格压缩成一个:
sed 's/ */ /g' file这个用g就够了,不需要循环。但如果要处理更复杂的重复模式,比如把连续多个空行压缩成一个空行,就需要用循环:
sed '/^$/{N;/^\n$/D}' file这条命令的逻辑是:遇到空行时,用N读入下一行;如果下一行也是空行(模式空间变成\n),用D删除第一个换行符之前的内容(即删掉第一个空行),然后重新开始循环。这样连续空行就会被逐个“吃掉”,只留下一个。
5.3 实战:用 sed 处理结构化文本
假设有一个配置文件,格式如下,需要把所有key = value形式的行提取出来,并转换成key:value的格式:
# 配置示例 name = server1 port = 8080 # 注释行 timeout = 30命令可以这样写:
sed -n 's/^\([a-zA-Z_][a-zA-Z0-9_]*\) *= *\(.*\)/\1:\2/p' config.txt拆解:-n关闭自动输出;s命令用分组捕获键和值;^\([a-zA-Z_][a-zA-Z0-9_]*\)匹配合法的变量名;*= *匹配等号及两侧空格;\(.*\)捕获值;替换部分用\1:\2重组;最后的p只打印发生替换的行,这样注释行和空行自然就被过滤掉了。
这个模式在处理各种 key-value 格式的配置文件时非常通用,稍作修改就能适配不同的分隔符和格式要求。
6. 常见问题排查与避坑速查
6.1 替换不生效?先检查这几个地方
sed 替换不生效是新手最常遇到的问题,排查顺序建议如下:
- 有没有加
g?默认只替换第一个匹配,如果一行里有多个匹配项,后面的不会变。 - 正则写对了吗?特殊字符(
.、*、[、]、\)在正则里有特殊含义,如果是要匹配字面量,需要转义。比如匹配 IP 地址里的点,要写成\.。 - 大小写匹配吗?正则默认区分大小写,需要忽略大小写时加
i标志。 - 地址范围对吗?如果加了地址限制,确认地址是否匹配到了你预期的行。可以先用
sed -n '/pattern/p'确认匹配情况。 - 是不是用了
-i但没生效?-i是原地修改文件,GNU sed 和 BSD sed 的-i参数格式不同。GNU sed 用-i或-i.bak,BSD sed 必须跟一个参数,比如-i ''或-i '.bak'。
6.2 原地修改文件的安全姿势
-i选项让 sed 直接修改原文件,用起来很方便,但风险也大。一旦命令写错,原文件可能被改得面目全非。我的建议是:
- 先不加
-i运行一遍,确认输出符合预期。 - 加
-i.bak保留备份,这样即使改错了还能从.bak文件恢复。 - 对重要文件先手动备份,不要完全依赖 sed 的备份机制。
- 批量处理多个文件时,先用
find或ls确认文件列表,避免误伤。
# 安全做法:先预览 sed 's/old/new/g' file.txt # 确认无误后原地修改,保留备份 sed -i.bak 's/old/new/g' file.txt6.3 性能优化:处理大文件时的注意事项
sed 本身性能很好,但不当用法也会拖慢速度。几个优化建议:
- 尽量用地址缩小处理范围,不要对每一行都跑复杂正则。
- 避免在循环里反复调用 sed,能一条命令搞定就不要拆成多条。
- 大文件优先用
LC_ALL=C,这会让 sed 用字节模式而非字符模式处理,速度通常快不少,尤其是在处理纯 ASCII 文本时。 - 复杂的跨行处理考虑换 awk 或 perl,sed 的优势在于行内操作和简单流程控制,超出这个范围后强行用 sed 反而事倍功半。
常见问题速查表:
| 问题现象 | 可能原因 | 解决方法 |
|---|---|---|
| 只替换了第一个匹配 | 缺少g标志 | 加上g |
| 替换后文件没变化 | 没用-i或输出到了终端 | 加-i或重定向输出 |
| 正则不匹配 | 特殊字符未转义 | 检查并转义.*[等 |
-i报错 | BSD/GNU 语法差异 | BSD 用-i '',GNU 用-i |
| 多行命令死循环 | b标签跳转条件有误 | 检查循环退出条件 |
替换内容里的&异常 | &有特殊含义 | 写成\& |
7. 我的 sed 使用习惯与最后几条建议
用了这么多年 sed,我最大的体会是:不要试图记住所有命令,但要理解它的工作模型。模式空间、保持空间、地址定址、逐行处理,这四个概念搞清楚了,遇到具体需求时你就能推理出该用什么命令,而不是靠死记硬背。我到现在也会偶尔查手册确认某个命令的细节,但这不影响我高效使用 sed,因为底层模型是清晰的。
另一个习惯是:任何 sed 命令先在echo上测试。echo "样本内容" | sed '命令'这个模式几乎成了肌肉记忆。它能在不碰任何文件的情况下验证命令的正确性,成本极低,收益极高。尤其是涉及-i的原地修改,我一定会先用 echo 验证,再在副本上测试,最后才动原文件。
最后分享一个组合技巧:sed 和 grep、xargs 配合,能实现很多批量操作。比如批量修改当前目录下所有.conf文件里的某个配置项:
grep -rl 'old_value' --include='*.conf' . | xargs sed -i.bak 's/old_value/new_value/g'先用 grep 找出包含目标内容的文件,再用 xargs 把文件列表传给 sed 批量处理。这个模式在运维和配置管理中非常实用,比写循环脚本简洁得多。唯一要注意的是文件名里如果有空格或特殊字符,xargs 可能会出问题,这时候用-print0和xargs -0来配合处理。