
如果你写过一阵Shell脚本大概率会遇到这种场景手头有几十个配置文件要把某个参数从A改成B或者要从几万行的日志里把报错行抽出来处理。用vim一个个打开改效率实在太低grep只能负责“找出来”改不了内容。真正顺手的其实是sed。sed全称stream editor中文叫流编辑器是Shell编程里最基础也最常用的文本处理工具之一和grep、awk并称“文本处理三剑客”。很多刚入门的朋友总觉得sed的语法别扭一看那一长串sed -i s/x/y/g就头皮发麻其实它的核心逻辑非常清晰。这篇文章就针对sed的基础命令做一次系统梳理。我会从它的工作原理讲起再覆盖定址方式、增删改查命令、替换命令的细节、常见翻车现场最后落到实际工作流里怎么用。不管是纯新手还是用过一阵但总踩坑的读者相信都能从中捞到点干货。1. 为什么文本处理偏偏绕不开sed从“读一行、改一行”的流式本质说起1.1 先搞懂“流编辑器”里的“流”是什么意思传统编辑器比如vim打开的是一个文件的全量副本你看到的、操作的都是整个文件的缓冲区改完再整体写回。这种方式在处理小文件时很舒服但到了几GB的日志、几千行的配置文件面前就显得笨重了——打开慢、内存占用高、脚本里也不方便调用。sed的工作方式完全不是这样。它天然面向“流”也就是一种按顺序不断到来的文本数据流。你可以把sed理解成一条流水线文本从入口进来逐行被读取经过处理再从出口出去。每次内存里只保留当前正在处理的这一行处理完立刻输出然后接着读下一行。这种“读一行、改一行、吐一行”的模式就是它叫流编辑器的根本原因。这个设计带来的好处非常实在一是不管文件多大sed的固定内存占用几乎可以忽略不计二是它和管道pipe的契合度极高前一个命令的输出可以直接作为sed的输入再往后一个命令继续传递中间不需要落地临时文件。也正因为这样sed在日志链路处理、发布脚本、自动化运维里几乎是标配。1.2 模式空间sed内部那间“单行加工车间”要真正理解sed绕不开一个概念模式空间pattern space。我习惯把它想成车间里的一条传送带sed每读到一行文本就把它放到这条传送带上然后执行你写的命令命令执行完传送带上的内容被输出或者被丢弃紧接着读取下一行重复这个过程。举个例子sed s/old/new/ file.txt的执行过程就是读第一行把这一行放进模式空间执行替换命令把处理完的结果输出然后读第二行继续同样的动作直到文件结束。整个过程里模式空间始终只装一行内容这也是sed高效的密码所在。理解了这个机制后面很多命令的行为就好解释了。比如p命令本质上就是“把模式空间里的内容打印一份出来”。如果你不配合-n参数sed默认会把原始输入和p的打印结果一起输出于是你会看到一行变两行的怪现象——不是Bug是你对模式空间的行为习惯还不够熟。1.3 同门师兄弟对比sed、grep、awk各自的分工很多初学者容易把sed、grep、awk混在一起觉得它们都能“处理文本”其实三者的分工有明确边界。我习惯这样类比grep负责“过滤器”它只负责从一堆文本里把符合条件的行挑出来不改内容。sed负责“流水线作业员”它对每一行做增删改替换适合整行级别、模式级别的处理。awk负责“数据分析师”它会把每行拆成字段适合做表格化处理和统计运算。举个例子你要从日志里找出所有包含“ERROR”的行用grep你把找到的这些行里的时间戳格式改一下用sed你要统计每个IP各自出现了多少次用awk。大部分场景里三者还会串在管道里配合使用比如grep ERROR app.log | sed s/时间戳// | awk {print $1}。先过滤、再清洗、再统计各干各的活彼此不抢戏。明白这一点之后你就不会在一个需要统计字段求和的任务里跟sed死磕了。sed真正擅长的是“按照某种规则逐行地修改文本形状”。2. 动手前必懂的定址与动作模型sed命令的最小完整拼装2.1 基本语法里藏着的两层逻辑sed的命令看起来五花八门但剥开来看结构非常统一sed [选项] 地址动作 文件这里“地址”负责回答“对哪些行下手”“动作”负责回答“对这些行做什么”。两者组合起来才是一条完整的sed指令。许多人学sed感觉混乱就是因为把地址和动作搅在一起看没有拆开理解。比如sed 2,5d file.txt这条命令2,5是地址意思是第2行到第5行d是动作代表删除。合起来就是把第2到第5行删掉。再比如sed /error/d file.txt/error/是地址表示所有包含“error”这个关键词的行d是动作表示删除。合起来就是删除所有包含error的行。如果没有写地址默认地址是“所有行”也就是每一行都会执行后面的动作。这也是sed s/x/y/g能作用于整个文件的原因——它没有限定地址所以每一行都执行了替换。2.2 五种定址方式覆盖日常绝大多数需求地址是sed命令的灵魂。把地址搞明白了sed你就掌握了一大半。日常开发中常见的定址方式有下面几种定址写法含义示例2第2行sed 2d file.txt删除第2行$最后一行sed $d file.txt删除最后一行/正则/匹配正则表达式的行sed /^#/d file.txt删除以#开头的注释行2,5第2行到第5行sed 2,5d file.txt1~3从第1行开始每隔3行sed 1~3d file.txt删除第1、4、7……行这里面最常用的是行号范围和正则匹配。范围地址2,5在处理固定区段时极其方便而正则地址在写配置清理和日志过滤时几乎天天用。还可以把行号和正则混合比如sed 1,/END/d file.txt表示从第1行开始一直删到首次出现“END”的那一行。还有一个容易被忽略的好用写法0,/正则/。它比1,/正则/更保险因为如果文件第一行恰好匹配正则1,/正则/会从第一行一直删到文件结尾容易造成误删而0,/正则/能避免这个边界问题。2.3 常用选项-n、-e、-i、-f的定位定址和动作是命令的零件选项则是调整命令行为的开关。我挑四个最常用的选项给大家讲清楚-n关闭默认输出。正常情况下sed会把每一行都输出一遍加了-n之后只有命令里明确要求打印的行才会输出。配合p命令使用可以从大文件里精准抽出需要的行。-e允许在同一条命令里执行多个编辑动作。比如sed -e s/a/b/ -e s/c/d/ file.txt相当于一条命令替换两种模式。-i就地修改文件。默认sed只是把处理结果打到屏幕上文件本身不变。加上-i之后结果直接写回文件。这是批量修改配置的利器但也是新手最容易误操作导致文件被改坏的选项。-f从脚本文件读取sed指令。适合多条复杂命令复用的情况。另外补充一点-e和分号有类似的效果sed s/a/b/; s/c/d/ file.txt也能写在一对引号里用分号分隔两种写法挑自己习惯的用就行。3. 增删改查第一梯队d、p、a、i、c五个高频动作实战3.1 删除d命令的几种典型打法删除是sed最常见的用途之一。d命令会删除模式空间里当前的内容并且不会输出该行。我实际工作中用最多的几个场景是# 删除指定行号 sed 3d file.txt # 删除第2到5行 sed 2,5d file.txt # 删除所有以#开头的注释行 sed /^#/d nginx.conf # 删除所有空白行和纯空格行 sed /^\s*$/d file.txt # 删除包含指定关键词的行 sed /pending/d order.log注意第二个到第四个例子里的正则^#表示以井号开头^\s*$表示从头到尾只有空白字符。这些正则在清理配置文件时特别常用。比如你得处理一个被注释塞满的nginx.conf一条sed /^#/d config.conf就能把无用的注释全部滤掉剩下的有效配置一眼就能看清。又比如处理从数据库导出的数据时夹杂的空行会让后续脚本解析出错用sed /^\s*$/d清理一下干净利落。另外如果你只想在屏幕上预览删除效果、并不想真正改动文件不加-i即可输出到stdout的结果可以直接通过管道继续处理或者重定向到新文件。3.2 打印p命令必须配合-n才有正确姿势p命令的作用是把当前模式空间的内容打印出来但正如前面说的sed默认会输出每一行如果你不控制输出用p就会造成重复输出。正确的用法是加上-n只让被选中的行出现在屏幕上# 打印第2行 sed -n 2p file.txt # 打印第2到5行 sed -n 2,5p file.txt # 打印所有包含ERROR的行 sed -n /ERROR/p app.log # 打印最后一行 sed -n $p file.txt这个命令组合在日志排查时太好用了。文件有十几万行你想看看第500行附近到底发生了什么用sed -n 500,520p app.log瞬间把那段上下文捞出来比用vim打开整个文件再跳转要轻快得多。有些朋友会问那grep ERROR app.log和sed -n /ERROR/p app.log有什么区别功能上确实重叠但sed的优势在于它是在一个通用的“流水线”框架里解题打印之后还能继续接替换、删除等动作而grep只是单纯的行筛选器。在管道里sed -n /ERROR/{;p}甚至可以同时打印行号和内容这是grep不方便直接做到的事。3.3 追加与插入a命令和i命令的实际影响范围a表示在匹配行后面追加新文本i表示在匹配行前面插入新文本。这两个命令在日常脚本里用于自动生成配置片段、往文件里写标记行。# 在第3行后面追加一行 sed 3a\这是一行新内容 file.txt # 在匹配到“启动失败”的行后面追加一行提示 sed /启动失败/a\请检查磁盘空间和日志详情 app.log # 在文件开头插入标题 sed 1i\# Generated by deploy script file.txt写到这里特别提醒一个常见坑在GNU sedLinux大部分发行版自带里a和i后面跟反斜杠再跟内容是比较稳妥的写法比如sed /xxx/a\new line file。如果你需要追加多行GNU sed里可以用\n直接拼接。但是BSD sedmacOS自带对这个语法兼容性比较差最保险的跨平台做法是把多个追加命令分开写或者实测后再部署到不同平台。我见过不止一次同一个脚本在Linux上跑得好好的拿到macOS上就报错最后查到就是a命令的换行写法差异。3.4 整行替换c命令的使用边界c命令会把匹配到的行整体替换成新内容它和s替换的区别在于s是在行内做部分替换c直接丢弃整行换上新行。最典型的场景是把配置文件里的某项值整行换掉而不关心这一行前半部分写了什么# 把第5行整行替换 sed 5c\max_connections 200 my.cnf # 把所有以port开头的行整行替换 sed /^port/c\port3307 my.cnf这个命令在修改服务配置时很好用尤其是你只关心某个参数名不在乎它原来在文件里的具体位置和缩进方式时。但也要注意c在处理范围地址时可能会对整个范围只输出一行替换内容不是每一行都替换。GNU文档里的说明不太直观实际测试最稳妥。4. 替换命令s的完整拆解分隔符、正则、分组引用与特殊字符处理4.1 为什么默认只替换第一个匹配从flags开始理解s命令是sed家族里出场率最高的角色基础语法是sed s/模式/替换内容/标志位比如sed s/8080/9090/ nginx.conf就是把每行第一个匹配到的“8080”改成“9090”。注意这里的关键词第一个。这是几乎所有新手踩的第一个坑——如果你不写标志位每行只替换第一个匹配到的内容后面再出现的同类内容就不会被处理。要替换所有匹配必须加g标志sed s/8080/9090/g nginx.conf常用标志位我整理了一张表标志作用示例g全局替换每行所有匹配sed s/a/b/g file数字只替换第N次匹配sed s/a/b/2 filep输出被替换的行配合-nsed -n s/a/b/p filei或I忽略大小写sed s/error/warning/I logw将结果写入文件sed s/a/b/w out.txt file其中i在GNU sed和BSD sed里大小写差异需要注意GNU sed支持I也支持i为了稳妥建议实测后统一。4.2 换掉默认分隔符/太挤了怎么办s命令的标准分隔符是/但在处理文件路径、URL这些本身就包含大量斜杠的内容时转义会让你抓狂。比如你想把/usr/local替换成/opt写成sed s/\/usr\/local/\/opt/g file可读性极差。此时最好的做法是换一个分隔符。sed允许你自定义分隔符常见的选择是#或者|sed s#/usr/local#/opt#g file sed s|http://old.com|http://new.com|g conf这个技巧极大提升了命令的可读性尤其是处理路径配置时强烈建议养成“遇到大量斜杠就换分隔符”的习惯。注意换分隔符之后原来的分隔符在匹配内容里出现就不需要转义了但因为其他地方可能需要保留反斜杠所以别把转义这件事彻底忘掉。4.3 和\1替换内容里的“引用符号”s命令的强大之处在于替换内容里可以用特殊符号引用匹配到的数据。两个最重要的符号是和\1。代表整个模式匹配到的内容。比如你要把每个“version1.0”里的版本号两边加上方括号可以写sed s/version[0-9.]*/[]/g file.txt这里[0-9.]*匹配的是一串版本号指代的就是匹配到的原文结果类似[version1.0]。\1则代表正则里第1个括号分组匹配的内容。这是重组文本格式的利器。比如日志里每行格式是“IP - 时间 - 状态”你想把IP和时间的位置换一下sed -E s/([0-9.]) - ([0-9:]) - (.*)/\2 - \1 - \3/ access.log用-E开启扩展正则后括号不用再加反斜杠可读性好很多。([0-9.])是第一个分组“\1”就是匹配到的IP([0-9:])是第二个分组\2是时间最后通过调整序号实现了字段重排。这个技能在清洗日志、整理导出数据时能帮你省掉写Python脚本的力气。4.4 特殊字符转义的完整清单s命令的替换内容里有几个字符如果不处理就会出问题需要用\转义否则会被当成“整个匹配内容”的引用。\本身是转义符号想输出一个反斜杠需要写\\。分隔符字符比如用/做分隔符时内容里的/需要转义为\/但如果你换用了#分隔符内容里的#又要被转义而/反而不用管了。举个例子你要把文本里所有的“AB”替换成“A和B”如果直接写sed s/AB/A和B/g结果会变成把“A”后面的任意内容替换掉因为被当成引用符号。正确写法是sed s/A\B/A和B/g file.txt还有替换路径时很多人在替换内容里忘记对反斜杠做处理结果输出的路径少了一截。我的经验是替换内容里只要有反斜杠或先写好对应转义再跑sed -n s/.../.../p测试输出确认无误再上-i。4.5 -i就地修改别丢掉了备份习惯-i是sed从“预处理工具”变身为“直接改源文件”的开关。它的坑主要集中在平台差异上GNU/Linuxsed -i s/8080/9090/g nginx.confmacOS/BSDsed -i s/8080/9090/g nginx.conf注意-i后面必须跟一个空字符串参数否则会报错。更稳妥的工作方式是配合备份后缀让sed自动生成原文件的备份sed -i.bak s/8080/9090/g nginx.conf这样执行后会产生nginx.conf.bak备份文件一旦改坏了还能快速回滚。我在生产环境里批量修改配置文件时几乎总是带着.bak后缀确认没问题再清理备份。这个习惯救过我不少次。5. 常见翻车现场与排查思路为什么你的sed不按剧本走5.1 明明写了替换怎么只改了第一个这是一个高频求助帖。比如echo apple orange apple | sed s/apple/pear/输出却是pear orange apple第二个apple纹丝不动。原因前面说过s命令默认只替换每行第一个匹配。解决办法也简单加上g标志即可。但要注意如果你正处在“替换了但好像只替换了一处”的困惑里第一反应应该是检查有没有g而不是怀疑sed坏了。5.2 p命令把文件内容打印了两遍有人写sed /ERROR/p app.log想要打印日志里的错误行结果发现每一行都出现两次。这就是没加-n的结果sed默认输出所有行p再额外打印匹配行于是普通行出现一次、匹配行出现两次。正确做法是sed -n /ERROR/p app.log。这个坑的根源在于对“默认输出”的理解不够扎实。建议新手在脑子里建立一个条件反射sed总有隐式的“输出模式空间内容”这一步加了-n才会关闭它用p命令时十有八九要配-n。5.3 地址正则里的斜杠和特殊符号写sed地址时正则里的/符号常常引发连锁错误。比如你想匹配内容里包含/usr/local的行如果直接写sed /\/usr\/local/d file满屏的反斜杠容易写错。解决方式和替换分隔符同理地址定界符也可以换写成sed \#/usr/local#d file就舒服多了。另外[、]、*、.这些正则元字符在匹配时要注意你是在做“正则匹配”而不是“字符串查找”。想匹配一个点号需要写\.否则.会匹配任意字符。比如你想删除包含版本号“1.0”的行sed /1.0/d会误删“120”所在的行正确写法是sed /1\.0/d。5.4 -i在符号链接和不同平台上的暴脾气-i的另一个隐蔽坑是它会破坏符号链接。假设你有一个软链接指向实际的配置文件用sed -i修改这个软链接时sed不会修改链接指向的原始文件而是把链接本身替换成一个新文件。这个行为在不同发行版上表现不完全一致但在Linux上确实存在。重要配置要改动前建议先用ls -l确认一下是不是软链接或者先复制一份实体文件再操作。平台差异也是重灾区。同样的命令Linux下sed -i s/a/b/g filemacOS下sed -i s/a/b/g file如果你在macOS的脚本里忘了加那个空字符串参数会直接报错“extra characters after command”。跨环境部署的脚本建议在文件开头统一判断系统类型或者干脆用Perl的一行命令替代sed -i打磨好自己的跨平台方案。5.5 变量替换时单引号与双引号的选择sed命令里如果要使用Shell变量你得特别注意引号用法。例如old8080 new9090 sed s/$old/$new/ file # 错误单引号里的$不会被Shell展开 sed s/$old/$new/g file # 正确双引号让变量被展开这里是一个非常常见的翻车点单引号会屏蔽Shell的变量展开导致sed去匹配字面量“$old”结果什么都不发生。反过来如果你在双引号里用了正则、等符号Shell也可能会尝试展开其中的$比如替换内容里有$1这种变量时就要小心。我的习惯是替换内容里涉及Shell变量时用双引号包整个sed表达式而涉及大量正则符号时优先用单引号两者冲突时用-e拆开或者先把变量拼好再传入。5.6 中文和特殊字符匹配不到先查locale部分朋友遇到过sed匹配中文失效的问题命令看着没问题但就是不匹配。多数情况跟当前系统的locale有关。如果LANGCsed的字节处理方式对多字节中文并不友好导致正则匹配失败。解决方法是把locale调到UTF-8export LANGen_US.UTF-8 sed -n /错误/p app.log在脚本里处理中文文本前先主动设置LANG或LC_ALL是一个好习惯。另外如果依然匹配不上还可以试试把.*换成[^ ]*这类更精确的模式避免贪婪匹配带来的意外。6. 把sed用进工作流管道、脚本文件与批量处理场景6.1 管道里串着用的经典姿势sed最强的应用场景不是单独操作文件而是嵌在管道里作为文本流转中的一个加工环节。最常见的组合是先从日志中筛出目标行再用sed清洗格式grep ERROR app.log | sed s/\[INFO\]//g | awk {print $1, $2, $NF}再比如实时监控日志时动态过滤掉不想看的内容tail -f app.log | sed /heartbeat/d这条命令会持续打印日志但把心跳包刷屏的行全部过滤掉只留下真正需要关注的业务日志。用管道把grep、sed、awk串起来处理线上问题是我日常排查问题最顺手的方式。6.2 用sed脚本文件管理多条复杂指令如果你要执行的sed指令超过两三条写在命令行里容易出错也难复用。这时候用-f参数把指令写进一个文件# 文件 process.sed # 删除空行 /^\s*$/d # 把INFO级别改成DEBUG s/INFO/DEBUG/g # 在文件末尾追加标记 $a\# end of processed file执行方式sed -f process.sed input.log output.log脚本文件的另一个好处是注释保留过几个月回来看还能明白当时这段处理逻辑是干什么的。6.3 批量修改多个文件的正确姿势批量改文件时新手最常见的错误是把for循环里的文件路径没加引号导致含空格的文件名被拆成多段。稳妥写法for f in /etc/conf/*.conf; do sed -i.bak s/old_value/new_value/g $f done文件名的引号必须加路径通配符在for里会正确展开但每个变量引用都要用双引号包起来。配合前面说的.bak备份后缀这个循环可以放心跑在生产环境。如果你需要批量替换Nginx站点目录下所有配置文件里的域名这行命令几分钟内就能搞定find /etc/nginx -name *.conf -exec sed -i s/old.example.com/new.example.com/g {} \;6.4 大文件性能为什么sed比脚本逐行读文件更快很多人会问处理大文件时为什么我写个Shell循环读每一行再判断替换比sed慢这么多核心原因是Shell的while read line循环每次读取一行都要启动一个外部命令来处理进程切换开销巨大而sed是内置的单进程C代码从头到尾在同一进程里完成所有处理效率完全不在一个量级。举个我踩过的例子一个3GB的日志文件用Shell while循环处理跑了一个多小时还没完换成sed s/pattern/replace/g log new.log三分钟出结果。这不是夸张是量级的差距。因此能交给sed、awk这类原生工具的任务绝对不要用Shell循环去硬扛。6.5 一个完整案例发布脚本里的配置热更新最后放一个我在实际发布脚本里经常用到的结构。假设项目发布时需要把配置模板里的端口、版本号、日志路径一次性替换成当前环境的值version2.3.1 port8080 log_path/var/log/app sed \ -e s/__VERSION__/${version}/g \ -e s/__PORT__/${port}/g \ -e s#__LOG_PATH__#${log_path}#g \ config.tpl config.ini这段的巧妙之处在于第一处替换用/做分隔符第二处也保持统一第三处涉及到路径变量就换成#做分隔符避免了斜杠转义。同时由于需要Shell变量展开整个sed表达式用双引号包裹。这个模板我一直在用配合备份和测试输出基本不会出问题。我的个人习惯是正式执行sed -i之前永远先用不带-i的命令跑一遍把输出核对无误之后再加-i真正落盘涉及重要文件时再配上.bak备份后缀。这短短两秒的确认帮我躲过了不少因为正则写错导致的配置清空事故。sed这东西用熟了之后会变成Shell编程里最顺手的一把刀希望这篇文章能帮你把这把刀磨利。