sed流编辑器详解:从模式空间到高频用法与实战踩坑
刚开始用Linux的时候我总觉得批量改文本是件麻烦事几十个文件要统一替换某个词要么打开编辑器一个个改要么赶紧临时写个脚本。后来一位老同事看我折腾半天过来敲了一行sed命令几秒钟搞定我当时就愣在那里。从那天起sed就成了我工具箱里最常用的一把刀。这篇就来聊聊这个老牌行处理工具从工作机理到高频用法再到我实际踩过的一些坑一次说清楚。sed全称是 stream editor流编辑器它不像是vim那样交互式地打开文件让你上下翻页而是像一条流水线逐行读入、按你给的规则处理、然后输出。正因为这种流式的工作方式它特别适合三件事批量替换、按条件筛选输出、对文件做机械化的小改动。不管你是刚接触命令行的新手还是想把手头日志处理效率提上去的进阶用户这篇都能给你一些直接能用的东西。1. 先搞清楚sed的定位一台流水线上的文本处理机1.1 为什么有vim和grep还不够很多人的疑惑是看文件我用cat、less改文件我用vim找内容我用grepsed到底有什么用答案是四个字批量、非交互。vim适合你亲手编辑一个文件但你要是想把整个项目 120 个文件里的旧域名全部换成新域名或者把一个 3GB 的日志文件里属于某个时间段的行抽出来vim就有点招架不住了。grep擅长找但它不擅长改。sed恰好补上这个空档它既是找的工具也是改的工具而且完全不依赖交互一条命令写完结果直接落在标准输出上配合重定向就能落到新文件配合-i参数就能原地改。我自己的体会是如果把 Linux 文本处理比作一个工具箱grep是放大镜awk是精密仪表sed呢就是一把多用途瑞士军刀——大多数时候你可能只需要它最常用的那几个功能但关键时刻它确实能顶上来。1.2 sed的核心工作机理模式空间要真正用好sed必须理解一个核心概念模式空间pattern space。sed的处理过程可以想象成一条传送带从输入流文件或管道读入一行文本。把这行文本放进一个临时缓冲区这个缓冲区就叫模式空间。把你写的脚本规则比如把foo替换成bar应用到模式空间里的内容上。处理完的结果输出到标准输出终端。清空模式空间读入下一行重复以上过程。关键在于sed默认每次只处理一行处理完就走不回头。这就带来两个推论一是它天然适合超大文件因为内存里永远只存着一两行内容二是它默认没有上下文概念要处理跨行逻辑就得靠后面会讲到的保持空间和多行模式。提示sed默认会把处理后的每一行都打印出来哪怕你只是做了一个不涉及那行的条件判断。很多人第一次写sed发现自己不需要的行也被输出了就是因为没搞清楚这个默认行为。1.3 最简单的命令形态sed的基本语法长这样sed [选项] 脚本 文件其中脚本通常是一个由地址命令组成的字符串。地址用来指定要处理哪些行命令指定做什么动作。缺地址时默认对所有行执行命令。比如最常见的替换sed s/old/new/ 文件名这个命令的含义是把每一行中第一次出现的old替换成new然后整行输出。注意是第一次出现如果一行里出现多次后面那些不会动想全部替换得加g标志我们下一节细说。sed还有几个常用选项-n关闭默认输出只有加了p命令的行才会打印。-e添加多条脚本或者当成一条脚本的起始。-i原地修改文件不输出到屏幕。-f从一个脚本文件里读取命令适合复杂规则。先记住这些往下走我们再逐个展开。实际用的时候八成场景都绕不开下面这一节的基本操作。2. 替换、删除、打印这三个动作覆盖了八成需求2.1 替换命令s完整语法与标志位细节替换命令的官方名字叫ssubstitute最完整的形态是sed 地址s/正则表达式/替换文本/标志地址先放一边我们先看s本身。斜杠是默认的定界符但有个很实际的问题如果你的旧文本里本身就包含斜杠比如要替换路径/home/user你就得费劲转义写成\/home\/user难看还容易错。解决办法是换一个定界符sed允许你用任意字符当定界符比如sed s#/home/user#/data/user#g 文件名 sed s|旧内容|新内容|g 文件名我个人最常用的是#和|处理路径类替换时清爽得多。g标志就是全局替换一行里所有匹配都被替换不加只替换第一个。除此之外还有几个少有人提但很实用的标志数字只替换第几次出现的匹配。比如s/foo/bar/2表示只替换每行第二次出现的foo。p替换成功后打印该行通常配合-n使用用来过滤并提取。w 文件名把匹配成功的行写入指定文件。讲个我自己的使用场景。排查线上问题时日志里大量相同的报错信息我只想看到每行里第二次出现的某个时间戳之后的内容这时候s/.../.../2这种按次数替换就非常精准。很多人不知道这个技巧遇到类似需求会绕一大圈。2.2 反向引用一行命令处理抓取重排sed的替换文本里有两个特殊的引用方式代表整个匹配到的内容。\1、\2等代表正则里第 1、2 组括号捕获的内容。举个例子假设你有一个文件每行格式是姓名,城市想变成城市-姓名sed s/^\([^,]*\),\(.*\)$/\2-\1/ 文件名这里\([^,]*\)捕获逗号前的部分姓名\(.*\)捕获逗号后的部分城市替换时用\2-\1调换顺序。注意传统sed里括号是要写成\(和\)的加个转义。如果你用的是加-E选项的写法就能直接用(和)清爽很多sed -E s/^([^,]*),(.*)$/\2-\1/ 文件名提示新手写sed正则时最常见的困惑就是这个转义问题。我自己现在几乎总是带上-E让正则写法更接近直觉省去一堆反斜杠的麻烦。老式的\(写法在老旧脚本里常见读别人脚本时要认得自己写时优先用-E。的典型用法是批量加前缀后缀。比如给每一行包一层 HTML 标签sed s/^.*$/li\/li/ 文件名匹配整行引用全文前后套上标签。这种写法做批量包装、批量加注释都特别顺手。2.3 删除与打印结合地址精准下手删除命令是d打印命令是p。单独用的时候价值不大因为sed默认就会输出每一行d只是把选中的行删掉p则是把选中的行再打印一遍。真正有价值的是配合地址。最常用的组合是# 删除第5行 sed 5d 文件名 # 删除第3到第8行 sed 3,8d 文件名 # 只打印第1到第10行配合 -n sed -n 1,10p 文件名 # 删除所有空行 sed /^$/d 文件名 # 删除所有包含 DEBUG 的行 sed /DEBUG/d 文件名你看一旦地址出现命令的杀伤力就出来了。sed -n 1,10p这个写法我几乎天天用相当于低成本实现head的高级版本——你还可以写sed -n 100,200p直接抽文件中间某一段这在看大日志文件时比head和tail灵活得多。多命令组合也简单用分号分隔即可sed -n 1,20p; /ERROR/p 文件名这个意思是打印前 20 行另外把包含ERROR的行也打印出来。逻辑上相当于两个命令依次执行。表格式小结一下这一节的高频命令命令含义常见示例s/旧/新/g全局替换sed s/foo/bar/g 文件d删除选中行sed 3,8d 文件p-n只打印选中行sed -n 1,10p 文件打印行号sed -n 5,10 文件a/i在行后 / 行前插入文本sed 5a 新增内容 文件a和i这两个插入命令也值得知道。比如要在第 5 行后面插入一行------直接写sed 5a ------ 文件。批量给配置文件的指定位置加注释或插入配置片段这个命令非常高效。3. 地址与范围学会精确命中要处理的行3.1 三种寻址方式行号、步进、正则sed的地址系统是它的灵魂。归纳起来就三种按行号直接写数字如3表示第3行3,8表示第3到8行。还有一种特殊写法$表示最后一行比如sed -n $p 文件等价于打印文件最后一行在某些场景下比tail -n 1还好用因为你知道它完全基于行号定位不受管道缓冲影响。按步进写法是起始~步长。典型应用是处理奇数行或偶数行# 打印奇数行 sed -n 1~2p 文件名 # 打印偶数行 sed -n 2~2p 文件名 # 删除第3行开始每隔4行的那些行 sed 3~4d 文件名步进寻址在抽样检查时特别有用。比如一个日志文件有 10 万行你想看看每隔 1000 行取一行sed -n 1~1000p一条命令抽 100 个样本点规律立刻看出来。按正则用/正则/去匹配内容。凡是匹配到的行都被选中sed -n /^ERROR/p 文件名 # 打印以 ERROR 开头的行 sed /timeout/d 文件名 # 删除包含 timeout 的行 sed -n /[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}/p 文件名 # 匹配日期格式这里要特别说一句sed默认使用基础正则BRE{}这类量词需要转义成\{ \}。加了-E用扩展正则ERE就没这个麻烦{4}直接写。3.2 范围寻址从哪开始到哪结束最强的是范围寻址用两个地址表达式sed会在遇到第一个匹配时开始选中模式遇到第二个匹配时结束。写法是地址1,地址2地址可以是行号也可以是正则。几个我在真实场景里反复用到的例子# 打印从 BEGIN 行到 END 行之间的所有内容 sed -n /BEGIN/,/END/p 日志文件 # 删除从 transaction start 到 transaction end 之间的内容 sed /transaction start/,/transaction end/d 日志文件 # 从第10行开始到第一个空行结束 sed -n 10,/^$/p 文件这个能力对提取日志里的某一段特别关键。我常碰到的情况是日志里成片出现相同前缀的错误堆栈每条错误从Traceback开始到空行结束。用sed -n /Traceback/,/^$/p 日志文件就能把所有错误堆栈一条不落地全抽出来比grep只抓那一行信息量大得多。范围寻址还有一个变体地址1,N表示从地址1开始再往后数 N 行。比如sed -n /ERROR/,3p 日志打印每个 ERROR 行以及它后面 3 行——排查问题的时候很有用因为报错的关键上下文往往就在后面几行。注意N这种写法要 GNUsed支持BSD 版macOS 自带会报错这个差异我们第6节细说。3.3 取反用感叹号翻转选择地址后面加个!就能取反作用范围瞬间扩大一倍# 打印所有不包含 ERROR 的行相当于 grep -v sed -n /ERROR/!p 日志文件 # 删除除了第1行到第20行之外的所有行相当于截取前20行到原文件 sed 1,20!d 文件 # 在最后一行前插入一行提示信息 sed $!a --- 文件sed 1,20!d这个写法我偶尔用来快速截断大文件sed -i 1,20!d 文件只保留前 20 行。注意这和head -n 20的区别在于sed -i是直接覆盖原文件不需要先重定向到临时文件再mv一步到位。地址和命令组合起来你其实已经能解决绝大多数文本处理问题了。但如果你只停在单行处理那sed的潜力只发挥了一半。接下来这节稍微进阶一点但也是老手和新手的分水岭。4. 模式空间与保持空间跨行处理的隐藏能力4.1 单行处理模式的天然局限sed默认逐行处理这既是它高效的原因也是它的天花板。比如你想把连续的几行合并成一行或者想统计一个多行段落的总行数默认模式就办不到了——因为处理第1行时你根本不知道第2行甚至第10行长什么样。好在sed提供了几个专门对付多行的命令核心就两个缓冲区模式空间pattern space和保持空间hold space。模式空间是主工作台我们已经知道了保持空间是一个备用的暂存区sed在处理过程中不会自动碰它只有你显式用H、G、h、g、x这些命令读写。打一个比方模式空间是桌面上你正在编辑的文档保持空间是旁边的抽屉。你随时可以把文档塞进抽屉h或者把抽屉里的东西拿出来叠加到文档上G也可以两者互换x。4.2 六个关键字母N、P、D、H、G、x先看一张速查表命令作用N把下一行追加到模式空间换行符分隔相当于在桌面上叠下一张纸P打印模式空间中第一个换行符之前的部分D删除模式空间中第一个换行符之前的部分H把模式空间内容追加到保持空间的末尾G把保持空间内容追加到模式空间末尾x交换模式空间和保持空间的内容这些命令里N、P、D是多行模式三件套H、G、x是跨行暂存三件套。分别看两个例子。例子1合并两行假设一个文件内容是名字张三 职业工程师 名字李四 职业设计师想把每两行合并成一行用Nsed N; s/\n/ / 文件原理第一轮读入名字张三N把下一行职业工程师也追加进来此时模式空间里有两行中间是一个换行符。s/\n/ /把换行符替换成空格两行变一行输出。第二轮继续读入第三、四行同样处理。这就实现了两行合并。例子2找出以及合并到特定行再比如你想给匹配到特定模式的行后面紧跟的下一行也一起处理sed -n /请关注/,${N;p} 文件含义是从匹配请关注的行开始一直到最后一行每次读一行就追加下一行然后打印。输出时会看到两行内容合并成一个块输出中间还是保留换行p打印的是整个模式空间。例子3交换限定范围内的两行场景想把第3行和第4行互换位置。前面学的单行命令做不到得借助保持空间sed -n 3{h;d}; 4{G} 文件拆开看当读到第3行时h把第3行存进保持空间d删除并结束对该行的处理注意这里d会直接进入下一轮而不再继续匹配后面的命令读到第4行时G把保持空间里的第3行内容追加到模式空间末尾于是输出第4行第3行完成交换。这个技巧看起来绕但理解了保持空间抽屉的作用之后就顺了。坦白说这些高级命令在日常工作中用得不算频繁但一旦遇到合并相邻行批量调整段落顺序把分散的片段拼装这类需求你会庆幸自己知道它们存在——否则就得写脚本循环慢处理或者干脆手动改上半天。5. 真实场景复盘日志提取、配置批改与代码替换理论说再多不如三个实际场景来得直观。这里我把工作里最常用的三种sed用法完整拆解一遍每条命令都可以直接抄去用。5.1 场景一从大日志里提取某个时段的记录有一次线上报警问题集中在 14:30 到 14:35 之间。日志文件 2 个多 GB直接grep关键词会混入其他时间段的东西打开编辑器更不现实。日志的行首时间格式是2025-01-15 14:30:01这种。范围提取的命令sed -n /2025-01-15 14:30:0/,/2025-01-15 14:35:5/p app.log window.log原理就是范围寻址从匹配第一个时间前缀的行开始到匹配第二个时间前缀的行结束中间全部打印。两个边界我故意把秒位写宽泛一点避免因为某一秒恰好没有日志而漏掉边界内容。对着提取出来的window.log再配合grep -c ERROR统计错误数、awk按接口聚合排查效率直接上升一个档次。提示如果两个边界之间跨度很大这命令会一次性把中间所有行都打进内存缓冲区吗不会。sed范围寻址依然是流式的它只是维护一个范围内的开关状态输出是边读边写的吃内存很稳。这也是我喜欢用sed处理大日志的根本原因。5.2 场景二批量注释与恢复配置文件某个 Web 服务的配置文件里有一段需要临时禁用的监听配置假设那段配置位于第 40 到第 46 行。手动打开文件逐行加#太麻烦恢复时还得逐个删容易漏。批量注释sed -i 40,46s/^/#/ server.conf这句的意思是对第 40 到 46 行的行首^做替换在行首插入#。想恢复反向操作sed -i 40,46s/^#// server.conf如果不想依赖行号毕竟配置文件可能有人动过行号会变改用模式匹配更稳妥sed -i /^listen 8080/,/^}/s/^/#/ server.conf这段表示从以listen 8080开头的行开始到第一个以}开头的行结束范围内所有行首都加#。这样就算前面新增删除了几行只要这段配置的结构不变命令照样精确命中。5.3 场景三项目里批量替换接口地址某个项目要切环境代码里散落着几十处写死的接口地址比如http://旧地址.com/api要统一换成https://新地址.com/api。如果手改几十个文件逐个搜索替换费时且容易漏如果写脚本为了这么点事又有点小题大做。一条命令先找到所有包含目标文件再用sed批量改grep -rl 旧地址.com --include*.js . | xargs sed -i s#http://旧地址.com#https://新地址.com#g这里有两层grep -rl列出所有包含旧地址的.js文件路径管道交给xargs逐个传给sed -i原地修改。特别注意我用了#当定界符因为原地址里就有//如果用默认的/定界符到处都是转义斜杠命令可读性极差。g标志也别忘记一个文件里同一行可能出现多次旧地址不加g只替换每行第一个会有漏网之鱼。在这个场景里我还习惯先跑一遍不带-i的版本把输出重定向到一个临时文件抽查确认没问题后才加上-i动真格。这是处理批量改代码这类不可逆操作时该有的谨慎。6. 踩坑实录sed在不同环境下的行为差异没有踩过坑的工具是不完整的。sed在 GNU 版本主流 Linux 发行版自带和 BSD 版本macOS 自带之间存在几个让我吃过亏的差异列出来给大家避雷。6.1 原地修改 -i 的坑在 Linux 上sed -i s/foo/bar/g 文件直接改原文件没毛病。但在 macOS 上同样的命令会报错必须写成sed -i s/foo/bar/g 文件原因在于 BSDsed的-i后面必须跟着一个备份后缀名哪怕你不想备份也得给个空字符串占位。如果你恰好需要在 macOS 和 Linux 之间移植脚本稳妥做法是sed -i.bak s/foo/bar/g 文件这样两边都能跑并且还会留下一个.bak备份文件。我个人的习惯是凡是批量原地修改一律先留备份确认无误再统一删除.bak。多一个备份文件比改错后无法恢复要划算得多。6.2 范围 N 和转义差异前面提过的地址1,N写法也是 GNU 扩展BSDsed不认。类似地\d这类 Perl 风格转义在某些sed版本里完全无效老老实实写[0-9]才是通行做法。还有换行符。如果你处理的文件来自 Windows行尾是\r\n/^$/匹配空行就可能失效因为每行结尾都带着一个\r空行实际是\r加换行正则在 Linux 环境里默认不匹配\r。解法是先统一换行符或者正则里显式处理sed s/\r$// 文件 # 去掉行尾的 \r然后再做你的逻辑我在 Windows 和 Linux 混用的环境里处理过不少从 Windows 导出的文件这个\r的问题遇到一次就知道有多坑了。6.3 用前先验证别让 -i 变成后悔药最后一个建议也是最实用的任何复杂的sed命令都先去掉-i跑一遍看看输出。我的习惯流程是先用head -n 20 文件或者干脆sed -n 1,20p 文件看看目标文件的结构。写下sed命令先不加-i把结果重定向到/tmp/out.txt。用diff 原文件 /tmp/out.txt或者直接cat /tmp/out.txt | head -n 30抽查。确认匹配范围和替换结果都正确才加上-i。这一步看起来多花了一分钟但它能拦住绝大多数灾难性操作。尤其是当你用行号做范围寻址时文件内容变动过一次后行号就可能对不上先验证一下能救你不少备份文件的命。我自己用sed这么多年最大的体会就一句话它不炫技但只要你掌握了地址命令模式空间这套思维90%的文本批量处理需求都能在一条命令里解决。别急着写脚本先想想sed能不能一把梭——大多数时候能的。