资讯动态

Linux sed 命令详解:从文本替换到正则实战

发布时间:2026/10/22 18:52:50 来源:尧图企业网站定制
你有没有过这种时刻一个项目里几十个配置文件要把旧的 IP 全部换成新的还要顺手改两个端口号。用 vim 一个个打开改眼睛盯久了容易看漏手一抖还可能改错行。我第一次系统性地用 linux sed 替换命令处理这种事情是在一次批量调整服务参数的时候从那以后这类活我再没手动干过。sed 是 Linux 下做非交互式文本替换最基础也最常用的工具它不强求你会写多复杂的正则也不要求你记住什么配置格式一条命令加一个表达式就能把批量修改这件事做得干净利落。下面我就一步一步拆开讲从 s/old/new 的基本结构开始到地址范围、正则分组、原地修改最后把我在实际使用中踩过的坑和几个高频实战场景都整理出来。无论你是刚接触 Linux 命令行的新手还是已经写了不少 shell 脚本的老手只要想用更少的时间处理文本替换这篇都值得读完。1. 为什么我劝你先学 sed 再学那些花哨工具1.1 它不是编辑器却干掉了大部分批量修改的活很多人第一次接触 Linux 文本处理都是从 vim 或者 nano 开始的。打开文件光标移过去删掉旧内容敲上新内容保存退出。这套流程在只改一两个文件的时候没什么问题可一旦文件数量上去了比如几十个配置文件分布在不同的目录里每个文件都要改同样的几处手动编辑的效率就非常难看了。sed 全称是 stream editor流编辑器。它不做交互不需要打开文件后人工盯着屏幕。它的工作方式更像一条流水线把文件内容按行送进传送带每一行经过一个处理工位处理完了再从另一头出来。你要做的只是告诉它“哪些行需要动、怎么动”。这决定了它天然适合批量、重复、有规则的修改任务。举个例子你有一个目录里面是 20 个服务配置文件每个文件里都有一行Timeout30现在要把 30 改成 60。用 vim 的话你得打开 20 个文件逐个找、逐个改中间还要小心别把别的30也顺手改了。用 sed 就是一条命令sed -i s/Timeout30/Timeout60/g /etc/services/*.conf这里-i表示直接修改原文件s是替换命令g表示整行全局替换。一行命令20 个文件全部处理完而且改动规则完全一致不会出现手滑漏改的情况。我常说Linux 文本处理工具里如果你只打算认真学一个那就学 sed。它没有 awk 那么强的字段处理能力也没有 perl 那么夸张的表达式支持但它的定位特别精准规则简单、批量替换、过程可脚本化。这三个特性组合下来已经覆盖了日常 80% 的文本修改需求。1.2 和 vim、awk、perl 的边界到底在哪里很多人会问那我直接用 vim 的全局替换不也行吗:%s/old/new/g确实也能做到但 vim 是交互式编辑器它需要你把文件先打开、占据一个终端会话然后才能操作。在 shell 脚本、cron 定时任务、CI/CD 流水线里vim 基本没办法自动化。sed 的优势是完全非交互命令写进脚本就能直接跑。awk 也不是不能替换它有sub()和gsub()函数但 awk 的重心始终在“按字段做统计和计算”替换只是它顺手提供的功能。如果你的需求是“对每一行的第 3 列做条件判断然后把第 5 列的值改写”那 awk 绝对比 sed 顺手。可如果只是“把文件中所有 A 变成 B”awk 写起来就远不如 sed 简洁。perl 更强大正则表达式的表达能力近乎完整还能处理多行模式、读取整个文件到内存但代价是它并不是每个系统都预装的而且语法学习曲线比 sed 陡不少。我的选择原则是能用 sed 解决的就别上 perl因为 sed 是 POSIX 标准的一部分几乎所有 Linux 发行版、BSD 系统、嵌入式环境里都有脚本移植性最好。下面这张表是我平时做选型时的参考工具核心优势主要不足最适合的场景vim交互式修改直观难以自动化批量效率低人工精改少量文件sed流式批处理脚本友好多行关联处理较弱规则明确的批量替换awk字段级处理能力强替换语法相对繁琐列处理、统计报表perl正则与复杂逻辑最强依赖安装学习成本高复杂文本结构解析一句话总结替换这件事先想 sedsed 不够用再往上找。这个学习顺序能让你少走很多弯路。2. 替换命令基础s/old/new/g 的每一个符号都有讲究2.1 先跑通第一个替换sed 的替换命令由一个s开头后面跟着分隔符、匹配模式和替换文本。最简单的例子echo hello linux | sed s/linux/sed/输出hello sed这个例子里s是替换命令第一个/和第二个/之间是“要匹配的内容”第二个/和第三个/之间是“替换成什么”。整个结构就是s/匹配/替换/标志。要注意一个非常容易踩的行为sed 默认只替换每一行中第一次匹配到的内容。如果一行里有多个相同的匹配后面的不会动。比如echo foo foo foo | sed s/foo/bar/输出并不是bar bar bar而是bar foo foo想要把一行里所有匹配都换掉必须加上g标志echo foo foo foo | sed s/foo/bar/g输出才是bar bar bar。这个“忘加 g”的坑我在后面会单独再讲因为它是新手最容易犯、也最隐蔽的一个错误。2.2 分隔符选错命令大概率翻车默认分隔符是/但如果你要匹配的内容本身就包含斜杠比如替换 URL事情就麻烦了echo http://example.com | sed s/http:\/\/example.com/https:\/\/example.com/为了转义斜杠命令里全是反斜杠可读性直线下降。其实 sed 允许你换分隔符#、、|都可以echo http://example.com | sed s#http://#https://#同样的效果看起来清爽多了。分隔符的选择原则很简单选一个在匹配模式和替换文本里都不出现的字符。如果内容里同时有/和#那就用或者|再往里躲一层。记住分隔符可以是任意字符但前后必须一致而且如果替换文本里包含你选中的分隔符依然要转义。我用分隔符的心得是处理路径、URL 时优先用#处理日志里带时间戳的文本时优先用|处理带正则字符的表达式时再考虑。别嫌事小分隔符选对了后期调试省一半时间。2.3 g、数字、i 这些标志到底控制什么除了gsed 替换命令还支持其他几个常用标志它们可以组合使用也可以单独使用。数字比如s/foo/bar/2表示只替换每一行中第 2 次匹配到的内容。g全局替换替换每一行所有匹配。i或I忽略大小写。只在 GNU sed 下支持BSD 的 sed 不一定有。p打印发生替换的行。配合-n参数使用效果最好可以用来只输出被修改过的行。w 文件把发生替换的行写入指定文件。组合起来非常灵活。比如你只想看日志文件里哪些行发生了替换不想看全部输出可以这样sed -n s/Error/错误/gp app.log-n让 sed 默认不输出任何行p只把发生替换的行打出来结果就是一份“修改痕迹报告”。这个用法在检查规则是否有误时特别好用。如果有多条替换规则要同时执行可以用-e拼接也可以用分号分隔sed -e s/foo/bar/ -e s/baz/qux/ file.txt sed s/foo/bar/; s/baz/qux/ file.txt两种写法效果一样。多条命令按顺序执行后一条命令处理的是前一条命令的输出结果。3. 精准控制范围从指定行号到按内容圈定3.1 行号定位的几种写法不带地址的替换命令会对所有行生效但真实场景里往往不是所有行都要改。比如配置文件的第一部分是注释第二部分才是真正的配置项你可能只想处理中间某个区间。sed 用“地址”来限定作用范围。地址可以是一个行号也可以是一个区间5s/foo/bar/只对第 5 行做替换。2,5s/foo/bar/对第 2 到第 5 行做替换。5,$s/foo/bar/从第 5 行到文件结尾。1~2s/foo/bar/第 1 行开始每隔 2 行处理一次也就是奇数行。举个例子有一个数据文件# header line name,score alice,90 bob,85你想跳过表头只处理数据行中的alice可以直接用行号范围sed 2,$s/alice/Alice/ data.csv注意一点sed 的行号和你在 vim 里看到的行号是一致的空行也算一行。很多人在手工数行号的时候忽略了空行结果替换到了错误的位置。处理之前先cat -n看一眼行号比凭感觉要可靠得多。3.2 按模式定位只改匹配到的行行号适合文件内容短、结构稳定的情况。如果文件很长或者每次数据的行号都会变更稳妥的方式是按内容定位。用/pattern/作为地址只有匹配该模式的行才会执行替换命令sed /^server/s/localhost/127.0.0.1/ nginx.conf这条命令的意思是只处理以server开头的行把其中的localhost改成127.0.0.1。其他行即使包含了localhost也不会被改动。还可以做一个区间定位比如只想处理第一个匹配# BEGIN到第一个匹配# END之间的内容sed /# BEGIN/,/# END/s/foo/bar/ config.txt这个写法的语义是从匹配# BEGIN的行开始到匹配# END的行结束在这段区间里执行替换闭区间两端都包含。它在处理配置文件里的功能块、代码里的代码块时非常有用。如果要实现“除了某个模式以外的行都替换”可以用!取反sed /^#/!s/foo/bar/ config.txt这个命令会替换所有不是以#开头的行里的foo。配合注释行做批量修改既精准又安全。3.3 一个配置片段改到怀疑人生的例子有一回我需要把一段 nginx 配置里某个 upstream 块的地址全部改掉但配置文件里其他地方也有相同前缀的地址不能无差别全局替换。当时文件长这样upstream backend { server 10.0.0.11:8080; server 10.0.0.12:8080; } # 外部调用地址 # server 10.0.0.11:8090;我只想改upstream backend {}块里的两个server下面的注释行绝不能动。如果用全局替换注释行也会遭殃。正确写法是sed /^upstream backend/,/^}/s/10\.0\.0\.1[12]:8080/10.0.1.100:8080/g nginx.conf这里用^upstream backend定位块的开始用^}定位块的结束中间范围内的10.0.0.11和10.0.0.12全部被替换到新地址。注释行由于在区间之外完全不受影响。这种“区间替换”是 sed 比较值钱的能力因为很多真实文件的修改需求并不是“全文件改”而是“某一段里改”。理解了地址机制才能发挥出 sed 的精准性。4. 正则进阶分组捕获、引用与贪婪匹配陷阱4.1 捕获组与反向引用把日期格式整个翻过来sed 的替换命令真正厉害的地方在于它能和正则表达式配合提取匹配内容的一部分再重新拼接成新的文本。这个能力靠的是“捕获组”和“反向引用”。先说基础正则。默认情况下sed 使用 POSIX 基础正则捕获组要写成\(和\)引用时用\1、\2。这样写比较丑所以几乎所有现代版本都支持-E参数开启扩展正则捕获组就可以直接写成(和)echo 2024-01-05 | sed -E s/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/输出05/01/2024匹配模式里([0-9]{4})捕获了年份([0-9]{2})捕获月份([0-9]{2})捕获日期。替换文本里\1、\2、\3分别对应这三个捕获组顺序随意调于是就把2024-01-05重排成了05/01/2024。注意这里替换文本里的/又需要转义了所以我个人在这种场景下更喜欢换分隔符echo 2024-01-05 | sed -E s#([0-9]{4})-([0-9]{2})-([0-9]{2})#\3/\2/\1#输出同样干净。这个技巧在做日志时间格式统一、CSV 字段重排时特别常用。还有一个引用符号它代表整个匹配到的内容不需要分组就能用echo error | sed s/error/[]/g输出[error]的作用是“把匹配到的字符串完整地引用进替换文本”非常适合给关键字加括号、加颜色标记之类的场景。4.2 贪婪匹配是怎么坑人的正则里最经典的陷阱是.*的贪婪性。它默认会尽可能多地匹配字符直到行为止而不是匹配到你“心理预期”的位置。举个例子你想把引号里的内容加粗echo namealice age30 | sed s/.*/[]/g你的直觉可能是第一个到第一个匹配一次第二个到第二个再匹配一次。但实际结果是.*会从第一个一直吃到最后一个把中间的alice age30全包进去最后输出namealice age30整行都被当成了一个匹配。解决这类问题的常用办法是用“非引号字符”来限定匹配范围echo namealice age30 | sed -E s/[^]*/[]/g[^]*表示“任意个非引号字符”它不会跨过引号边界所以能按对匹配到两个引号片段。这个细节很小但只要你处理过带引号的文本几乎都会被坑到一次。另外注意基础正则和扩展正则的差异默认情况下、?、|、{m,n}这些在基础正则里不是元字符就是普通字符。想用它们必须加-E。比如echo aaa | sed -E s/a/A/ sed s/^/ / file.txt不加-E时a只会匹配字面量a而不是一个或多个a。这一点非常容易踩如果你写出来的正则总是不生效先检查是不是忘了-E。4.3 替换文本里的特殊字符 和 \ 的转义问题匹配模式里有特殊字符需要转义替换文本里也有。替换文本中是“整个匹配的引用”如果你实际想输出的内容里包含字面需要转义成\。比如你想把所有email替换成emailsms不能直接写s/email/emailsms/g因为这里的会被解释成原先匹配到的email结果会变成emailemailsms。正确写法是echo contact email | sed s/email/email\sms/g输出contact emailsms同理替换文本里的反斜杠也有讲究。\1是反向引用\是字面的 而如果你需要输出一个字面反斜杠得写\\。这里还有一层 shell 的交互问题如果你用双引号把 sed 命令包起来shell 会先对字符串做一轮解释\可能被处理成\\可能被处理成\。所以我建议除非需要注入 shell 变量否则 sed 表达式一律用单引号包裹让 sed 拿到最原始的正则内容。5. 原地替换 -i 的坑备份、跨平台差异与权限5.1 -i 不是简单的覆盖文件-i参数让 sed 直接修改原文件很多人以为它就像写字一样把改动写回原文件就完了。实际上sed 的处理方式是读取原文件内容处理完生成一份完整的新内容写到临时文件里最后用临时文件覆盖原文件。这个机制带来一个容易被忽视的问题运行 sed 的用户需要对文件所在目录有写权限而仅仅对文件有写权限是不够的。因为覆盖操作本质上是“删除旧文件、改名新文件”目录权限不够就会失败。另外文件原有的属主、权限、扩展属性也可能会发生变化因为生成的新文件是按当前执行用户创建的。还有一个更隐蔽的坑如果目标文件是一个符号链接GNU sed 在-i时默认会替换符号链接指向的目标文件并重新创建文件这可能导致符号链接关系被破坏。遇到这类文件最好先确认一下它的真实路径再考虑要不要动。我对-i的态度很简单能不用就不用要用就 -i.bak。加一个后缀sed 会在原地修改前自动生成一个备份文件sed -i.bak s/foo/bar/g config.ini这条命令会生成一个config.ini.bak保存原始内容然后修改config.ini本身。5.2 GNU 与 BSD 的兼容写法Linux 上默认的 sed 是 GNU sed而 macOS、FreeBSD 上用的是 BSD sed两者在-i上的行为差异很大GNU sed-i可以不带后缀直接写sed -i s/foo/bar/ file。BSD sed-i后面必须跟一个后缀参数哪怕你不想留备份也要写sed -i s/foo/bar/ file表示“备份后缀为空”。如果把这个脚本拿到 macOS 上跑命令格式就会报错。跨平台兼容的写法是统一使用-i.bak。两种 sed 都能接受并且都会生成.bak文件改完确认无误后再手动删除这些备份。如果你经常写跨平台脚本还可以在脚本里做个简单封装sed_i() { if [ $(uname) Darwin ]; then sed -i $ else sed -i $ fi }然后再调用sed_i s/foo/bar/g file就能通吃两种环境。类似的差异还有-EGNU sed 和较新的 BSD sed 都支持遇到老系统可以用-r兼容但建议在脚本开头注释标明测试过的环境。5.3 没备份就改生产文件我救过的一个下午有一回我给一个服务改配置目标是把某配置文件中的日志路径换掉。当时图省事直接敲了sed -i s#log_path/var/log/old#log_path/var/log/new#g service.conf命令执行成功没有任何报错。但服务重启后一直报配置解析错误排查了半天才发现配置里有一行的内容本身包含字符而我在替换文本里没有意识到会被解释成“匹配到的整个内容”结果写出了一段诡异文本把配置语法破坏了。更麻烦的是由于用了-i且没有备份原始内容已经找不回来最后只能从版本控制里恢复再重新改一遍。那次之后我定了两条规矩第一凡是-i之前一定先把替换结果输出到标准输出看一眼或者输出到一个临时文件用diff对比确认无误再执行真正的修改。第二生产环境上一律用-i.bak改完把 .bak 文件保留一段时间确认运行稳定后才清理。这两条规矩听着保守但真的能省掉很多不必要的“救火时间”。6. 实战三个一定用得上的 sed 替换场景6.1 批量改配置文件中的 IP 和端口最常见的批量替换需求就是改配置里的 IP 地址和端口。比如有一批.conf文件里面所有10.0.0.5:8080都要改成10.0.0.6:9090而且只改那些确实包含这个地址的文件。先用grep -rl找出包含目标的文件列表再交给 sed 处理grep -rl 10\.0\.0\.5:8080 /etc/app/conf.d/ | xargs sed -i s/10\.0\.0\.5:8080/10.0.0.6:9090/g这里有两个值得注意的细节。其一grep -rl的-r是递归-l是只输出文件名这样就不会把一堆无关文件也拖进来修改。其二IP 里的.在正则里是“任意字符”必须写成\.才能匹配字面量点号否则像10x0x0x5这样的内容也会被误伤。如果想把这条命令包装成可复用脚本可以让 IP 以变量形式传入old_ip10\.0\.0\.5 new_ip10.0.0.6 sed -i s/${old_ip}:8080/${new_ip}:9090/g service.conf注意这时 sed 表达式必须用双引号才能让 shell 展开变量因此 old_ip 里的转义要提前处理好。更稳妥的做法是先对变量做正则转义避免点号被当成元字符。6.2 日志脱敏与口径统一处理日志是 sed 替换的另一大主场。最常见的是 IP 脱敏把日志里的 IPv4 地址全部打码sed -E s#([0-9]{1,3}\.){3}[0-9]{1,3}#[IP]#g app.log如果是手机号脱敏可以保留前三位和后四位中间打星sed -E s/(1[0-9]{2})[0-9]{4}([0-9]{4})/\1****\2/g app.log第二条命令里(1[0-9]{2})捕获前 3 位([0-9]{4})捕获后 4 位中间 4 位用星号替代。输出结果类似138****1234。这种脱敏在把生产日志发给外部团队排查问题时非常常用既能隐藏敏感信息又不影响对方查看日志结构和时间线。顺带一提如果你发现日志里错误级别写法不统一有的写error有的写ERROR有的写Error也可以一次性统一sed -E s/^[Ee]rror/ERROR/ app.log^锚定行首[Ee]匹配大小写两种写法统一成大写ERROR。这个操作对后续的日志检索和告警规则匹配特别有帮助因为很多分析工具对大小写是敏感的。6.3 在脚本管道里动态替换变量sed 替换不只是用来改文件也经常用于“模板渲染”。比如我有一个nginx.conf.tpl模板文件里面用__PORT__和__HOST__做占位符部署脚本里从环境变量读取实际值再生成最终的配置cat nginx.conf.tpl \ | sed s/__PORT__/$APP_PORT/g \ | sed s/__HOST__/$APP_HOST/g \ nginx.conf这里/分隔符又可能碍事因为端口和主机名里如果包含/或等字符sed 就会乱掉。建议占位符故意选__PORT__这种形式避免和 shell 变量符号$冲突也避免和分隔符冲突。如果只是简单的“占位符替换”很多人也会用envsubst命令但它会默认把环境变量符号$VAR也替换掉有时候太激进。sed 的好处是明确只会改你指定的占位符其他内容一律不动可控性更强。如果你担心变量值里带有 sed 特殊字符可以在传入前做一次转义或者干脆把用户输入先过滤一遍。我在 Docker 的 entrypoint 脚本里经常用这套逻辑等于是拿 sed 做了一个轻量级模板引擎稳定、可读、没有额外依赖。7. 什么时候我不会用 sed 替换7.1 涉及复杂结构时我会改用其他工具sed 再强大仍然是“面向行的工具”。一旦文本结构复杂到需要跨行理解或者处理对象是 JSON、YAML、XML 这类有嵌套结构的格式sed 就变得很难维护。举个例子JSON 文件里要改某个嵌套字段的值用 sed 靠正则匹配也许能改对但只要字段顺序变化、缩进变了、嵌套层级不同你的正则就可能失效甚至会把结构改坏。这时候用专业的jq工具处理 JSON或者用 Python 脚本解析才是最靠谱的。同样的道理YAML 文件里如果某个值在不同层级重复出现而你想改的只是特定层级的那一个sed 很难做“层级感知”的替换。这不是 sed 的缺陷而是它的设计边界。规则简单、结构扁平的文本sed 是神器规则复杂、需要理解结构的文本先考虑更合适的解析工具。7.2 我一直在用的安全替换流程最后分享一下我处理替换时一直坚持的操作顺序尤其针对重要文件# 第一步先看替换结果不要加 -i sed s/foo/bar/g service.conf /tmp/service.conf.new # 第二步对比差异 diff service.conf /tmp/service.conf.new # 第三步确认无误后再原地替换 sed -i.bak s/foo/bar/g service.conf这个流程看起来多了两步但每一步都在降低风险。先看结果能发现正则写错、匹配范围不对等问题diff能让你精确知道每一处改动是什么最后才落到修改。生产环境上尤其是配置文件、数据库脚本、定时任务我都建议按这个顺序来。补充一个小技巧如果你想知道替换命令到底命中了几处可以这样统计sed -n s/foo/bar/gp service.conf | wc -l-n只输出发生替换的行wc -l统计行数。虽然这不是精确的替换次数一行多处替换时只算一行但用于快速确认规则是否有生效已经非常够用。先看输出再动手、先备份再 -i、先测正则再上生产这三个习惯让 sed 从“看起来危险”变成“随拿随用”。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑