如果你写过一阵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.txt |
1~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 sed(Linux大部分发行版自带)里,a和i后面跟反斜杠再跟内容是比较稳妥的写法,比如sed '/xxx/a\new line' file。如果你需要追加多行,GNU sed里可以用\n直接拼接。但是BSD sed(macOS自带)对这个语法兼容性比较差,最保险的跨平台做法是把多个追加命令分开写,或者实测后再部署到不同平台。我见过不止一次,同一个脚本在Linux上跑得好好的,拿到macOS上就报错,最后查到就是a命令的换行写法差异。
3.4 整行替换:c命令的使用边界
c命令会把匹配到的行整体替换成新内容,它和s替换的区别在于:s是在行内做部分替换,c直接丢弃整行换上新行。最典型的场景是把配置文件里的某项值整行换掉,而不关心这一行前半部分写了什么:
# 把第5行整行替换 sed '5c\max_connections = 200' my.cnf # 把所有以port=开头的行整行替换 sed '/^port=/c\port=3307' 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' file |
p | 输出被替换的行(配合-n) | sed -n 's/a/b/p' file |
i(或I) | 忽略大小写 | sed 's/error/warning/I' log |
w | 将结果写入文件 | 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。
&代表整个模式匹配到的内容。比如你要把每个“version=1.0”里的版本号两边加上方括号,可以写:
sed 's/version=[0-9.]*/[&]/g' file.txt这里[0-9.]*匹配的是一串版本号,&指代的就是匹配到的原文,结果类似[version=1.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命令的替换内容里,有几个字符如果不处理就会出问题:
&:需要用\&转义,否则会被当成“整个匹配内容”的引用。\:本身是转义符号,想输出一个反斜杠需要写\\。- 分隔符字符:比如用
/做分隔符时,内容里的/需要转义为\/,但如果你换用了#分隔符,内容里的#又要被转义,而/反而不用管了。
举个例子,你要把文本里所有的“A&B”替换成“A和B”,如果直接写sed 's/A&B/A和B/g',结果会变成把“A”后面的任意内容替换掉,因为&被当成引用符号。正确写法是:
sed 's/A\&B/A和B/g' file.txt还有替换路径时,很多人在替换内容里忘记对反斜杠做处理,结果输出的路径少了一截。我的经验是:替换内容里只要有反斜杠或&,先写好对应转义,再跑sed -n 's/.../.../p'测试输出,确认无误再上-i。
4.5 -i就地修改:别丢掉了备份习惯
-i是sed从“预处理工具”变身为“直接改源文件”的开关。它的坑主要集中在平台差异上:
- GNU/Linux:
sed -i 's/8080/9090/g' nginx.conf - macOS/BSD:
sed -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' file - macOS下:
sed -i '' 's/a/b/g' file
如果你在macOS的脚本里忘了加那个空字符串参数,会直接报错“extra characters after command”。跨环境部署的脚本,建议在文件开头统一判断系统类型,或者干脆用Perl的一行命令替代sed -i,打磨好自己的跨平台方案。
5.5 变量替换时单引号与双引号的选择
sed命令里如果要使用Shell变量,你得特别注意引号用法。例如:
old=8080 new=9090 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有关。如果LANG=C,sed的字节处理方式对多字节中文并不友好,导致正则匹配失败。解决方法是把locale调到UTF-8:
export LANG=en_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 一个完整案例:发布脚本里的配置热更新
最后放一个我在实际发布脚本里经常用到的结构。假设项目发布时,需要把配置模板里的端口、版本号、日志路径一次性替换成当前环境的值:
version="2.3.1" port="8080" 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编程里最顺手的一把刀,希望这篇文章能帮你把这把刀磨利。