Linux sed 命令高级应用:从模式空间到实战

发布时间:2026/10/17 1:05:31
Linux sed 命令高级应用:从模式空间到实战
这套Linux文本处理实验课程做到第十三章标题叫“sed额外总结”。我起初看到“额外”两个字没太当回事——前十二章已经把增删改查、正则、流处理都过了一遍还能“额外”出什么花来但这一章走下来我才意识到之前的理解有多粗糙sed根本不是只会s/xxx/yyy/g的替换小工具而是一台小巧、极致的文本状态机。写这篇总结就是想把这些零散的知识点——模式空间、保持空间、地址范围、多行模式、替换的细节——按项目实战的逻辑重新串一遍同时把实验里反复踩过的坑和可以直接抄的用法一并整理出来。适合刚学完sed基础、想真正用于日常文件处理的人也适合配置批量修改、日志清洗这类场景要落地但不愿意写一堆脚本的人。1. 别急着背命令先想清楚这个需求该不该用 sed1.1 “流式编辑器”的定位和适用边界sed 全称是 stream editor翻译过来就是“流式编辑器”。这里的“流”字特别重要它决定了 sed 的一切行为模式从输入流中每次读取一行放到一块缓冲区里然后依次执行你给它的命令处理完这一行缓冲区清空再读下一行直到文件末尾。这个设计和 vim 这种全屏编辑器完全不同。vim 是把整个文件装进内存随便跳来跳去改sed 从来不看整个文件它只关心“当前这一行”。所以 sed 在处理超大文件时优势非常明显。我实测过处理一个几个 GB 的日志文件sed 做全局替换几乎是秒级完成内存占用稳如老狗如果用 Python 脚本做同样的事光启动解释器就是一大开销还容易把整份文件读进内存把自己搞崩。明白了这个定位你就能画出一条清晰的分界线只要需求是“逐行处理文本、做筛选、替换、删除、局部修改”sed 都是第一梯队的选择。但要记住它的短板——它没有像 awk 那样的自动字段拆分和计算能力也没有 Python/Perl 那样的变量、数组和复杂控制结构。真遇到“按列求和”“多条件统计分组”“根据前几行的内容决定后面怎么处理”这种需求不如直接用 awk 或写个几十行的小脚本别跟 sed 死磕。1.2 十二个课时之后大多数人到底卡在哪儿我观察过不少一起做这套实验的同学大家的 pattern 也差不多基础命令能背但把几个命令组合起来就发怵知道s能替换却说不清s命令的完整结构到底有哪些标志位知道p和d能打印、删除但碰到/begin/,/end/这种范围匹配就一脸懵更别提保持空间和多行模式这几座大山了。其实这些问题都指向同一个根源对 sed 的底层机制——模式空间、地址匹配、循环流程——理解还停留在“记住语法”的层面没有真正转化成“搭建处理流程”的思维。所以这份额外总结我不会再按命令清单的方式讲而是把这些机制挨个拆开再落到可以直接复制的操作上。你会发现很多你以为很玄的 sed 写法拆穿了就一句话的事。2. 模式空间与地址匹配sed 的一切都建立在这两块基石上2.1 模式空间sed 的“工作台”在哪模式空间pattern space是 sed 最核心的概念没有之一。可以把它理解成 sed 内部的一小块临时缓冲区平时我们做的所有修改、打印、删除操作对象都是这块缓冲区里的内容。sed 的工作循环大致是这样从输入流读入一行存进模式空间。按顺序执行用户给的每一条 sed 命令。所有命令执行完毕后如果该行没有被d删除、也没有使用-n关闭默认输出模式空间里的内容就被输出到标准输出。清空模式空间回到第 1 步读下一行。这里有一个初学者最常忽略的事实在模式空间里修改内容不会直接改文件。文件里每一行始终是原来的样子sed 只是把“修改后的副本”打到屏幕上。想要真正修改文件要么重定向输出到一个新文件要么用-i选项。这一点后面我还会专门提因为它引发的“咦怎么没改”的困惑大概是 sed 相关提问里出现频率最高的。理解了模式空间你就理解了为什么 sed 的脚本总是“命令的序列”因为每条命令都是依次作用在同一块模式空间上的。举个例子sed s/ //g; s/[A-Z]/__/g names.txt第一条s命令把所有空格删掉第二条s命令把每个大写字母包上下划线两条命令操作的是同一块缓冲区先“洗掉空格”再“加标记”。如果你试图用两个独立的sed命令各做一步然后再想办法合并就绕了远路。2.2 地址与范围匹配“哪些行”是 sed 命令的主语sed 命令可以带地址也可以不带。不带地址的命令对每一行都执行带一个地址的命令只对匹配的行执行带两个地址的命令则定义了一个“范围”范围内所有行都执行。三类写法按顺序理一下无地址sed s/a/b/每一行都替换。单个地址sed 3d只删除第 3 行sed /error/d删除包含 error 的行。两个地址sed 2,5p打印第 2 到第 5 行sed /START/,/END/p从匹配 START 的行开始打印直到匹配 END 的行结束。很多人对两个地址的机制有误解以为是“两个条件同时满足才处理”。实际上双地址范围的工作方式是状态触发从遇到第一个地址的行开始范围“打开”此后的每一行都命中直到遇见第二个地址的行范围“关闭”。也就是说/BEGIN/,/END/会命中 BEGIN 和 END 之间的所有行而不是“同时包含 BEGIN 和 END 的行”。这个区分在实际中非常重要。比如你有一份配置文件中间夹着一段自动生成的内容# BEGIN 自动生成段 gateway192.168.1.1 dns192.168.1.1 # END 自动生成段你想删除这段正确写法是sed -i /BEGIN 自动生成段/,/END 自动生成段/d app.conf这段命令会把整个区间删掉符合预期。但如果你头脑里把它理解为“同时包含两个关键字的行”写出来的命令就会完全跑偏——你会试图用/BEGIN.*END/去匹配单行结果一段也删不掉然后在群里抓狂。还有一个 GNU sed 特有的细节值得提一下范围结束的那一行如果恰好又匹配了开始地址GNU sed 会立即重新打开范围。例如文件内容是START END START END用/START/,/END/p处理GNU sed 会连续打印四行但如果换一个严格遵循 POSIX 的 sed 实现行为可能不一样——第三行的 START 不会被识别为新范围的开始因此只会打印前两行。这个差异平时遇到的概率不高但一旦你的脚本要跨平台跑比如在 Linux 和 macOS 上共用就很容易成为隐蔽的坑。我自己的习惯是尽量不死磕这种边缘行为用awk做状态处理更可控后面专门讲。2.3 地址与命令组合把多个 sed 命令绑到同一组行上如果想让一串命令只对特定行生效就要用花括号把命令组合起来sed -n /^ERROR/{;p;q} service.log这条命令的意思是找到以 ERROR 开头的行先输出它所在的行号再输出它本身的内容然后立刻退出。命令输出当前行号p打印模式空间q退出 sed。这套组合拳在做“查日志快速定位第一条错误在哪一行”时非常好用。花括号内的多条命令需要用分号分隔。我给出一个实操建议最后一条命令后面不要习惯性加分号不是因为会报错而是为了可读性——你在给别人讲解和维护脚本的时候一眼能看清“这个块里有哪几条命令”比“分号多一个少一个不报错”重要得多。用换行写会更清楚sed -n /^ERROR/{ ; p; q } service.log另外要记得p和-n通常是成对出现的。如果你不加-n而命令里又有p那么命中 ERROR 的那一行会被打印两次——默认输出一次p命令又输出一次。我刚学的时候经常被这种“多打一遍”搞到自我怀疑建议你直接把“需要精确控制输出时永远带上-n”当成肌肉记忆。3. 保持空间与多行模式sed 的进阶分水岭3.1 保持空间一块专门用来暂存数据的“草稿纸”如果说模式空间是工作台那保持空间hold space就是旁边的一块草稿纸专门用来临时存放数据。它初始是空的和模式空间之间通过几个命令搬运数据h把模式空间内容复制到保持空间覆盖H把模式空间内容追加到保持空间换行分隔g把保持空间内容复制回模式空间覆盖G把保持空间内容追加到模式空间换行分隔x交换模式空间和保持空间的内容大部分人在第一次接触 sed 时根本不会用到保持空间因为单靠模式空间就已经能做很多事但一旦要处理“跨行数据”保持空间就是无可替代的关键。我印象最深的经典例子是“倒序输出整个文件”sed -n 1!G;h;$p file.txt这个命令看起来很吓人拆开就不难了。1!G意思是“非第一行”才执行G把保持空间的内容追加到模式空间末尾h在每一行处理完都执行把当前模式空间整个保存回保持空间$p只在最后一行时打印模式空间。逐行推演一下文件三行内容是 a、b、c第一轮模式空间是 a保持空间存下 a第二轮模式空间是 bG把 a 追加进来变成 b\nah存回去第三轮模式空间是 cG把 b\na 追加进来变成 c\nb\na最后打印结果就是 c、b、a。整个过程没有创建任何临时文件全在两块缓冲区里完成非常优雅也非常绕。说实话这种写法并不适合用到日常生产脚本里但它能帮你检验对模式空间和保持空间的理解是否透彻。如果“G追加了什么、h覆盖了什么”你能一眼说清那 sed 的机制你就过关了。3.2 多行命令 N、D、P超越“一行一处理”的钥匙普通的n、p、d只处理模式空间里的一行内容而大写版本的N、P、D涉及模式空间里的多行内容是解决跨行问题的第二把钥匙。先说被用得最多的N把下一行读进来追加到当前模式空间后面两行之间用换行符分隔。它最常见的用途就是“把两行合并成一行”sed N; s/\n/ / example.txt文件内容如果是两行一对的“属性名 / 属性值”处理完就变成“属性名 属性值”的单行。N读取两行到模式空间s/\n/ /把中间那个换行符替换成空格再默认输出搞定。需要注意N在文件末尾的行为很特殊如果已经读到文件最后一行而没有下一行可读GNU sed 会直接终止整个脚本不再执行后续命令。这就出现了一个经典现象文件行数是奇数时最后一行在N; s/\n/ /的处理里可能会“消失”——因为最后一行奇数行没有组成一对N失败导致整个脚本静默退出这一行根本来不及正常输出。我吃过这个亏后养成一个习惯凡是涉及N的多行处理都先确认文件行数是不是偶数或者加一层$的兜底处理。这段经验放到底部“常见问题”里再细说。再说P和D。P打印模式空间里的第一行到第一个换行符为止D删除模式空间里的第一行并把剩余内容拿回来重新执行脚本——注意D不会自动读入新的一行它让 sed 的指令循环“从头开始跑一遍但处理的是同一块模式空间里剩下的内容”。这种“分而治之”的模式空间操作在解析固定格式的多行记录时非常有用。3.3 什么时候该果断放弃 sed 转用 awk讲到多行模式我必须说句实话sed 的N、P、D和保持空间如果搭配出复杂流程脚本可读性会急剧下降。像我上面倒序输出的那个例子每次自己看都要在纸上推演好几遍更别说让同事接手了。现实的经验是一旦业务流程里出现“遇到某个标记开始、遇到另一个标记结束、期间要参考前面几行的状态来决定当前行怎么处理”这类需求直接换awk。awk 里你可以自然地使用变量、数组、流程控制逻辑清晰得多。比如“从 BEGIN 标记到 END 标记之间统计某个字段的和”awk 二十行内写得明明白白换成 sed 会痛苦到怀疑人生。所以我的建议是一个务实的分工sed 负责所有“我能一行或几行说清楚、只是稍微花哨一点”的文本变换替换、提取、删除区间。awk 负责“需要维护状态、需要计算、需要跨行记忆信息”的复杂处理。如果逻辑复杂到 awk 都难读就该上 Python 了毕竟工程可维护性一定是第一位的。4. 替换命令的精细控制把 s 命令的潜力榨干净4.1 s 命令的完整结构从 g 到数字、p、i、ws命令可能是 sed 用户日常用得最多的命令但很多人对它的理解停留在s/old/new/g这一层。实际上它的完整形态是s/正则表达式/替换文本/标志分隔符不一定是斜杠。只要替换文本和正则里不包含该字符任何字符都能当分隔符。最常见的场景是替换路径因为路径中充满斜杠sed -i s#/etc/apt/sources.list#/etc/apt/sources.list.bak#g app.conf如果还傻傻用/当分隔符你就得一行里写好几个转义斜杠\/可读性直接崩塌。换成#后命令清爽得多。同理|、:、也都是常用替代分隔符。再来说标志位每个都值得记g全局替换。没有它一行内只替换第一处匹配。数字s/a/b/2表示只替换每一行第二个匹配到的 a。这个在 GNU sed 和多数较新的 BSD sed 里都支持。p打印模式空间。配合-n使用时只输出发生替换的行这在调试时非常好用。i或I忽略大小写匹配。w file把发生替换的行写入指定文件。eGNU 扩展把匹配到的文本当作 shell 命令执行并用命令输出替换它。这个功能很强但也容易失控不建议对不可信输入使用。举个例子日志文件里把“错误级别”字段统一改成大写并导入另一个文件sed -n s/levelerror/levelERROR/w errors.txt app.log-n关闭默认输出p标志打出替换结果w又把同样的行写入 errors.txt。如果只想看到结果不住文件去掉w即可。4.2 捕获组与文本重排用 () 把内容拆开重组正则表达式里的捕获组在 sed 替换中极其常用。默认写法是\(和\)如果加上-E参数就能直接用干净的括号(和)我强烈建议所有新写的 sed 脚本都用-E——它是一致性的现代写法还免去了一堆反斜杠的视觉噪音。配合捕获组替换文本里用\1、\2等引用对应位置的内容用引用整个匹配内容。举个实际例子把姓名-编号格式的列表转换成编号:姓名echo alice-01 bob-02 | sed -E s/([a-z])-([0-9])/\2:\1/输出为01:alice 02:bob这条命令的含义用括号把字母段和数字段分别捕获替换时把捕获到的第 2 组放在前面、第 1 组放在后面中间加冒号。没有捕获组的话这个需求就要靠复杂的反向引用才能实现。也有它的重要用途。想给所有匹配到的数字加上中括号echo disk 100g | sed -E s/[0-9]/[\]/输出disk [100]g。注意替换文本里的前面要加反斜杠写成\否则 sed 会把它当成“整个匹配”而非字面量字符。这个细节很容易踩坑当你真的想让输出里出现一个符号时必须转义对应地转义后的\就是一个实在的 符号。GNU sed 还提供一组大小写转换的扩展在替换文本里用\U到\E之间包住的内容全转大写\L到\E之间全转小写\u转首字母大写\l转首字母小写。比如把配置里的name: alice改成Name: Alice就不需要写两个替换echo name: alice | sed -E s/(\w): (\w)/\u\1: \u\2/输出Name: Alice。这个功能不常用但一旦需要能省下不小的麻烦。4.3 正则的坑贪婪匹配、字符类与中文处理sed 的正则和 Perl/Python 正则最大的差别是它默认是贪婪且基础的老式正则没有\d、\wGNU 支持一部分 Perl 风格的\w但兼容性一般也没有非贪婪的*?。所以当你写s/.*: //想删掉“第一个冒号之后”时你实际删掉的是“最后一个冒号之后”——这就是贪婪匹配最容易吓到人的地方。解决思路不是去贪“非贪婪”运算符没有而是用否定字符类把范围卡死。想删掉“冒号加空格之后的内容”正确的写法是sed -E s/^[^:]*: //[^:]匹配任意非冒号字符*继续匹配直到遇到第一个冒号为止于是你精确地匹配到了第一个冒号前的内容。字符类也是容易混淆的重灾区。[[:space:]]匹配所有空白字符空格、Tab、换行等[[:digit:]]匹配数字[[:alpha:]]匹配字母它们和\s、\d这类简写不一样在各种 sed 实现里兼容性更好。在服务器上写脚本时我一般优先用[[:digit:]]而不是\d宁慢勿错。还有一个很实际也很坑的问题中文字符的多字节处理。如果你的 shell 的 locale 是C或POSIXsed 会把每个中文字符当作两个或多个字节来匹配s/你/XX/g这种替换的结果大概率是乱码。处理中文文本之前请确保环境变量是 UTF-8export LC_ALLC.UTF-8 export LANGen_US.UTF-8在 UTF-8 的 locale 下面sed 的正则能正确把多字节字符当成一个字符来匹配中文替换才安全。5. 三个可以直接抄走的实战场景5.1 批量修改配置从备份到区间删除一步到位服务器上临时改配置最常见的是批量替换某个域名或 IP。假设要统一把所有api.example.com换成internal.local并且想保留一份修改前的副本直接一行搞定sed -i.bak -E s#https://api\.example\.com#http://internal.local#g app.properties这里有两个关键点。第一-i.bak会生成一个app.properties.bak备份文件这是可预期的行为不是 出故障 了如果你完全不需要备份就用-i不带后缀。第二api.example.com里的.是“匹配任意字符”的正则元字符如果不写成\.它会误匹配apiXexampleYcom这样的意外内容——我在一次配置批量处理里就因为这个差点把一堆无关域名也替换掉后来养成了“一切点号必须显式转义”的习惯。配置里经常还藏着自动生成的段落想整体清理掉就用范围删除sed -i /# BEGIN 迁移块/,/# END 迁移块/d nginx.conf这条命令在前面提过不重复。更精细一点的场景是在指定区间内再做替换。比如你只想在“实验配置区间”内把enablefalse改成enabletrue其他地方的enablefalse不动sed -i /# 实验配置区间/,/# 配置区间结束/ s/enablefalse/enabletrue/ app.ini注意这里的写法先写地址范围空一格再接s/.../.../意思是“让这条替换命令只在这个范围内执行”。这是 sed 里“地址 命令”的标准组合理解了它的结构你会发现自己能拼出很多实用的一行命令。5.2 数据清洗行尾、空行、首尾空白一次清干净拿到一份从 Windows 拿来的文本文件最常见的问题就是行尾带着\r在 Linux 上显示成^M或影响后续工具的解析。统一清除sed -i s/\r$// data.csv空行删除也很常见但是要注意很多文件的“空行”其实包含空格或 Tab。用严格^$匹配会漏掉这些“看起来是空行、实际有空白字符”的行正确写法是用 POSIX 字符类sed -i /^[[:space:]]*$/d data.txt给所有行“减肥”去掉行首行尾空白sed -i -E s/^[[:space:]]//; s/[[:space:]]$// data.txt这里同时用了两条s命令用分号分隔。如果你想照顾可读性也可以用-e s/... -e s/...的写法。两条替换的顺序是先去掉行首空白再去掉行尾空白因为如果先删行尾再删行首执行结果是等价的但用分号的命令顺序就是实际执行顺序你需要对依赖关系有清晰认识。删除表头、只保留前 N 行这类行级裁剪也很常用比如删除第 1 行到第 5 行sed -i 1,5d report.csv在某个匹配行之后插入新内容、之前插入内容sed -i /^\[network\]/a proxy_timeout10 config.ini sed -i /^\[network\]/i # 网络配置段 config.ini多行插入用\n串起来sed -i /^\[network\]/a# 新加的超时配置\nproxy_retries3 config.ini这些都是基础操作里最能用上的几招每次都能保证稳定。5.3 合并多行与提取区间处理跨行数据的两把刀很多时候我们需要把跨行的文本拼成一行。比如日志里有些字段被折叠成了两行想压缩成一行方便后续 grepsed :a; N; $!ba; s/\n/ /g multiline.txt这条命令看起来神神叨叨拆开解释:a定义了一个名为 a 的标签N把下一行追加到模式空间$!ba表示“如果当前不是最后一行就跳回标签 a”循环结束时整个文件全部都在模式空间里最后s/\n/ /g把所有换行替换成空格。事实上这就是“把整个文件合并成一行”的经典 sed 三连。它可以用在一些极端场景比如把整个 HTML 压缩成一行传输但我必须坦白可读性确实一般而且遇到很大的文件或某些 BSD 版本时会有限制。如果只是两两合并用之前提的N; s/\n/ /更直白。另一个实用场景是提取区间内容。比如从 HTML 或日志中抓取一个区块sed -n /div classresult/,/\/div/p page.html或者从一个文本协议里抓取某个会话记录段sed -n /^Session Start/,/^Session End/p session.log需要注意正则里的/如果和分隔符冲突就用#分隔sed -n \#div classresult#,\#/div#p page.html这其实也顺便复习了分隔符替换的用法遇到路径、HTML 标签这类充满斜杠的内容用#能少写无数个转义反斜杠。6. 常见问题与排查技巧实验阶段踩过的坑6.1 高频问题速查表我把实验过程中自己和周围同学遇到过的高频问题整理成一个速查表每条后面附了原因和正确用法。现象原因解决办法执行 sed 后原文件没变sed 默认只输出到屏幕不写原文件想直接改文件用-i或重定向到新文件加了-n后没有任何输出-n关闭了默认输出但命令里没有p确认需要打印的行加了p命令同样的行被打印了两次没有加-n且命令里有p精确控制输出就成对使用-n和p/A/,/B/删掉了 A 和 B 之间所有行双地址范围是状态触发不是两个条件同时满足想同时满足两个条件请用/A.*B/或在 awk 里做在替换结果里变成了匹配内容替换文本中的是“整个匹配”的引用字面量要写成\文件末尾多了一个.bak文件-i.bak就是备份后缀这是预期行为不要备份就写-i不要加后缀中文替换后出现乱码locale 不是 UTF-8 时sed 按字节处理多字节字符设置LC_ALLC.UTF-8再运行奇数行文件用N处理最后一行不见了GNU sed 在N没有下一行可读时会终止脚本多行处理前确认文件行数或以$做兜底替换路径时反斜杠写到手抽筋分隔符用的是/路径里全是/改用#或 正则里的.匹配得比预期多.匹配任意字符在正则里是元字符匹配字面点号必须写成\.6.2 调试 sed 的实战小工具很多 sed 脚本写出来效果不对第一反应是抓瞎。我个人的调试流程基本固定成四步。第一步先用l命令看当前行的真实内容它能显示不可见字符sed -n l data.txt输出里\t代表 Tab、\r代表回车、$代表行尾这样你能立刻确认是不是 CRLF 或多余的空白字符在捣乱。第二步用快速确认匹配是否发生在预期行sed -n /error/ app.log只输出匹配行的行号先验证地址范围对不对再检查改动逻辑。第三步小范围试跑不写文件sed -n s/foo/bar/p app.log-n加p标志只打印“发生了替换”的行你可以直观确认替换结果是否符合预期而不需要看整个文件几十万行输出。第四步确认无误后再落盘。落盘前可以先备份sed -i.bak s/foo/bar/g app.conf diff app.conf app.conf.bak用 diff 检查备份和修改后的差异这个过程把一个黑盒操作变成白盒可查的变更记录。我在正式环境做批量修改时几乎都会保留一份.bak等运行一段时间确认无误后再清理心里踏实得多。最后还有一个心态方面的经验sed 脚本再复杂本质还是“逐行读、逐行改、逐行出”。碰到不理解的写法就在纸上把模式空间和保持空间的内容一步步写出来比瞎试参数快得多。保持空间和地址范围这两个概念吃透了你就能从“背命令的人”变成“会设计文本处理流程的人”。我自己把第十三章的实验重复做了三遍第一遍跟着走第二遍把命令都改成自己的场景第三遍把上面的坑一个个踩实。说句实话sed 这个工具虽然已经诞生几十年但现在每天处理配置文件、清洗日志、快速提取信息它依然是我最常用、最信任的兵器之一。但它也不是万能的真遇到复杂状态处理就果断换 awk遇到整体逻辑就上 Python工具之间配合着用才是让人最舒服的工作方式。