
天天在终端里跟文本文件打交道的人一定绕不开这个场景配置文件里几十个IP要换日志文件里某段格式要改或者一组文件里同一个关键词要批量替换。手写编辑器逐个文件去打那效率太低人也遭罪。这时候sed替换命令就是最顺手的那把刀。我用了十几年Linux接触sed到现在最常用的就是它的替换功能。这工具说白了就是专职处理文本的自动化流水线你给它输入文本流它按照你设好的规则逐行处理再把结果输出。不管是单文件修改、多文件批量处理还是嵌在shell脚本里做数据处理它都能顶上去。这篇就围绕sed替换命令把用法、原理、坑点一次理清楚适合刚开始接触命令行、被文本处理折磨过的开发者也适合想提升日常效率的运维和数据分析朋友。1. 先搞清楚sed到底在干什么很多人用sed时容易懵就是因为直接背参数、抄命令却不知道它背后是怎么工作的。结果换个场景就不会用了或者遇到意外行为完全摸不着头脑。1.1 不是文本替换器是流式工厂流水线我第一次接触sed时以为它就是一个按下按钮就全局替换文本的工具。实际上它是一台流式处理流水线。sed这个名字的含义就是流编辑器Stream Editor和vim这种交互式编辑器完全不同。它的工作方式可以这样理解有一条传送带文本文件的每一行都是一个被传送的物品。sed程序站在传送带旁边把物品一件一件拿下来检查这一行内容是否符合你设定的条件。如果符合就按照指定操作处理一下比如替换内容然后放到输出传送带上如果不符合就直接原样放行。整条流水线从头走到尾sed一次处理一行处理完立刻输出内存里永远只保留当前正在处理的行。这个机制解释了很多现象。比如为什么sed处理大文件很快因为它不需要把整个文件都装进内存哪怕是几个GB的日志文件也能平稳处理。为什么sed默认只替换每行的第一个匹配结果而不是全部因为这是设计者认为最常用的需求想要全换加个g标志就行。这些行为不是bug是流式处理设计下的必然特征。我打个比方一台流水线机器上有个检测探头检查每个经过的零件上有没有划痕有就翻新一下然后放行没有就直接放走。这个探头不可能回头看已经走过的零件也不关心下一个零件长什么样它只对当前手上的这个零件负责。sed就是这个探头。1.2 什么时候应该用sed什么时候不该用用熟了之后你会发现sed不是银弹。知道它的边界能让工作更高效。适合用sed的场景有这些批量修改多个文件的同一处内容比如把项目里所有配置文件里的IP地址换成新的。在管道命令链里充当文本处理中间环节比如从某个命令的输出中提取、清洗需要的信息。对日志文件做快速格式化或脱敏比如把时间戳去掉、把敏感ID打码。需要写进shell脚本里做自动化文本处理sed命令轻量、随处可用不像Perl或Python还需要依赖环境。不太适合用sed的情况需要跨多行进行复杂逻辑判断的文本处理。sed虽然支持范围寻址比如从第10行到第20行之间操作但对于如果这一行是A那接下来三行忽略直到遇到B才恢复这种有状态逻辑写起来很痛苦远不如awk甚至Python清晰。对CSV或JSON这类结构化数据进行解析和提取。CSV要处理引号转义、字段边界JSON要处理嵌套层级这些用专门的工具比如jq、Python的csv模块会稳妥得多。涉及浮点运算、复杂的数值变换。sed只认文本不做数学计算这种场景交给awk更合理。给新手一个判断标准如果你发现自己为了一个任务在sed上憋了超过十分钟或者写出来的sed命令长度快要超过半行屏幕了这时候就该停下来想想awk是不是更合适或者直接用Python写个一次性脚本。2. 替换命令的基础操作与细节真正上手首先要把s命令的底子打牢。这个命令是sed里应用最频繁的值得把每个细节都搞清楚。2.1 最经典的s命令从每行第一个到全部替换s命令的完整结构是sed s/旧内容/新内容/标志。默认情况下sed s/旧/新/只会替换每一行中第一个匹配到的内容。我举一个非常容易踩坑的小例子。有一个文本apple banana apple cherry apple pie执行sed s/apple/orange/输出结果是orange banana apple cherry orange pie注意第一行第二个apple没有变。原因我在前面说过sed逐行处理默认处理完第一个匹配就移动到下一行。如果想每一行的所有匹配都换掉加上g标志就行sed s/apple/orange/g输出orange banana orange cherry orange pie这里给一个参数速查表标志作用使用场景g全局替换一行内所有匹配都换最常用几乎默认必加数字如2只替换每行第指定个匹配只需要改第二个出现的旧词时p打印处理后的行一般配-n用只想查看被替换的行i忽略大小写替换时不区分大小写w 文件把替换结果写入指定文件需要单独保存匹配行组合使用也常见比如sed -n s/error/警告/p这个命令只打印发生替换的那几行在日志文件中快速定位包含关键字的记录非常好用。2.2 定界符的秘密不止斜杠s命令默认用/做定界符但如果你要替换的内容本身包含/比如路径就会写得很痛苦。像这样sed s/\/usr\/local\/bin/\/opt\/bin/g这一堆斜杠转义可读性极差稍不注意就多写或者少写一个反斜杠。其实sed的定界符可以是任意字符。替换路径时换成#或者|就清爽多了sed s#/usr/local/bin#/opt/bin#g也可以在替换URL时用比如sed shttp://old.example.comp https://new.example.compg这算是一个隐藏技巧很多用了多年命令行的人都未必注意到。但是要提醒一点选了哪个字符做定界符内容里如果再出现这个字符就必须转义。所以选定界符时先看看你要处理的文本里哪个字符出现得最少。处理路径用#处理带#号的文本就用/处理网址用根据内容灵活选择。2.3 原地修改-i参数顺手但危险sed默认把处理结果输出到标准输出原文件不动。如果想要直接修改原文件加-i选项。第一次看到这个功能我脑海里弹出一个词真方便。但有一次操作失误体验了它的真危险。事情是这样的当时有一个项目里面几十个yaml配置文件都要改某个缩进我写了一条命令把^两个空格缩进换成^四个空格缩进。结果因为正则没写对把所有的行首空格都删掉了。文件全乱了好在当时有用版本控制一条回滚命令才救了回来。从那以后我养成了三个习惯分享给你第一使用-i之前先不带-i跑一遍把输出检查一遍再动手。 第二重要文件先备份。sed -i.bak s/旧/新/ 文件这条命令会生成一个后缀为.bak的备份文件修改原文件。所有配置类文件、代码文件都应该这样操作。 第三尽量不要在不确定的场景直接对多个文件执行-i。先拿单个文件验证再推广到全部文件。另外还有个跨平台的区别。Linux上的GNU sed-i后面可以直接跟替换表达式。但macOS上的BSD sed-i需要额外的备份后缀参数如果没有提供后缀要这样写sed -i s/旧/新/ 文件注意-i和之间有一个空格。3. 正则表达式与sed替换的深度配合sed的真正威力藏在和正则表达式的配合里。掌握了正则替换命令才能说是真正入门。3.1 基础正则和扩展正则一个转义符的差距sed默认使用基础正则表达式BRE在这种模式下一些在别的正则引擎里可以直接用的元字符比如、?、{、}、(、)都必须加上反斜杠才能发挥特殊作用。举例来说想把连续出现两次以上的字母a替换成A在基础正则下要写sed s/a\/A/g注意那个\。如果不加反斜杠a在基础正则里就是普通字符a后面跟一个加号。这确实容易忘。好在GNU sed提供了-E选项有些版本也支持-r不过-E已经成为POSIX标准启用扩展正则ERE后写正则的方式和大多数编程语言一致sed -E s/a/A/g我个人强烈建议只要正则稍微复杂一点就加上-E。不是显得专业而是\、\?这种转义写法太反直觉容易出错。特别是管道上的命令错一个字符就会导致替换结果完全不对。延展一下在扩展正则模式下分组符号直接()次数量词直接{m,n}就不再需要转义。但注意这时候如果想要匹配字面意义上的()反而要普通字符本身就是说要匹配括号就要写成\(。3.2 分组捕获与反向引用把替换玩出花样这是sed替换最精彩的部分。正则里的括号不仅能分组还能把匹配到的内容记住然后在替换部分引用。举一个最常见的例子交换两列内容。有一行文本是姓氏,名字zhang,san li,si wang,wu执行sed -E s/(^[^,]),([^,]$)/\2,\1/这条命令中第一个(^[^,])捕获姓氏第二个([^,]$)捕获名字替换部分\2,\1就把两者顺序调换了。输出san,zhang si,li wu,wang分组捕获的价值在于你不需要知道具体内容是什么只要知道它的格式就能提取、重组、重排。这个能力在处理批量数据时节省时间的效果非常显著。另一个实用的引用是符号。在替换部分代表整个匹配到的内容。用它来给匹配内容加前缀或后缀比分组要方便得多。比如想给文中所有出现的版本号加上引号sed -E s/v[0-9]\.[0-9]//g如果文本是version v2.3 released执行后就是version v2.3 released。把整段匹配内容直接原样放到了引号中间不用写分组。和\1各有适用场景需要部分引用时用分组反向引用需要整段引用时用。有一个小组节需要特别注意如果你的替换文本里需要出现字面意义上的字符那就要写成\进行转义否则它会被sed展开成整个匹配内容结果会完全出乎你的意料。3.3 实战nginx访问日志中的信息提取这里我用一个实际场景演示正则与sed结合的完整操作。假设现在要分析一份nginx访问日志每一行格式大概长这样127.0.0.1 - - [12/Oct/2023:08:10:22 0000] GET /api/users HTTP/1.1 200 1024需求是把所有请求中/api/后面的路径部分提取出来去掉时间戳。直接看这个需求用单条sed命令就能完成sed -E s/^([0-9.]).*GET ([^ ]).*/\1 \2/这条命令分组的思路是第一组([0-9.])抓取IP地址第二组([^ ])抓取请求路径由于[^ ]可以匹配到空格前的所有字符正好截取完整的URI。替换时把IP和路径用空格连接输出。实际场景中如果你还需要把状态码提取出来可以扩展成三组sed -E s/^([0-9.]).*GET ([^ ]).* [0-9] ([0-9])$/\1 \2 \3/执行结果就是IP 路径 状态码的紧凑格式。这种操作在分析日志时非常顺手比打开Excel慢慢筛选要快得多。4. 实战案例从配置批量修改到日志清洗纸上谈兵没有说服力。这里走一遍几个真实场景从单文件到多文件从简单替换到组合处理。4.1 批量替换配置文件里的IP与端口假设某个测试环境的服务配置分散在多个目录的config文件中旧的数据库IP是192.168.1.100端口是3306现在要换成新环境的192.168.1.200和3307。先在单个文件上验证命令是否正确sed s/192\.168\.1\.100/192.168.1.200/g; s/:3306/:3307/g 文件名.conf注意这里有两处细节第一正则中的.要写成\.。因为在正则表达式里.是任意字符的通配符如果写192.168.1.100而不转义它会匹配192x168y1z100这种意外内容。虽然数字环境里这种意外出现的概率低但养成正确转义习惯很重要。第二端口替换时我在3306前面加了:这是刻意为之。避免把数据库配置中可能出现的其他数字串里包含的3306也替换掉。加限定字符缩小匹配范围是降低替换风险的有效手段。验证输出没问题后执行原地修改并备份sed -i.bak s/192\.168\.1\.100/192.168.1.200/g; s/:3306/:3307/g 文件名.conf注意分号的使用sed支持在同一条命令里用分号分隔多个操作表达式这比写多条sed管道效率高可读性也好。如果要对整个目录下所有conf文件做同样操作find . -name *.conf -exec sed -i.bak s/192\.168\.1\.100/192.168.1.200/g; s/:3306/:3307/g {} \;这里用find找到所有符合条件的文件再通过-exec对每个文件执行一次sed。好处是规则清晰、可控性强比直接sed接多个文件名更安全而且find可以附带更多过滤条件比如按时间只处理最近修改过的文件。4.2 日志清洗去掉噪音、统一格式日志文件往往格式混乱混合了时间戳、来源模块、各种括号信息想快速提取关键内容就得靠sed连番清洗。一个常见场景某服务日志每一行都带方括号包裹的时间戳和线程名类似[2023-10-12 08:10:22] [main] INFO 用户登录成功 usernamejohndoe [2023-10-12 08:11:45] [worker-1] ERROR 连接超时 ip10.0.0.5现在只想保留日志级别、事件描述、关键参数去掉时间戳和线程名。一条命令搞定sed -E s/^\[[^]]\] \[[^]]\] ([A-Z]) (.*)$/\1 \2/执行后INFO 用户登录成功 usernamejohndoe ERROR 连接超时 ip10.0.0.5这个模式中\[[^]]\]专门匹配方括号开头 若干非右方括号字符 右方括号用它来跳过时间戳这段噪音。如果你需要进一步把冒号前后的参数拆成键值对格式可以叠加后续sed处理。注意-E在这里的必要性如果不用扩展正则[^]]这种写法中的必须转义命令变成sed s/^\[[^]]\\] \[[^]]\\] ...写起来和心理负担都会大很多。4.3 与管道和其他命令的组合用法sed最常见的位置其实是在命令管道中间。比如从一个命令的输出中提取信息查看系统某进程及其启动命令过滤出需要的字段ps aux | sed -n s/.*python3 \(.*\.py\).*/\1/p | sort -u这条命令从ps aux输出里抓取所有python3进程启动时携带的脚本文件名去重排序后展示。-n加上末尾的p标志让sed只输出匹配的行不打印其他进程信息。再比如配合grep先筛选再替换grep ERROR app.log | sed -E s/.*ip([0-9.]).*/\1/ | sort | uniq -c这段组合的含义是先从日志中筛出所有ERROR行然后从每行提取ip后面的IP地址最后统计每个IP出现的次数。一条管道下来就把哪些IP引发了最多错误给统计出来了没有多余文件也没有中间状态。sed在这里扮演的就是提取器的角色。5. 常见问题与排查技巧实录最后这部分是我的踩坑合集。有些问题遇到过两三次才彻底想明白写出来帮各位减少试错时间。5.1 替换没生效的五个原因原因一忘了加g标志。这是最常见的。以为替换了所有的结果每一行只换了第一个。判断方法很简单把输出全部打印出来看一遍如果每行第二个旧值还在那就是这个原因。原因二正则匹配不到。尤其是想匹配行尾内容时容易忘记$锚点。比如你想删掉每行末尾的日期写成sed s/2023-10-12//如果日期不在行首这个命令可能碰巧替换了别处的文本而不是行尾的。正确写法应该是sed s/2023-10-12$//。原因三特殊字符没转义。替换文本或者模式中的.、、/、\这些字符该转义的没转义。排查方法推荐一个二分定位法先用最简单的字符串比如只替换一个单词做测试确认基础命令没有问题再逐步加入正则和特殊字符很快能找到哪一步出了问题。原因四引号混用导致变量没展开。在shell脚本里sed s/旧/$变量/和sed s/旧/$变量/的结果可以是完全不同的。单引号内的内容原样传给sed$变量不会被shell解析双引号内的$变量会被shell展开成变量的值。想用外部变量做替换内容必须用双引号包住整个表达式并且内部如果有其他字符需要转义时要注意处理。做一个对比就更清楚了NAMEjohndoe sed s/用户/$NAME/ log.txt # 结果输出里的用户被替换成了字面量 $NAME sed s/用户/$NAME/ log.txt # 结果输出里的用户被替换成了 johndoe原因五编码或不可见字符问题。文本里有\rWindows换行符残留或者\t你看上去是空格或者行尾但正则匹配不上。排查方法用cat -A 文件查看隐藏字符或者用sed -n l 文件让sed把不可见字符以转义形式显示出来。5.2 macOS与Linux的差异同一套sed命令在两套系统上行为可能不同。最典型的差异是-i参数Linux上-i直接接命令macOS上-i必须接备份后缀参数哪怕为空即sed -i ...。另一个差异是部分正则元字符的支持度BSD sed对\t这类转义的支持不如GNU sed稳定。如果你写的脚本需要在两个平台上跑两条建议一是用-E而不是-r开启扩展正则二是先小范围验证再全量执行。5.3 我的几个使用习惯文章最后分享几个实践经验基本上是花了不少冤枉时间才养成的习惯。第一重要操作必做备份。要么配合版本控制要么用-i.bak要么先cp 文件 文件.bak。成本很低但能救命。第二能一步到位就不要绕弯。一条sed命令能解决的事不要拆成五条管道。不是追求炫技而是减少中间环节出错的概率也让之后的维护者很可能就是两个月后的自己更容易理解。第三善用s///g;t这种循环替换技巧。sed默认替换不重叠匹配如果文本里有aaaa你想把aa替换成a一次sed s/aa/a/g的结果是aa而不是a。想要彻底替换到无重叠为止可以写成sed :a; s/aa/a/g; ta这是利用t标签实现循环跳转。这种技巧平时用得不多但真遇到时极其有用。第四也是最重要的一条习惯先打印后修改。任何一条sed命令都先不加-i跑一次看着输出确认是预期效果再加-i真正执行。这个习惯帮我避过了无数次灾难性的批量替换。sed命令的学习曲线不算陡峭基础的替换功能半天就能上手但想要用得行云流水还是得在实战中不断积累。碰到文本处理的活儿多想一想这条能不能用sed来切一刀用着用着就顺手了。毕竟对天天泡在终端里的人少浪费几分钟就能多喝一口咖啡。