sed命令实战总结:批量文本修改与流式编辑的隐形主力
去年年终我接了一大批存量脚本的维护里面全是日志清洗、配置文件批量改写之类的活。一开始我习惯用 grep 过滤、用 vim 手工改几百个文件改下来直接怀疑人生。后来把 sed 命令系统过了一遍才发现它才是文本处理的隐形主力——不显眼但凡是“批量改文本”的活儿sed 永远是最趁手的那把刀。这篇东西不是 man 手册的翻译而是我从实际项目中总结的 sed 使用经验核心模型怎么理解、替换怎么玩出花、多行怎么处理、原地修改有哪些坑、什么时候应该换工具。适合刚接触 sed 的人建立正确心智模型也适合已经会用但想提升效率的兄弟查漏补缺。1. sed 的真实定位流式改造工不是 grep 的加强版很多人第一次接触 sed 是因为 grep 不够用——grep 只能把匹配的行挑出来但我想把某些字符改掉、把某几行删掉grep 就无能为力了。这时候有人会说“用 awk 啊”但 awk 擅长的是按字段做统计和格式化输出对“第 2 行到第 10 行整体替换”“删除从 A 配置段到 B 配置段之间的内容”这类行级编辑操作awk 写起来反而绕。sed 的全名是 Stream Editor流编辑器。它不打开交互界面不让你光标移来移去而是把文本当成一条水流一行一行地从输入流里读进来经过你的命令加工再一行一行地写到输出流里去。这种设计决定了它的三个核心特性无状态、可重复每次处理都从第一行开始同样的输入必然得到同样的输出非常适合批量操作。管道友好cat file | sed ...或者sed ... file | grep ...随便拼它就是个中间加工环节。天然支持脚本化命令可以被-e参数、分号、脚本文件组织起来塞进自动化流程里。我整理过一个简单的工具定位表能帮你快速决定该用哪个工具强项弱项典型场景grep过滤、匹配行不能改内容查某类日志行sed行级增删改替换字段统计很别扭批量改配置、清洗文本awk按字段统计、格式化行级编辑逻辑绕计总和、提取列、生成报表vim交互式精细编辑不能批量自动化单个文件手工调整关键认知在这里grep 是过滤器awk 是计算器sed 是改造工。它默认对每一行执行你给出的编辑指令配合地址address可以精准圈定“对哪些行动手”。这里有一个容易忽略的点sed 默认会把模式空间的内容打印出来即使你没写p命令。所以最简单的sed s/old/new/ file会把每一行都输出一遍其中匹配到的行被替换没匹配到的原样输出。这个“默认打印”行为是新手看到“为什么我加了-n结果不一样”之类困惑的根源。理解它你才算真正迈进了 sed 的门。2. 从一次真实清洗作业看 sed 的工作模型模式空间与地址定址我先给你看一个真实场景。某次我需要处理一批 nginx 配置要求把listen 8080改成listen 18080同时把所有server_name那段里的域名后缀从.example.com统一改成.internal.example.com。文件里有注释、有空行、有奇怪的缩进。我先只盯着替换本身写了这么一条sed s/listen 8080/listen 18080/ nginx.conf如果只看结果它确实把目标字符串换掉了。但如果你想真正掌握 sed必须理解它背后发生了什么sed 从输入流读取一行文本放进一个叫“模式空间”pattern space的内存缓冲区然后按顺序执行你给出的命令所有操作都作用在模式空间里的这一行执行完所有命令后默认把模式空间的内容输出然后清空模式空间读入下一行重复这个过程。这就是 sed 的本质一个逐行处理的状态机。你写的每条命令都是在描述“在什么条件下对当前这行做什么操作”。2.1 地址定址只对部分行动手sed 命令基本格式是地址 命令。地址决定“管哪些行”命令决定“怎么改”。地址有四种写法我挨个说行号sed 3d file删除第 3 行sed -n 5p file只打印第 5 行。正则sed /error/d file删除所有包含 error 的行sed -n /^#/p file打印所有以#开头的行。范围sed /BEGIN/,/END/d file删除从匹配 BEGIN 的行到匹配 END 的行之间的所有内容包括这两行。步进sed 1~2d file删除奇数行。1~2的意思是“从第 1 行开始每隔 2 行处理一次”。没有地址就默认所有行这在 man 手册里叫省略地址等价于.*匹配所有行。很多新手写sed s/foo/bar/以为只改第一处匹配其实它是对每一行都执行一次替换只是默认只替换每行的第一处。这两件事容易搞混我后面会展开。2.2 用范围定址处理配置块回到 nginx 的场景。我想只改server块内部的listen不想碰全局的listen就可以用范围地址sed /^server {/,/^}/s/listen 8080/listen 18080/ nginx.conf这个命令的精髓是/^server {/,/^}/把范围限定在“以 server { 开头的行”到“以 } 开头的行”之间然后在这个范围内执行替换。sed 会记录当前是否处于该范围内这个状态是跨行维持的——这正是 sed“流式”处理里最强的地方之一它允许你用一对边界正则来圈定一个动态的行区间而不需要你先手动算出具体行号。我在实际改配置时经常用这种模式尤其是处理多个 server 块时——范围定址会让每一块内的改动都独立生效不会误伤其它区域。如果你用行号写死前面加了一行注释后面所有行号就全错位了而用正则边界就完全不怕这种“插入行导致偏移”的问题。3. 替换命令 s 的十八般武艺分隔符、标志位与反向引用s命令是 sed 里出场率最高的语法是s/旧内容/新内容/标志位。三根斜杠只是习惯写法不是硬性规定——只要第一个字符是分隔符后面就都认。这个特性帮你解决了一个非常常见的痛点处理路径、URL 这类本身带斜杠的文本时不用疯狂转义。比如我想把日志里的/var/log/app全部改成/var/log/app_archive# 用斜杠做分隔符需要写成这样看着就头疼 sed s/\/var\/log\/app/\/var\/log\/app_archive/ app.log # 换成 | 或者 # 做分隔符立马清爽 sed s|/var/log/app|/var/log/app_archive| app.log sed s#/var/log/app#/var/log/app_archive# app.log我自己的习惯是内容里有斜杠用|有竖线用#有井号用。选分隔符的目标只有一个——让表达式里的转义减到最少人的眼睛能少受点罪。3.1 标志位g、i、p、数字g是最常用的标志位意思是全局替换把每行里所有匹配都换掉而不是只换第一个。这是新手最容易踩的第一个坑# 只替换每行第一个 foo sed s/foo/bar/ file # 替换每行所有 foo sed s/foo/bar/g filei是忽略大小写在匹配配置项名或者日志关键词时很实用比如sed s/error/warning/i file。p是打印匹配行通常配合-n使用用来查看“哪些行会被替换”而不用真的改文件这是我在大规模改动前必做的“预演”sed -n s/old/new/p file还有数字标志位表示只替换第 N 次出现。比如sed s/://2 data.txt只把每行第二个冒号换成全角冒号这种需求在处理 CSV 这类格式时经常出现——第一个冒号是字段分隔符不能动后面的可以改。3.2 反向引用捕获组才是替换的灵魂s命令里最出彩的是反向引用。用括号\(和\)把匹配内容包起来在后面用\1、\2引用第 1、第 2 个捕获组。这让你能把“匹配到的东西”重新安排位置而不是简单地整体替换。举个例子把日志里2024-06-01 12:30:45这种时间格式转成20240601123045sed -E s/([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})/\1\2\3\4\5\6/ app.log注意我用了-E参数开启扩展正则这样写捕获组的括号就不需要反斜杠{4}这种量词也能直接用。如果不用-E就得写成\([0-9]\{4\}\)又长又丑。我的建议是能用-E的地方尽量用可读性提升不是一点半点。除了\1还有代表整个匹配内容。比如给所有出现的版本号加个方括号sed s/v[0-9]\\.[0-9]\/[\]/ version.txt在处理“在匹配内容前后加东西”的场景非常好用不用先捕获再引用省一步事。但要注意在替换部分里有特殊含义如果你真的要替换成字符就得写\这是个很容易翻车的细节。3.3 实战案例从配置里提取键值对我经常需要在脚本里动态修改配置。比如某个 Java 应用配置里有一行app.port8080我想只改端口号保留前面的 keysed -E s/^(app\.port)[0-9]/\18080/ app.properties^锚定行首(app\.port)捕获前缀后面的[0-9]匹配旧端口替换部分用\1保住前缀拼上8080。这比直接s/8080/8080改后的值/安全得多因为你只精准圈定app.port这一行不会误伤其他地方出现的相同数字。4. 不止替换删除、插入、追加与字符变换的组合拳新手容易把 sed 和替换划等号其实它的核心操作有六类替换s、删除d、插入i、追加a、修改c、字符变换y。每类配合地址都能玩出花样重要的是学会把它们组合成一条完整的处理流程。4.1 删除的三种姿势最基础的是按行号删sed 3d file删第三行。但实际场景更常见的是按条件删# 删掉所有空行 sed /^$/d file # 删掉所有以 # 开头的注释行 sed /^#/d file # 删掉 10 到 20 行 sed 10,20d file # 删掉从 DEBUG 日志段到下一个 INFO 段之间的内容 sed /DEBUG_START/,/INFO_START/d app.log注意范围删除是包含边界行的。如果想保留某一头就得动点脑筋。比如我想删掉/BEGIN/到/END/之间的内容但想保留 END 那一行可以写成sed /BEGIN/,/END/{/END/!d} file这里用到了大括号把多个命令组合在一起先定位范围再在范围内执行{...}里的命令/END/!d表示“如果不是 END 行就删”。这种“范围 花括号 条件”的组合是 sed 进阶的标志性写法已经有点编程的味道了。4.2 插入、追加与整段替换i在匹配行之前插入a在匹配行之后追加c整行替换。它们最常见的用法是给文件加头、加尾、替换整段。# 在第 1 行之前插入一行 sed 1i # Generated by deploy script file # 在所有 server 块开始之前插入一行注释 sed /^server {/i # server block start nginx.conf # 在文件末尾追加一行 sed $a # end of file file # 把整行 old config 替换成两行 sed /^old config/c\new config line1\nnew config line2 filec命令的一个细节是它会把范围内所有匹配行整行替换成指定的文本而且只输出一次替换结果不会对每一行都输出。这个行为和di组合在部分场景下有微妙差别实测时才注意到。大部分时候我直接用c简洁明了。4.3 字符变换不用写正则的批量映射y命令做的是字符级别的映射类似 tr 命令。它不像s那样匹配模式而是把源字符集合逐个对应到目标字符集合。比如统一引号# 把英文引号替换成中文引号这题用 s 写正则反而麻烦 sed y//“”/ article.txt更实用的场景是大小写强制转换比如把某些配置值统一成大写sed y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/ file。但说实话这种纯字符映射我一般直接用tr管道解决sed的优势还是在于能和地址定址结合。4.4 把多条命令串起来组合命令的方式有几种# 多个 -e 参数 sed -e s/foo/bar/ -e /^$/d file # 用分号 sed s/foo/bar/; /^$/d file # 写成脚本文件 # commands.sed # s/foo/bar/ # /^$/d sed -f commands.sed file我遇到比较长的处理链时会倾向用脚本文件因为可以加注释、好维护。比如一套标准的日志脱敏流程去掉空行、把 IP 后两段置零、把手机号中间四位打星号写成cleanup.sed后就能在多个项目里复用了。5. 原地修改 -i 与多行模式面对真实文件的完整操作前面所有例子我都没改源文件只是把处理后的内容打印到终端。但实际工作中你要的是“改完就保存”。这就要用到-i参数也是 sed 里风险最高的参数——用不好数据直接没了。5.1 -i 的机制与备份习惯-i的原理很简单sed 先把处理结果写到一个临时文件处理完再把临时文件重命名覆盖原文件。所以它不是真的“原地”修改而是通过临时文件实现的原子替换。标准用法# 直接覆盖原文件 sed -i s/old/new/g file # 带备份修改前先备份成 file.bak sed -i.bak s/old/new/g file我强烈建议凡是第一次用-i处理重要文件都带上备份后缀。处理完确认结果没问题再把.bak清掉。这不是保守是我真吃过亏某次改配置文件时正则写错了-i直接把整个文件处理成了几乎空白幸好当时带了.bak才能秒回滚。从那以后-i.bak成了我脚本里的固定动作。还有一个容易忽略的点sed -i在 macOS 的 BSD sed 和 Linux 的 GNU sed 上有差异。macOS 上-i后面必须跟一个后缀参数比如-i 表示不带备份而 Linux 上-i可以单独用。跨平台脚本要注意这个差异否则同样的命令在另一台机器上直接报错。5.2 配合 find 处理批量文件处理大量文件时sed -i通常和find组合。比如把某目录下所有.conf文件里的127.0.0.1替换成实际服务器地址find /etc/app -name *.conf -exec sed -i.bak s|127.0.0.1|10.10.1.20|g {} find ... -exec ... {} 会把所有找到的文件一次性传给 sed效率比{} \;高不少。这个组合是批量运维的黄金搭档我用了很多年。5.3 多行模式N、D 与模式空间的进阶玩法前面说 sed 默认一次处理一行但真实场景里很多结构跨行。日志里的堆栈信息、配置里的多行注释、SQL 里的跨行语句都是典型。sed 提供N命令来解决这个问题它把当前行的下一行也读到模式空间里和当前行叠加在一起用换行符\n分隔这样一次就能处理两行。经典的合并行例子把每两行合并成一行用逗号连接sed N; s/\n/,/ file每执行一次N模式空间里就有两行替换把中间换行符换成逗号输出合并结果然后继续读下面的行。注意因为模式空间被合并了后续的行读取节奏会跟着变——这就是“流式状态”的体现你必须有意识地跟踪模式空间里的内容。删除多行注释块的场景也常用 N。比如删除 C 风格注释/ * ... * /sed -E /\/\*/{:a; N; /\/\*/!{$!ba}; s/\/\*.*\*\///g} source.c这个命令不是给新手看的但它揭示了一个重要的点sed 支持用:label定义标签、用b或t跳转这门语言其实图灵完备。复杂的多行处理逻辑要靠这种标签循环来实现。我自己写这种复杂命令时会先在测试文件上逐步验证并且把脚本整理成文件而不是堆在一行里。说实话多行处理做久了你会发现复杂逻辑用 sed 强行写也不是不行但可读性会很差。三行以内的多行处理sed 很优雅超过三行我建议直接换 awk 或者 perl/python 一行行处理。工具是为解决问题服务的别为了秀技巧把自己绕进去。6. 踩坑记录与性能选择什么时候该用 sed什么时候该换工具最后分享一些我个人积累的实战经验和翻车记录。这些细节在 man 手册里都有但不踩几次坑你根本不会注意。6.1 四个高频坑第一个是分隔符冲突。用s做路径替换时路径里的每个斜杠都要转义一不留神就写错。我的解法前面说过换分隔符。这个技巧拯救了无数个下午。第二个是 CRLF 换行问题。从 Windows 拷过来的文件经常带\r\n直接用 sed 匹配^$会失效因为空行里有隐藏的\r。我的处理习惯是先sed -i s/\r$//把回车符去掉再继续其他操作。第三个是和\在替换部分里的转义。想把内容替换成时必须写\想替换成反斜杠时得写\\。这个细节在生成代码、写配置模板时特别容易踩。第四个是 locale 对正则的影响。在部分 locale 下[[:alnum:]]和[a-zA-Z0-9]的匹配行为不一致处理中文或带重音字符时可能出现意外。我的建议是脚本开头统一export LC_ALLC保证正则在任何环境下行为一致。6.2 性能与工具边界sed 的性能相当好因为它是纯 C 实现而且处理模型简单——读一行处理一行内存占用恒定。处理几百 MB 的日志文件sed 也就几秒钟的事。相比之下用 python 循环逐行处理同样规模的日志启动解释器的开销加上逐行大象 I/O往往慢一个量级。所以“能用 sed 解决的就不用脚本语言”这不仅是习惯也是实打实的效率考虑。但 sed 不是万能的。我给自己定了几条边界需要跨字段做条件统计交给 awk。需要建立数据结构、循环嵌套、从多个文件关联信息交给 awk 或 python。需要用正则做很复杂的回溯匹配交给 perl它的正则引擎更完整。需要保留匹配到的内容做二次处理比如从日志里提取字段再汇总交给 awk。工具的边界不是鄙视野而是效率。sed 擅长的领域是“批量、重复、规则明确的行级编辑”——一旦处理的逻辑超出这个范畴你花在调试 sed 命令上的时间早就超过了换工具的成本。6.3 我常用的工作流拿到一批待处理文本时我基本按这个流程走先用sed -n s/old/new/p预演看匹配范围是否正确不改文件确认预演没问题再用sed -i.bak执行真正的修改用diff file.bak file对比改动差异最后一遍目检确认没问题后清理备份文件。这套流程看起来多花了几步但每次都能在有问题的正则面前及时喊停。说句掏心窝的话sed 的威力不需要你记多少命令它最值钱的地方是让你在几分钟内完成手动需要一上午的机械劳动。我也见过同事用二十行的 python 脚本做了一个 sed 三行就能搞定的事工具本身没有高低但选对工具写出的命令维护成本真的天差地别。最后补一个小技巧sed 的输出默认带上行尾的换行符如果你处理的文件最后一行没有换行符部分 sed 版本会在输出时强行补一个这在拼接文件内容时会造成意外。遇到这种场景可以用tr -d \n或者直接在源文件层面处理别在 sed 里死磕。文本处理这事多踩几次坑你就会形成自己的工具直觉sed 能不能上、该不该上心里自然就有数了。