1. sed为什么到现在还是文本处理的必修课先说结论在Linux/Unix环境下处理文本sed是绕不开的那道坎。很多刚接触shell编程的人上来就学grep遇到提取字段再学awk唯独把sed放在好像只是替换一下文本的位置等到真的需要在脚本里批量修改配置文件、从几千行日志里抽取关键内容、或者在不打开编辑器的情况下改写文件时才发现自己缺了最顺手的那把刀。sed全称Stream Editor流编辑器。它的核心思想是一条一条地读取输入按你给的指令逐行修改然后输出。这个流式特性决定了它天生适合管道协作——cmd | sed ... | cmd像流水线上的工位一样前一个程序处理完的数据顺手交给sed再加工一道。实际工作中我统计过日常shell命令组合里sed的出现频率几乎和grep持平尤其在做日志分析、批量改配置、清洗数据这几类任务里sed是不可替代的。这篇文章我会从sed的执行模型讲起再把地址匹配、常用命令、保持空间、脚本协作、典型坑点全部过一遍。无论你是刚入门shell的新手还是已经写了几年脚本但没系统梳理过sed的老手照着实操都能用起来。文章里的示例我都尽量用真实场景不是那种为了演示而演示的玩具代码。2. 读取、执行、显示理解sed的流式执行模型2.1 三个动作构成一次循环sed处理文本的基本单位是行。它不关心你的文件有多大反正每次只处理一行处理完读下一行。这个逐行循环背后其实是三个连续动作在执行从输入流读取一行放入模式空间pattern space按顺序执行脚本中给定的所有命令对模式空间里的内容做修改把模式空间的最终内容输出到标准输出然后清空模式空间进入下一轮。这三步构成了sed循环的最小单元。简单文件或者管道流是这样就算处理几万行的日志也这样。理解了这一步你就能解释很多新手经常遇到的为什么我的sed命令输出了两遍的问题。2.2 默认的自动打印p命令为什么会产生双份输出sed默认情况下每次循环的最后一步会把模式空间的内容打印一次。这个动作叫自动打印是内置的、默认开启的。如果你在脚本里又显式写了p命令内容就会被打两遍。举个例子有一个test.txt里面只有一行hellosed p test.txt输出结果是hello hello第一行来自显式p命令第二行来自循环结束时的自动打印。很多初学者在这里懵掉以为是p命令有问题。实际上并不是如果你不希望自动打印这行用-n参数就能关掉sed -n p test.txt-n的意思是不要自动打印只输出你明确要求输出的内容。这个参数在日志提取、精确控制输出行的场景下非常重要几乎我写的每条sed命令里超过一半都会带-n。理解了这个执行模型后面所有命令的奇怪表现基本都能自己推理出来。比如d命令删除一行时会直接跳过自动打印a命令追加文本时追加的内容是在模式空间输出之后才输出的。这些细节不多想一步就很容易被表面现象带偏。3. 不看懂地址规则sed命令就是一条条瞎写3.1 三种基本地址写法sed脚本里绝大多数命令前都可以加地址用来限定这条命令作用在哪些行上。地址写错命令要么不生效要么作用在错误的行上调试起来非常烦人。三种基本写法必须烂熟于心行号直接写数字。比如3d就是删除第三行1p就是打印第一行。正则用斜杠包裹/pattern/。比如/error/p表示只对匹配到error的行执行p。步进地址first~step这是GNU sed的扩展写法。1~2表示从第1行开始每隔2行取一行效果就是奇数行。2~2就是偶数行。在sed -n 2~2p file里它等价于提取偶数行。还有一种特殊的$表示最后一行。注意$在地址里表示最后一行sed -n $p file就是打印文件的最后一行。这个$跟在正则里的行尾锚点$不一样要看上下文理解。3.2 范围和逗号最容易被用错的点地址可以是一个也可以是一段范围。范围用逗号分隔sed -n 3,6p file # 打印第3行到第6行 sed -n /start/,/end/p file # 从匹配到start的行开始一直打印到匹配到end的行第二种写法特别常见于日志提取。比如从日志中提取一段错误信息这条错误从ERROR START开始到ERROR END结束那么用sed -n /ERROR START/,/ERROR END/p就能把整段内容捞出来。范围地址两个端点都可以用正则也可以是行号加正则混合比如3,/ERROR/p就是从第3行开始匹配到ERROR为止。实际工作中有一个容易被坑到的点如果第二行地址范围结束条件始终匹配不上sed会一直处理到文件末尾。这不是bug正是流处理器的特性——它不知道文件后面还有没有匹配行只有读到EOF才知道结束。所以你在写范围提取时一定要确认结束模式肯定会出现否则会多捞出一大段内容。再补充一个addr1,N的写法表示从addr1开始往后连带上N行。例如sed -n /ERROR/,5p在匹配到ERROR那一行以及后面5行。这在调试堆栈快照时非常好用。4. 最常上手的几个sed操作直接照着抄4.1 替换s命令的分隔符与标志位sed用得最多的命令无疑是s。基本形态sed s/旧/新/标志标志位常用三个标志含义g全局替换一行内所有匹配都替换而不是只替换第一个p打印替换后那一行i忽略大小写数字只替换第N次出现比如把abc.txt里所有的小写old改成newsed s/old/new/g abc.txt这里有个容易踩的坑替换字符串里的有特殊含义它表示匹配到的整个内容。比如你想给每一行加引号echo hello world | sed s/.*//输出是hello world。自动指代了匹配到的hello world。但如果你确实需要字面的字符需要写成\转义。另一个坑在分隔符。当匹配内容里大量出现斜杠时继续用/做分隔符会写成一堆转义非常痛苦。比如替换文件里的路径sed s/\/usr\/local\/bin/\/opt\/bin/g paths.txt这种写法又丑又容易错。sed允许你换分隔符常用的是|、#、sed s|/usr/local/bin|/opt/bin|g paths.txt sed s#/usr/local/bin#/opt/bin#g paths.txt只要三条命令里两两不冲突随便用。我在脚本里统一用|因为它比/和#在文件名里出现得少一些。4.2 删除、插入、追加、改行不只是行删除d命令大家都会用sed /^#/d file删除注释行。除了删行还有三个操作同样常用a在指定行后面追加文本i在指定行前面插入文本c把匹配到的整行替换成新文本。打个比方在配置文件的特定位置插入一项配置sed /^\[server\]/a port8080 app.conf这条命令会在匹配到[server]那一行后面追加一行port8080。注意a命令后面追加的内容会另起一行输出。如果有多个追加行用\n分隔各文本或者用反斜杠换行写出。c命令则直接把整行替换掉sed /listen 80;/c listen 8080; nginx.conf这个适合批量修改配置里的某个参数。注意c替换的是整行如果你只想替换行内某一段应该用s而不是c。4.3 提取字段的轻量组合很多人提取文本字段第一反应是awk但awk在有些环境里如果没装或者你只是取第一列、去掉前几行用sed组合一下更快sed -n s/^\([^,]*\),.*/\1/p data.csv # 提取每行第一个逗号前的部分 sed -n 2,$s/^[0-9]*//p file # 从第2行开始去掉行首数字这里用到了捕获组\(...\)和\1反向引用。sed基本正则表达式里分组要用\(和\)包裹引用用\1、\2这个和扩展正则grep -E、awk的写法不一样刚用的时候特别容易手滑。比如想提取keyvalue中的valueecho namealice | sed s/^name\(.*\)/\1/输出alice。这个\1反向引用是sed里仅次于s命令的第二个高频技能点配合-n和p标志就是天然的小型awk。4.4 批量改文件sed -i的安全操作方式s命令默认只输出到标准输出不改原文件。真正的就地修改靠-ised -i s/old/new/g file.txt这条命令把文件里所有old替换为new且直接写回原文件。这个参数好用但也是风险最高的操作。一是没有备份的情况下改错了没法恢复二是不同平台对-i的参数要求不同。GNU sedLinux发行版默认直接用sed -i即可。BSD sedmacOS自带要求必须带一个后缀参数否则报错# macOS sed -i s/old/new/g file.txt # 或者留备份 sed -i .bak s/old/new/g file.txt脚本如果要在多平台跑建议先检测一下环境再决定-i的写法。我自己的做法是批量替换前先干跑一次不带-i执行一遍看输出是否正常确认无误后再加上-i执行同时用-i.bak留一个备份跑完检查没问题再删备份文件。这一套流程下来基本没有因为批量改文件翻过车。5. 模式空间与保持空间sed的两张草稿纸5.1 两把空间是什么sed处理行的内存区域其实有两个默认处理用的是模式空间pattern space另一边还有一个一直没提到的保持空间hold space。保持空间可以理解为sed自带的一块草稿纸——你可以在处理过程中把内容临时存进去稍后再取出来用。模式空间和保持空间的相互作用让sed能处理很多看似需要状态记忆的任务。两把空间之间有五个基本操作命令需要掌握命令作用h把模式空间内容复制到保持空间覆盖H把模式空间内容追加到保持空间换行拼接g把保持空间内容复制回模式空间覆盖G把保持空间内容追加到模式空间尾部换行拼接x交换模式空间与保持空间的内容5.2 把多行拼接成一行最典型的场景把两行合并成一行。有一个README.md第1行是标题第2行是正文我想把两行用空格拼起来输出sed N; s/\n/ / README.md这里先用了N命令把下一行合并进模式空间中间插入换行符然后s把换行符替换成空格。注意N是sed内置命令与关系运算符的N没有任何关系是从Next来的。它的操作会在模式空间里保留一个\n字符这个字符本身可以被正则匹配到。如果想把一个文件里的所有连续行拼接成一行用下面这条sed :a; N; $!ba; s/\n/ /g file稍作解释:a定义了一个标签aN把下一行并进来$!ba是在非最后一行的情况下跳回标签a继续执行。最后整个模式空间里就是整个文件的内容再用g标志把所有换行替换成空格。这叫读入多行再统一处理的模式遇到跨行数据清洗时很管用。5.3 交换行序、按块翻转、简易去重保持空间最常见的实战是交换相邻两行。比如想把奇数行和下一行换位置sed -n h; n; G; p file拆解一下h把当前行存进保持空间n读取下一行G把保持空间里的上一行追加到当前行后面p打印。两条原本相邻的行就完成了交换。去重另一个例子。有一个已经排序的文件想删掉连续重复的行sed能做到sed $!N; /^\(.*\)\n\1$/!P; D这条相对绕它检查当前行和下一行是否完全相同相同就跳过打印。现实中这个任务用uniq更简单但理解N、D、P这三者的配合对掌握sed多行处理是有帮助的。还有个更实用的场景把配置文件里每个段落连续的非空行之间加一个分隔行sed /^$/d; G fileG把保持空间里的内容拼到每一行后面而保持空间是空的所以每行后面多了一个空行。/^$/d先删掉已有空行效果就是给每个非空行之间插入一个空行。实际的排版类需求里这条很常见。6. 在shell脚本里让sed真正跑起来6.1 单引号、双引号与变量的注入方式sed命令写到脚本里最常见的坑是引号问题。前面所有示例我都用的单引号包裹sed脚本因为单引号会保留$、\等字符的字面含义方便正则和反向引用。但实际工作中我们往往需要把shell变量传进sed脚本比如oldfoo newbar sed s/$old/$new/ file.txt这种情况下必须用双引号shell变量才能展开。但双引号会连带影响$在正则里的含义$表示行尾锚点还好但如果你用了\$表示字面的美元符号在双引号里也要写成\\$转义层级瞬间混乱。我的经验是**当sed脚本里有正则元字符和反向引用时优先用单引号需要变量展开时改用双引号并且把脚本里的所有反斜杠按双引号规则重写一遍。**另外如果变量内容里本身可能包含特殊字符比如路径里的斜杠、空格、正则符号先对变量做一个转义预处理否则替换会出各种奇奇怪怪的结果old_escaped$(printf %s $old | sed s/[][\/.*^$]/\\/g)这行的意思是用sed自己把old里的正则特殊字符全部转义然后再作为替换源使用。属于用sed保护sed的经典做法。6.2 循环与条件判断别用sed跑循环用循环调sedsed本身不擅长复杂的循环控制但它可以在shell的循环里被反复调用。比如批量给多个文件统一替换for f in config/*.conf; do sed -i.bak s|http://192.168.1.10:8080|https://api.example.com|g $f done这种场景很常见。另外一个配合方式是用while read按行读取sed处理后的结果做后续逻辑sed -n /^user:/p /etc/passwd | while IFS: read -r name rest; do echo found $name done注意管道的while子shell问题管道会把右边的命令放进子shell运行你在循环里修改的变量在循环结束后不会保留到主脚本。如果需要在循环中收集结果用进程替换或者临时文件更靠谱。这个问题只要你写过几次含管道的脚本肯定遇到过。6.3 用sed生成配置和清理数据生产环境里一个很典型的应用是从模板生成配置文件。有一个模板文件template.conf内容里只有__HOST__和__PORT__两个占位符host192.168.1.10 port8080 sed -e s|__HOST__|$host|g \ -e s|__PORT__|$port|g \ template.conf production.conf这样每次部署时都能从一份干净模板生成环境相关的配置不用维护多个版本。-e参数表示追加一条sed指令一条命令里可以串联多个-e也可以用分号塞进一个字符串里。数据清洗也是高频场景。比如从CSV文件里删除包含NULL值的行、去掉行尾的\rWindows换行残留sed -i s/\r$// export.csv # 去掉CRLF里的\r sed -i /NULL/d export.csv # 删除含NULL的行在做跨平台数据交换时这两条几乎每次都能用上。\r$这个模式专门匹配行尾的\r字符删除后文件就变成了Linux标准的LF换行。7. 我踩过的sed的坑附排查思路7.1 sed -i 手滑改错文件的教训有一次写部署脚本想批量替换某目录下所有配置文件里的一个旧端口号命令写成了sed -i s/8080/9090/g /etc/app/*.conf执行完才发现这个目录下有一个backup.conf是旧的备份文件里面记录的生产环境地址也被一并替换了。虽然数据还在但恢复过程非常痛苦。从此我给自己立下规矩**凡是批量sed -i先带-n跑一遍看输出确认目标文件和替换范围都正确再带上.bak后缀执行并确认。**另外大力推荐find ... -exec sed ...组合因为可以精确控制文件列表find /etc/app -name *.conf -exec sed -i.bak s/8080/9090/g {} 这样只对.conf文件生效其他类型文件不碰安全等级提升一个档次。7.2 locale导致的多字节匹配问题处理中文日志时遇到过一个很隐蔽的问题。环境变量是UTF-8但用s/^\(.\{20\}\)[^,]*,/\1/提取中文内容时发现匹配出来的部分经常多出或少了半个字符。排查后确认是因为默认的locale下sed对多字节字符的支持不完全正则里的.只能匹配单字节。解决方式是显式把locale设成UTF-8export LC_ALLen_US.UTF-8 sed -n ... 中文日志.txt在脚本开头固定设置LC_ALLC.UTF-8或en_US.UTF-8再做中文处理基本能避免这个问题。如果还是出现半字截断那就要考虑用perl或者awk的gsub来做中文处理了不要死磕sed。7.3 管道缓冲导致的没输出假象管道处理大文件时sed处理完的数据常常会在管道里积压。你用cmd | sed ... | less时能正常显示但把输出重定向到文件时发现文件是空的或者很小第一反应以为是sed没执行。实际上可能是管道缓冲区没刷新或者前面的命令还没执行完。尤其当脚本里同时使用了grep、sed、awk多个管道时后面阶段会一直等着上游数据。排查方式很简单先跑cmd | sed ... | wc -l看行数是否符合预期如果行数对但文件空再检查重定向语法。通常把| sed换成 file直接测就能定位问题在哪一段管道。另外写脚本时尽量让sed处理完的数据立即落地避免在一个超长管道链里做太多中间处理这会大大增加排查难度。7.4 特殊字符转义分隔符选错的连锁反应替换IP加端口时如果内容含斜杠和点号直接写sed s/192.168.1.1:8080/new.host.com:9090/g出来的结果经常不对。原因有二一是.在正则里表示任意字符没转义IP的主机号部分会匹配出意想不到的字符二是分隔符如果也是/你要的效果被路径风格搞混。我建议替换时统一用|做分隔符并且把正则里的.转义成\.sed s|192\.168\.1\.1:8080|new.host.com:9090|g这样至少能避免一大半的替换失败。另一个高频问题是替换字符串里含这个字符在替换内容中会被当作匹配到的内容原样插入所以如果想写入真正的符号必须先转义\。写成脚本时如果变量内容来自用户输入转义处理就更不能省。7.5 GNU sed与BSD sed的兼容性前面提到-i的差异这其实只是两套sed差异的冰山一角。GNU sed支持\d、\、\?等扩展正则写法BSD sed对很多转义和扩展命令支持不全。跨平台脚本中最稳妥的策略是只用POSIX标准里保证有的语法。比如用\{m,n\}表示次数匹配用[0-9]而不是\d不要依赖\、\词边界这种在某些发行版上行为不一致的扩展。如果确实需要差异化处理可以在脚本开头检测系统类型再分叉处理if sed --version 2/dev/null | grep -q GNU; then sed -i s/foo/bar/g file else sed -i s/foo/bar/g file fi这个写法在同时兼顾Linux和macOS的团队机子上非常实用我维护的几台机器都是这么处理的。8. 收尾前再分享两个小技巧写了这么多最后补两个能直接提升效率的小经验。第一个是多用sed -n ...p替代直接sed ...。很多人的习惯是不加-n结果输出里除了想要的行还有自动打印的原始行导致命令结果比自己预期多很多。我现在的习惯是提取用-n替换用普通模式删除用普通模式。这个区分基本不会错。第二个是善用s命令的p标志做替换结果预览sed -n s/old/new/gp file这条命令只打印发生了替换的行大大减少了审查批量替换时的输出噪声。配合-i.bak结构就是一套完整的安全替换流程。这个先预览、后落地的套路比上来直接改原文件稳太多了。说到底sed不复杂它的难点不在命令本身而在把一个文本处理需求拆解成sed能理解的一步步预备动作上。多写几次、多踩几个坑慢慢就会发现sed在shell编程中的地位——不是炫技工具而是解决问题的手边常备工具。希望这篇梳理能帮你少走一些弯路把时间省下来去做更值得做的事情。
阅读完成 · 觉得有帮助?