凌晨两点多,一个批量改配置的活儿落到我头上:两百多份服务的配置文件,全都要把下游地址从一个旧的内部域名换成新的,还得分环境、分模块,旧的先注释掉留着回滚。手改不现实,用编辑器全局替换又容易误伤——有的文件里旧地址出现在注释里,有的藏在 fallback 列表中间,还有的写在一个多行块里面。那次我最后敲下去的是这么一条:sed -i.bak /^[[:space:]]*#/!s/old-svc\.internal/new-svc\.internal/g *.conf一条命令,一杯咖啡的功夫收工,而且每个文件都留了.bak,真出问题还能马上退回去。sed 这个工具在运维圈子里的地位其实挺微妙:人人都知道它叫流编辑器,人人都背过那句s/old/new/g,但真正敢在批量改配置、清洗日志、提取配置块这些活儿上放心交给它的,没想象中那么多。大多人卡在两个地方——一是被它那套模式空间的执行模型绕晕了,写出来的命令能跑但不敢确认对不对;二是被正则和-i的坑坑过一两次,从此见了 sed 就绕着走。这篇内容就是把 sed 从能背的那几条命令往敢用的那一套方法上推一推。我不会按手册顺序把命令字母挨个念一遍,而是按运维真实的工作路径来:先搞清楚它一行一行读进来之后到底发生了什么,再往上堆替换、删除、提取、批量落盘这些高频动作,然后把正则、分隔符、-i这三块最容易翻车的地方单独拆开讲,最后说说什么时候该干脆换工具。适合已经把基础命令忘得差不多、但天天要跟文本打交道的运维、后端和测试同学,零基础也能顺着往下看,有经验的可以直接跳到自己栽过跟头的那一节。1. 那条凌晨的批量替换命令,拆开看都是什么零件1.1 为什么这种活儿最适合 sed,而不是编辑器或脚本先说清楚 sed 的定位,不然后面所有技巧都是散的。sed 是Stream EDitor,流式编辑器:它读一段、处理一段、输出一段,从头到尾不把整个文件装进内存。这个特性决定了它在运维场景里的三件事特别顺手——文件有多大都不影响内存占用、能直接接在管道后面处理实时输出、语法短到可以塞进一行或者 Ansible 的 shell 模块里。对比一下就明白了。用 vim 批量改两百个文件,得靠录制宏或者:bufdo,一旦中途匹配错位,回滚成本极高;用 Python 写个小脚本当然更可控,但为了改一个字符串专门写个二十行的脚本、还得考虑编码和异常,边际成本不对等;而xargs sed -i这种组合,写出来就一行,不需要考虑文件对象缓冲区这些概念,写完就能跑。有个反直觉的结论值得先说:sed 不是万能的文本工具,它是一个专精的逐行替换与筛选器。它擅长的只有两件事——按行匹配,然后对匹配到的行做替换、删除、打印、插入。凡是超出这两件事的(求和、排序、字段计算、JSON 解析),都是它的能力边界之外,硬要用只会把自己写进坑里。第 6 节会专门讲这个边界。1.2 学 sed 的正确顺序:先地址,后命令绝大多数教程是从s命令开始讲的,因为替换最常用。但我的经验是,这个顺序会让人写出无数条能跑但不知道为什么能跑的命令。真正该先吃透的是地址(address)——也就是这段命令作用在哪些行上。sed 的完整语法骨架是这样的:sed [选项] 地址 命令 文件一个 sed 脚本是由若干条地址 命令组成的,不写地址就等于作用于所有行。比如:sed s/a/b/ file # 无地址,所有行都试一遍替换 sed 3s/a/b/ file # 只处理第 3 行 sed /start/s/a/b/ file # 处理所有包含 start 的行三条命令的长度差不多,但作用范围天差地别。现实中事故往往就出在这里:你想改配置文件里某一项,结果忘了加地址,把整份文件里所有同名 token 全替换了,包括注释里那行本来用于回滚的旧配置。所以我的建议顺序是:先花半小时把地址的写法摸透,再去看s的各种标志位,最后才是多行和保持空间。地址搞明白了,你写出来的每一条 sed 命令都是可预期、可验证的;地址没搞明白,你就是在靠运气跑命令。1.3 一条命令的四个零件:地址、命令、标志、选项还是拿开头那条命令拆:sed -i.bak /^[[:space:]]*#/!s/old-svc\.internal/new-svc\.internal/g *.conf拆成四块看:选项-i.bak:就地修改,并留.bak备份。这是 sed 的选项,不属于脚本内容。地址/^[[:space:]]*#/!:匹配行首可能有空白然后跟一个#的行,后面的!表示取反。合起来就是跳过所有注释行。命令s/old-svc\.internal/new-svc\.internal/g:替换。标志g:一行里所有匹配都替换,不是只换第一个。这四个零件里,地址和标志是 90% 问题的来源。正则写错会误伤,标志漏写会只换一半(g忘了是最经典的),-i用错会丢文件。命令字母本身反而简单,常用的就那么七八个。下面第二节就专门讲执行模型和地址,这是把 sed 从玄学变成可推导的关键。2. 模式空间:理解所有怪行为的钥匙2.1 一行一行读、默认打印、d 与 p 到底在干什么sed 的执行模型可以用一句话概括:每次读一行到模式空间,执行完所有命令后,若无特殊指令就打印,然后清空进入下一行。这个自动打印机制解释了 sed 最让人困惑的现象。sed s/a/b/ file # 输出整个文件(替换后的),因为每行都自动打印 sed -n s/a/b/p file # 只输出发生了替换的行第二行命令里有两个关键动作:-n关掉了自动打印,p是手动打印。所以它变成了一条过滤器——只吐出来替换成功的行。这个组合是运维里做从日志里捞出特定记录最常用的写法之一:sed -n /timeout/p app.log # 抓所有含 timeout 的行 sed -n /2024-05-1[0-9]/p app.log # 抓某几天的日志 sed -n /ERROR/p app.log | wc -l # 顺便数一下有多少条而d命令是另一套逻辑:它把当前模式空间的内容删掉,并且立刻开始下一轮循环,后面的命令都不执行了,也不打印。所以sed /^$/d file的意思是遇到空行就丢掉,不加进输出。这是删空行、删注释行的标准写法:sed /^$/d file # 删真空行 sed /^[[:space:]]*$/d file # 删只含空格和 Tab 的假空行 sed /^#/d file # 删注释行 sed /^#/d; /^$/d file # 注释和空行一起删 sed /^#/!d file # 反过来,只保留非注释行最后那条!取反很好用,等价于把不匹配的行全删掉,只留匹配的,比grep -v套管道清爽。这类命令在日常里出现频率极高——检查配置文件时先把几十行注释刨掉,剩下的才是真正生效的部分。2.2 地址的完整清单,以及那几个只有 GNU 才有的扩展地址分两类:行号类和正则类,都能组成范围。下面这张表是我实际写脚本时最常用的形态,建议直接记这张:地址形式含义典型用途3第 3 行改某个固定位置$最后一行尾部追加或删除/re/包含匹配的行最高频/re1/,/re2/从匹配 re1 的行到匹配 re2 的行(含)提取配置块1,10第 1 到第 10 行文件头部处理10,5第 10 行及其后 5 行(GNU)已知偏移量1~3从第 1 行起,每 3 行取一行(GNU)抽样0,/re/从文件头到第一次匹配(GNU)只改第一次出现/re/!取反跳过注释范围地址有两个容易误解的行为,必须说清楚。第一个:范围匹配是状态机,不是每行独立判断。/start/,/end/的意思是一旦遇到 start 就打开开关,直到遇到 end 才关闭。这意味着如果同一行里既有 start 又有 end,那么范围只有这一行;如果 end 出现在 start 之前,那么 sed 会一直开到下一次出现 end 为止,行为会很怪。第二个:第二条/re/只会在 start 之后的行里找,不会回头找。这个特性正好被用来提取配置块,比如从一堆虚拟主机配置里只挖出某一个:sed -n /^server {/,/^}/p site.conf-n配合p,输出就是从server {那一行到下一个顶层}之间的全部内容。这在排查某个站点的配置时非常好用,比awk写状态机短得多。0,/re/这个 GNU 扩展值得单独提一句,它解决了一个非常现实的痛点:# 只替换文件里第一次出现的 foo sed 0,/foo/s/foo/bar/ file # 对比:这条会把文件里所有 foo 都换掉 sed s/foo/bar/ file很多人以为不带g的s就是只换一次,其实它是每行只换第一次。要整个文件只换一次,0,/foo/是 GNU sed 下最干净的写法。2.3 保持空间与多行模式:什么时候才值得动用模式空间(pattern space)是当前处理的那一行,保持空间(hold space)是一块额外的暂存区,h/H把内容存进去,g/G取出来,x交换两者。听起来很高级,但坦白讲,日常运维里 95% 的活不需要碰保持空间。用它的典型场景只有两个:整个文件级别的大改造(比如反转行序),和跨行拼接。真正有点用的是多行模式,也就是N、D、P这几个命令。N是再读一行进来,和当前行用\n连接。最常见的用途是把折行的日志拼回去:# 把每两行合并成一行(用空格连接) sed N; s/\n/ / file # 把整个文件合并成一行 sed :a; N; $!ba; s/\n/ /g file第二条命令里的:a是标签,$!ba是如果不是最后一行就跳回 a,这是一个标准的循环写法。它的效果是先把整个文件读进模式空间,再一次性把换行替换掉。注意这条命令会把整个文件读进内存,大文件慎用。反过来的场景是把一行拆成多行,也就是在替换串里插入换行:# GNU sed 支持在替换端直接写 \n sed s/; /;\n/g config.txt # 想真正写入文件而不是只显示,习惯性加 -i sed -i s/; /;\n/g config.txt这里有个可移植性问题:GNU sed 的替换端支持\n表示换行,但一些老版本的 sed(比如某些 BSD 系)不支持,得用s/; /;\加回车再加/的写法。如果你写的脚本要跨环境跑,这一点得留意。还有一条被低估但特别好用的:q提前退出。sed -n /panic/{p;q} app.log的意思是找到第一条 panic 就打印并退出,在一个 5GB 的日志里找第一条记录,比grep -m1只在语义上更灵活,而且不会继续扫完全文,IO 上省得多。3. 日常真正用得上手的写法清单3.1 替换命令的四个标志位,以及跳过注释行的标准姿势s命令的完整形态是s/正则/替换/标志,标志位常用的有四个:标志作用备注g一行内替换所有匹配漏写是最常见事故p替换成功时打印该行必须配-nN(数字)只替换第 N 个匹配如s/a/b/2I或i忽略大小写GNU 扩展g漏写的后果很具体:一行的内容如果是listen 10.0.0.1:80, 10.0.0.2:80;,你想把两个 IP 都换掉,sed s/10\.0\.0\.1/10.0.0.9/只会改第一个,第二个原样留着,而且命令不报错——这种安静地改一半是最难排查的。数字标志在改配置时其实很有用:# 只替换一行中第二次出现的关键字 sed s/,/, /2 list.txt符号是替换端的一个特殊令牌,代表整个匹配到的内容。它的用途是在匹配内容外围做包装:# 给所有出现的数字加上方括号 echo port 8080, timeout 30 | sed -E s/[0-9]/[]/g # 输出:port [8080], timeout [30] # 把所有 short 类型的变量声明包一层 sed -E s/\bshort\b/[short]/g header.h但也是坑之一,如果你真的要插入一个字面的,必须写成\。这个第三节的正则部分会再提。跳过注释行是运维脚本里的刚需,标准写法就是开头那条:sed /^[[:space:]]*#/!s/old/new/g file拆解一下:^[[:space:]]*#匹配行首零个或多个空白字符,然后一个#,覆盖了# xxx和# xxx两种形式;后面的!是地址取反,让s只作用在非注释行上。这里用自己的字符类[[:space:]]而不是\s,是因为\s是 GNU 扩展,在 shell 里还得双重转义,写起来反而更绕。3.2 删除、插入、整行替换:改配置文件的三板斧d之外,还有三个整行级命令:a(append,在after后追加)、i(insert,在前插入)、c(change,替换整行)。它们的共同点是操作对象是整行,不是行内片段,所以定位必须用地址。# 在第 10 行后面追加一行 sed 10a\# added by deploy script file # 在所有匹配 backup 的行前面插入一行 sed /backup/i\# --- backup section --- file # 把匹配到的那一行整个替换掉 sed /^LOG_LEVEL/c\LOG_LEVELINFO filec命令特别适合某一项配置必须是固定值的场景,比如把LOG_LEVEL开头的行整行改成LOG_LEVELINFO。用s/^LOG_LEVEL.*/LOG_LEVELINFO/也能实现,但c更直观,而且不用操心原行里到底有没有等号和值——只要匹配到,整行就换掉。a命令有一个常见错误:在 GNU sed 里,a\后面跟文本是标准写法,但很多人会写成sed 10a # comment(漏了反斜杠),这时候 GNU sed 会把第一部分当写文件,行为很怪。稳妥的写法是用单引号包住,并在a后面紧跟一个反斜杠和一个换行,或者用 GNU 支持的一行式sed 10a # comment。跨平台脚本里我一般直接写:sed 10a\ # added by deploy script file看起来丑,但 GNU 和 BSD 都能正确解析。关机命令这一块不展开,重点说一个删除整块配置的常用组合。假设要删掉一个配置块:# 删掉从 [backup] 段头到下一个段头之间的内容(不含下一个段头) sed -n /^\[backup\]/,/^\[/{/^\[/!p} file这里用了一个小技巧:外层的范围定位到段头到下一个段头,内层的/^\[/!p表示不打印段头本身。这样一来,输出结果里就把 backup 整段剔掉了,而其他段完好。这种范围 内层过滤的组合是 sed 处理结构化文本的核心手法,比堆awk状态机短很多。3.3 打印、编号、加前后缀:那些看着不起眼但省事的小命令命令输出当前行号,配合别的命令可以用来做带编号的列表:sed file | sed N; s/\n/\t/ # 输出相当于 cat -n file单独用sed -n $是数行数最快的方式,比wc -l在某些场景下更稳(比如文件末尾缺换行符时,wc -l会少算一行,而sed -n $不会)。行首行尾加字符是纯靠替换完成的:sed s/^/ / file # 每行前面加 sed s/$/;/ file # 每行末尾加分号(做 SQL 或数据拼装时常用) sed -E s/^(.*)$/ \1/ file # 每行统一缩进两格最后那条用到了反向引用\1。反向引用是 sed 里另一把刀,后面第 4 节会详细讲,这里先记住形态:正则里用\(...\)(BRE)或(...)(-E模式)分组,替换端用\1到\9引用。还有一个用得特别多的小组合:sed做简单的编码兜底。比如 Windows 上传过来的文件带 CRLF,直接用会让 shell 脚本里每一行的命令都多一个\r,报一堆找不到命令的错。修复只需要:sed -i s/\r$// script.sh反过来,处理带 BOM 的 UTF-8 文件,用:sed -i 1s/^\xEF\xBB\xBF// file.csv这两个都是看到症状才知道要找它的典型,记下来能省不少排查时间。3.4 就地修改的完整姿势:备份、批量、批量再备份-i是 sed 线上作业的命门,它的完整用法有三种形态:sed -i s/a/b/ file # 就地改,无备份(GNU) sed -i.bak s/a/b/ file # 就地改,生成 file.bak sed -i s/a/b/ f1 f2 f3 # 一次改多个文件第二种是我在生产环境里的默认选择。.bak后缀不是装饰,它能在你发现正则写错时,用一条反过来的sed -i.bak2 s/b/a/ *.bak或者直接cp回去把损失降到最小。尤其是在批量改几十上百个文件时,那点磁盘空间换来的回滚能力完全值得。批量改的经典组合是和find/xargs一起用:# 找出所有 .conf 文件并批量替换 find /etc/app -name *.conf -print0 | xargs -0 sed -i.bak s/old/new/g这里用-print0和xargs -0是因为文件名里可能有空格。如果文件数量特别大(几千上万),xargs会分批调用 sed,每批都会重新启动一个进程,开销不小;这时候可以改用-exec ... :find /etc/app -name *.conf -exec sed -i.bak s/old/new/g {} 语法会把多个文件名拼成一次调用,比\;和xargs都更高效,而且不用担心参数长度限制。这一点很多教程没提,但在真机上一次跑几万个文件时,差距是肉眼可见的。4. 正则这一关:BRE、ERE、贪婪匹配和转义4.1 BRE 和 ERE 的差别,以及为什么建议默认加 -Esed 默认用的是 POSIX 基本正则(BRE),-E(或 GNU 的-r)打开扩展正则(ERE)。两者的差别集中在元字符的转义上,列个表看清楚:写法BREERE(sed -E)分组\(...\)(...)或|重复区间\{2,4\}{2,4}一次或多次\零次或一次\??反向引用\1\1同样是匹配两个或更多数字,两种写法是:sed s/[0-9]\{2,\}/NUM/ file # BRE,花括号要转义 sed -E s/[0-9]{2,}/NUM/ file # ERE,干净我的习惯是写复杂正则时一律加-E,原因很实际:bak 上面那些转义符号在双引号里还要被 shell 吃掉一层,\\{2,\\}这种写法一旦出错很难看出来。用-E之后至少少一层心智负担。但也要知道-E的一个副作用:用-E之后,\(就是字面意义上的左括号,不再是分组符。所以不要混用,一个脚本里要么全 BRE 要么全 ERE。另外要提醒的是,\s、\d、\w、\b这些是 GNU 的扩展,不是 POSIX sed 标准。在 Alpine 这类用 busybox 或者精简 sed 的环境里,sed s/\s\//可能完全不生效,而且不报错。写要跨环境跑的脚本,老老实实用[[:space:]]、[0-9]、[[:alnum:]_]、\b的替代方案(左右用\(^\|[^a-zA-Z]\)包裹),能省掉一次深夜排查。4.2 sed 没有非贪婪匹配,那该怎么办这是从 PCRE 转过来的人最不适应的一点。.*?在 sed 里不存在,.*永远是贪婪的——它会一路吃到这一行能匹配的最远位置。举个最容易踩的例子:你想把一段文本里每个引号内的内容拎出来。echo ax1 bx2 cx3 | sed -E s/.*(.*)/[\1]/ # 输出:[x3]结果只剩最后一个,因为.*吃掉了前面所有的x1 bx2部分。正确写法是用不包含引号的字符来约束长度:echo ax1 bx2 | sed -E s/([^]*)/[\1]/g # 输出:a[x1] b[x2][^]*就是模拟非贪婪的标准手法——用取反字符类代替.*。常见的等价替换有:想匹配别写应写引号内内容.*[^]*行内最后一个路径段/.*//[^/]*$到分号为止.*;[^;]*;到空白为止.*[^ ]*这条规则记牢了,sed 正则能力的上限会直接往上跳一大截。我处理过的一次事故就是这类:sed -E s/id(.*)/idXXX/本来只想把 URL 里 id 参数打码,结果.*一路吃到,把后面几个参数全吞了,幸好是打印到终端,没写文件。4.3 分隔符、、反斜杠:三个高频事故的根因第一个事故:分隔符冲突。s///的斜杠不是固定的,可以换成任意字符。改路径的时候如果不换,就得写成一堆\/:sed s/\/usr\/local\/bin/\/opt\/bin/g file # 能跑,但没法看 sed s#/usr/local/bin#/opt/bin#g file # 干净 sed s|/usr/local/bin|/opt/bin|g file # 也可以分隔符的选择原则很简单:挑替换内容里不出现的字符。改路径用#或|,改含#的内容(比如注释)就用/或。如果两边都含,用\x01这种不可能出现的字符也行,但一般没必要。注意一个细节:s#...#...#的时候,如果正则里要用#本身,还是得转义成\#。选分隔符只是让你少转义,不是免转义。第二个事故:被当成令牌。前面说过在替换端代表整个匹配。如果替换内容本身含,比如要把某个参数替换成一个带查询串的 URL:sed s/id[0-9]*/id100type1/ file这条命令会把展开成匹配到的原文,结果完全不是想要的。正确写法是\:sed s/id[0-9]*/id100\type1/ file这个坑在你处理 shell 脚本、SQL、URL 的时候几乎必然会遇到一次。第三个事故:反斜杠的层数。sed 里的反斜杠至少要过三层——sed 自己一层,shell 双引号一层,如果 sed 脚本本身写在字符串里还要再加一层。所以经验法则是:sed 脚本永远用单引号包。单引号里 shell 不做任何解释,你只需要考虑 sed 那一层。必须插入 shell 变量时才切到双引号,并且心里清楚多了一层转义:old10.0.0.1; new10.0.0.9 # 因为 IP 里的点需要转义 sed 层,写成 \. 在双引号里又会变成 . sed -i s/${old//./\\.}/${new}/g file # 用 bash 的参数替换先把点转义好这种写法绕但可靠。更省事的做法是用分隔符避开#,然后直接用双引号,前提是变量值里没有特殊字符:sed -i s#$old#$new#g file前提是你确信$old和$new里不含#、、\。生产环境里这三点往往不能确信,所以我更倾向于把待替换的字符串写到临时 sed 脚本里,用sed -f script.sed file调用,彻底躲开 shell 转义。5.-i背后的文件系统真相,以及我栽过的四个跟头5.1 它不是原地写,是临时文件加改名-i这个名字很有误导性,它给人的感觉是在文件的字节上原地替换。实际不是。GNU sed 做的是:在同目录下创建一个临时文件,把处理结果逐行写进去,处理完成后把临时文件 rename 成原文件名。原来的文件内容从头到尾没被改过,只是被换掉了。理解这一点,后面所有的坑都能自己推出来。5.2 硬链接会断、扩展属性会丢、软链接会被替换坑一:硬链接断开。如果a.conf和b.conf是同一个 inode 的两个硬链接,你对a.conf做sed -i,生成的是一个新 inode,b.conf仍然指向老 inode、老内容。改之前两个文件联动,改之后各走各的。这在一些用硬链接做多副本配置的环境里会非常隐蔽——你改了一个,另一个没变,而且还是正确内容,排查时根本想不到。坑二:文件元数据不完整。GNU sed 会尽量保留原文件的权限位和属主,但扩展属性(xattr)、POSIX ACL、SELinux 上下文这类元数据不保证保留。在启用了强制访问控制的机器上,sed -i之后文件标签变了对不上策略,服务起不来,报错还跟权限没关系,非常难查。遇到这种情况,改完记得用restorecon之类的工具把上下文还原。坑三:软链接被替换成普通文件。如果current.conf是指向v2.conf的软链接,sed -i会直接把链接本身替换成一个普通文件,链接关系没了,而v2.conf原封不动。你以为改了实际生效的配置,其实只改了个影子。GNU sed 有--follow-symlinks选项可以跟随链接改目标文件,但这一步在很多脚本里被漏掉了。我的做法是:批量改之前先跑一次find . -type l看看有没有软链接,有的话统一用--follow-symlinks,或者干脆先readlink -f拿到真实路径再改。5.3 那条把文件清空的经典命令这个坑几乎每个做运维的人都踩过或者见过:sed s/old/new/g app.conf app.conf跑完打开一看,文件是空的。原因不复杂:shell 在启动 sed 之前就先把 app.conf执行了,文件被截断成 0 字节,然后 sed 去读这个空文件,读到 0 行,写出 0 行。整个过程没有任何报错。正确的三种写法:sed -i s/old/new/g app.conf # 首选,自带原子性 sed s/old/new/g app.conf app.conf.tmp mv app.conf.tmp app.conf sed -i.bak s/old/new/g app.conf # 最保险,有备份如果不得不用临时文件, mv这一步不能省,否则中途出错会留下半截文件。还有一个更隐蔽的变体:-i和一个本身从 stdin 读数据的命令混用。比如cat file | sed -i s/a/b/,这里的-i没有意义(没有输入文件),sed 会直接报错退出。想通管道又要落盘,只能是sed ... tmp mv。5.4 macOS 和 Linux 的语法分叉,以及能跨平台的写法在 macOS 上跑sed -i s/a/b/ file,会得到类似sed: 1: file: invalid command code f的报错。原因是 BSD sed 的-i必须带参数,表示备份后缀;不想留备份就传一个空字符串:sed -i s/a/b/ file # macOS / BSD sed -i s/a/b/ file # Linux / GNU如果你的脚本要在两种环境都跑,有三种写法:第一种是检测后分支:if sed --version /dev/null 21; then sed -i s/a/b/ file else sed -i s/a/b/ file fised --version在 GNU 上能输出,在 BSD 上会报错,这是一个可靠的分支判断。第二种是干脆不用-i,统一用临时文件 mv,兼容性最好:sed s/a/b/ file file.tmp mv file.tmp file第三种是装 GNU 版的 sed(通常命名成gsed),脚本里统一用gsed。这在团队内部环境里最简单,但对外发脚本不友好。还有一种跨平台的细节:sed -E在 GNU 和 BSD 上都支持,但更老的版本只认-r。如果目标环境不确定,优先用-E,-r反而更GNU 专属一些。5.5 大文件与磁盘空间:一个容易忽略的成本-i是临时文件加改名,意味着处理 N GB 的文件时,磁盘上需要额外 N GB 的空闲空间,而且写入量翻倍。改一个 20GB 的日志文件,如果磁盘只剩 5GB,sed -i会中途失败,留下一堆残留的临时文件(通常是sedXXXXXX),原文件倒是完好的。这么大批量的处理,我的经验是先用sed -n做检查(不写盘),确认匹配范围正确,再考虑是否真的要落盘。如果只是清洗过滤而不是局部修改,直接用管道重定向到新文件、校验后替换原文件更稳:sed /pattern/d big.log big.log.clean mv big.log.clean big.log另外,GNU sed 的-i在 NFS 这类网络文件系统上的 rename 行为不一定原子,多台机器同时对同一文件做-i可能互相覆盖。共享目录里的配置文件,尽量别用-i改,走配置管理工具或者加锁。6. 边界感:这些活儿不该硬塞给 sed6.1 求和、分组、字段运算,该交给专门工具sed 的定位是逐行替换与筛选,它没有字段概念,没有变量,没有累加。有人试图用 sed 做去重、求和、按列统计,写出来的脚本长度和可读性都不忍直视。下面这些是明确的越界:需求硬用 sed该用什么按列求和写循环加保持空间awk {s$2} END{print s}排序去重基本做不到sort -u按字段过滤正则勉强能做awk $3 100统计出现次数做不到sort | uniq -cJSON 取值极其脆弱jqYAML 改值会破坏格式yqCSV 改列引号内含逗号就崩csvkit或专门解析器这不是说 sed 弱,而是每把工具都有自己的射程。用awk做两列求和是 5 个字符,用 sed 写出来要几十行而且没人看得懂,这个选择其实不需要犹豫。6.2 JSON、YAML、CSV:格式越结构化,越不能用 sed 硬改sed 是纯文本正则层面的工具,它不理解结构。这三个格式的共同特点是存在嵌套和转义,一旦正则匹配跨进了不该进的层级,输出就不再是合法格式。以 JSON 为例,表面上看sed s/port: 8080/port: 9090/能改,但实际上:port: 8080可能出现在嵌套对象里,也可能是某个字符串值的一部分;紧凑格式里根本没有空格;修改后可能改变字段顺序或破坏缩进(虽然 JSON 语义上无所谓,但 diff 会变得很难看)。YAML 更危险。缩进就是语法,一个不留神把两行之间的相对缩进改了,文件解析直接失败,而且报错位置往往离你改的地方很远。CSV 的问题在引号。a,b,c,d,e这种带引号包裹逗号的字段,用sed s/,/|/g会把引号里的逗号也换掉,数据就此损坏。真要处理 CSV,csvkit或者直接上 Python 的csv模块,几行代码的事,比调正则省心得多。我的判断标准很粗暴:如果这个格式有解析器,就用解析器。只有在明确知道我要改的位置在文件里只有一处、且这一处的上下文独一无二时,才用 sed 做定点修改。6.3 一份选型对照,以及我自己的默认决策路径最后把决策路径整理一下,这部分是我这些年踩坑之后沉淀下来的默认判断:场景首选备选单行内字符串替换,范围明确sed svim:%s删注释行、空行、某类日志sed /re/dgrep -v提取两个标记之间的块sed -n /a/,/b/pawk状态机按列处理、求和、统计awkPython复杂查找(多条件、文件类型)find-大文件抽样看前几条sed -n 1,20p/sed qhead批量改多文件find xargs sed -i.bak配置管理工具结构化格式改值jq/yq专门脚本需要幂等、可回滚的批量变更配置管理工具脚本 备份这里有个经验值得单独讲:能用grep先验证就不用急着上sed -i。改配置前,先跑一遍grep -n old-svc *.conf,看看命中了哪些文件哪些行,心里有个数;再用同样正则跑sed -n /old-svc/p确认 sed 视角下的匹配结果;两次对上,才敲-i。这个两次确认的习惯帮我避免过好几次改错文件的事故,成本不过多几秒钟。另外还有一个我在实际使用中养成的习惯:批量改之前先算一下命中数量。find /etc/app -name *.conf -exec grep -l old-svc {} | wc -l如果这个数字和预期(比如你知道只有 37 个文件用到这个地址)对不上,就说明匹配条件写宽了或者写窄了,这时候停下来检查,比改完再回滚便宜太多。sed 这个工具,学它花的时间其实很短,真正的成本在于建立我这条命令到底作用在哪些行上的确定性。地址想清楚,正则用取反字符类控制住长度,-i记得带备份、碰到软链接和结构化格式提前绕开,这三件事做到,它就能从背过的命令变成敢在线上敲的手感。至于更复杂的文本流转,交给awk和专门的解析器,各司其职,才是长期省命的方式。
阅读完成 · 觉得有帮助?