
做Linux下的shell编程尤其是搞文本处理sed这个命令是绕不开的。我最早接触sed的时候总觉得它像天书看着满屏的斜杠和反斜杠直接头皮发麻。后来真正上手写脚本、处理日志、批量改配置才体会到这工具到底有多强。文本处理是shell编程里最基础也最高频的场景而sed又是文本处理三剑客里心思最细腻的一个学会了它很多你原本要写十几行循环才能干完的活一行命令就能解决。这篇文章就是写给想认真学shell、又对sed有点发怵的朋友。我会把它掰开揉碎从执行原理、核心命令到自己动手的实战脚本再到我踩过的好几个坑一次都跟你说清楚。不画大饼不绕弯子就是实打实地把sed讲明白。看完你起码能做到拿到一个文本处理需求心里大概知道该用sed的哪几招写出来的命令不出洋相。1. 先搞懂sed的工作方式不然越学越乱很多教程上来就让你背sed命令格式什么sed s/old/new/g file背是记住了可一换需求就懵。我不建议大家这么学真正要理解的是sed处理文本的底层流程。只有知道它内部是怎么干活的你才能随手写出符合预期的命令。1.1 模式空间sed为什么是一行一行处理的sed全名叫Stream Editor流编辑器。这里的“流”字很关键它意味着sed不是一次性把整个文件读进内存而是像流水线一样一次读一行处理完再读下一行。每读进来的一行文本会先被放进一个叫“模式空间pattern space”的缓冲区里然后你写的那些sed命令全都施加在这个模式空间里的内容上。处理完这一行模式空间的内容会被清空或者覆盖接着读入下一行周而复始直到文件末尾。这个机制解释了很多新手困惑的现象。比如你写sed s/foo/bar/ file它会把每一行里第一次出现的foo都替换成bar不是因为sed聪明到每行都知道而是因为它对每一行都独立执行了一遍替换命令。理解了模式空间你就会明白sed天然就是逐行处理的那些要跨行匹配的需求要么用特殊的命令比如N、H这类的多行模式命令要么就得考虑换awk或者perl。另外还要知道sed默认会把处理完的每一行内容打印到标准输出所以你不加-n参数的时候屏幕上会哗哗地滚动输出整个文件的内容只不过被改过的地方已经变了。这个默认行为让不少人刚开始误以为“sed命令会修改原文件”其实根本不是它只是把处理后的结果输出到终端而已。原文件从头到尾没被动过。想要真正修改文件得配合-i参数这个后面细说。1.2 命令格式与常用参数-n、-e、-i、-fsed的基础用法长这样sed [选项] 脚本命令 [文件...]其中脚本命令的核心结构是“地址命令”。地址用来定位告诉sed你的命令作用于哪些行命令就是你要干的事比如替换、删除、打印。这个“地址命令”的结构是整个sed的灵魂后面我展开讲。先记几个高频参数。第一个是-n关闭默认输出。刚才说过sed默认会把模式空间的内容打印一遍但有时候我只想看到被我匹配处理的那几行不想看全文件。这时候就加-n然后配合p命令打印你想看的内容。例如只想看第2行到第4行可以这么写sed -n 2,4p file.txt第二个是-e添加脚本命令。如果一条sed命令里要执行多个操作可以写多个-e。比如同时做两个不同的替换可以这样sed -e s/foo/bar/ -e s/baz/qux/ file.txt其实你也可以用分号把多个命令串在同一个引号里比如sed s/foo/bar/; s/baz/qux/ file.txt效果一样。分号是命令分隔符怎么舒服怎么来。第三个是-i直接修改文件。这是sed真正发挥实用价值的地方。sed -i s/foo/bar/ file.txt这条命令会直接把file.txt里的foo替换成bar并且写回原文件屏幕上不打印任何东西。注意这是一个破坏性操作建议在测试阶段别急着用-i先用不带-i的方式跑一遍看看输出对不对确认无误后再加-i。更稳妥的做法是用-i.bak这样的形式直接备份原文件sed -i.bak s/foo/bar/ file.txt执行完你会看到目录里多了一个file.txt.bak那就是修改前的原始副本。我强烈建议你在处理重要配置文件的时候养成这个习惯万一改出问题了还能一键还原。第四个是-f从脚本文件读取命令。当你要执行的命令特别多写成一长串命令行不现实的时候可以把命令逐行写进一个文本文件里然后sed -f script.sed file.txt。这个适合复杂的文本处理流程脚本文件还可以复用算是个小型自动化工具了。我一般会把一些固定的清洗流程存成.sed脚本比如“去行尾空格压缩空行替换制表符”下次处理同类文件一行调用就行。还有一个-r参数GNU sed里是-rmacOS里是-E启用扩展正则表达式。扩展正则的好处是括号不再需要写反斜杠转义、?、|这些元字符直接可以用。我平时写sed基本都会带上-r不然一堆反斜杠看得眼睛疼。不过要注意macOS自带的BSD sed和Linux的GNU sed在参数上略有差异后面坑的部分我会专门提。2. 会用这几招sed就入门了一半sed的命令汇总起来有二十多种但日常真正高频率用到的一只手数得过来。s替换、d删除、p打印、a追加、i插入、c更改、y转换。把这几个配合地址范围用熟你已经能应付绝大部分文本处理场景了。2.1 替换命令s从基础到进阶s命令是sed里最常用的没有之一。基本格式是sed s/正则表达式/替换内容/标志。注意这里的分隔符默认是斜杠/但你可以换成任何字符只要保持一致就行这个特性极其实用后面我会单独讲。先看最基本的替换echo hello world | sed s/world/sed/输出hello sed这里有个新手很容易忽略的点s命令默认只替换每一行中第一次匹配到的内容。你想把一行里的所有匹配都换掉得在末尾加g标志意思是global全局替换。比如echo foo foo foo | sed s/foo/bar/g输出是bar bar bar不加g的话只会输出bar foo foo。这个g的缺失是很多刚上手的人“明明替换了但怎么没全换”的根本原因。除了gs命令还有其他有用的标志。数字标志可以让sed只替换第N次出现的匹配。比如一行里有多个foo你只想换第二个echo foo foo foo | sed s/foo/bar/2输出是foo bar foo。这个“指定第几次替换”的功能配合循环还能实现一些比较妖的操作比如交替替换。p标志替换后打印发生改变的行一般和-n配合使用sed -n s/foo/bar/p file.txt这条命令只输出那些发生过替换的行。这个用法在“从一堆日志里只揪出有特定关键词且需要顺手改动的行”时非常好用相当于替换过滤一次搞定。w标志把替换结果写到指定文件里。比如sed s/foo/bar/w output.txt file.txt它会正常输出替换后的结果同时把被修改过的行写入output.txt。这个在做数据分拣的时候有用比如从大文件中把符合特征的行单独抽出来存成一个新文件。还有I标志GNU sed匹配时忽略大小写。我现在用的多一些因为日志里经常出现大小写混杂的情况sed s/ERROR/警告/I app.log2.2 地址范围用行号说人话为什么说“地址”是sed的灵魂因为没有地址的命令作用在每一行上带了地址命令才有的放矢。最基本的地址就是行号。sed -n 3p file.txt # 打印第3行 sed -n 5,10p file.txt # 打印第5到10行 sed 1d file.txt # 删除第1行 sed $d file.txt # 删除最后一行$在这里表示文件末尾行号地址还支持一些特殊写法。1,5表示从第1行到第5行5,$表示从第5行到最后一行1~2表示从第1行开始每隔2行处理一次也就是奇数行。这个步进地址在做隔行处理的时候很顺手比如我想把偶数行全部删掉sed 2~2d file.txt除了行号更强大的是正则表达式地址。用一对斜杠包住正则就能匹配对应的行。比如sed -n /error/p app.log # 打印包含error的行 sed /^#/d config.conf # 删除所有以#开头的注释行地址还可以组合成范围从匹配第一个条件的行开始到匹配第二个条件的行结束。比如从包含“BEGIN”的行开始到包含“END”的行结束打印这个区间sed -n /BEGIN/,/END/p data.txt这个写法在处理日志区间、代码片段、配置块时特别常用。比如我想抓取nginx配置里server块的内容可以从server {开始到第一个}结束配合打印就能精确提取。不过用正则地址范围有个细节如果文件中出现了两次BEGIN到END的区间sed会对每个区间都执行一次操作不是只处理第一个。对大多数人来说这其实是个好消息因为文本里同类结构往往不止一个这个全家桶式的匹配反而省事。如果只想处理第一次出现的区间可以把范围写成0,/END/注意这个起始地址是0不是1这是GNU sed的特殊写法表示从文件开头就开始匹配但打印的起点是包含END的那一行。2.3 分隔符的讲究为什么有人用|不用/默认情况下s命令的分隔符是/这就带来一个问题如果你要替换的内容本身也包含斜杠比如替换一个路径那命令就会变成地狱级别的转义地狱。举个例子你打算把配置里的/usr/local/bin替换成/opt/new/binsed s/\/usr\/local\/bin/\/opt\/new\/bin/g config.txt满屏的\/稍微看走眼就写错。解决办法很简单换一个分隔符就行了。s命令允许你把分隔符换成任意字符只要在用的时候保持一致。比如sed s#/usr/local/bin#/opt/new/bin#g config.txt sed s|/usr/local/bin|/opt/new/bin|g config.txt一下就清爽了。这个#和|当分隔符的做法在处理路径、URL、日期等本身就带斜杠的文本时属于刚需技巧。我自己的习惯是遇到含/的文本优先用#因为它在正则里含义单纯不太容易跟其他符号混淆。替换内容里的也是一个高频技巧。在替换内容中表示“整个匹配到的文本”相当于一个变量引用。比如我想把日志里所有出现的IP地址用方括号括起来sed -r s/[0-9]\.[0-9]\.[0-9]\.[0-9]/[]/g app.log这里的就代表匹配到的IP地址本身不用再写一遍冗长的IP表达式。还有一个我经常用的场景是把某段固定文本包上特殊标记比如给所有出现的TODO前加上[!]sed s/TODO/[!]/g source_code.txt替换内容里再用\1这类反向引用可以跟正则里的括号分组配合实现字段重排。比如把名,姓的格式调换过来echo Alice,Smith | sed -r s/(\w),(\w)/\2,\1/输出Smith,Alice。你看有了正则分组和反向引用sed能干的活就远不止简单替换了它能做字段级别的重排这在处理CSV、键值对这类结构化文本时非常实用。3. 增删改查一套组合拳很多人以为sed只能做替换其实它的删除、打印、追加、插入、修改功能同样能打。把这几招组合起来你会发现一个被低估的事实sed完全可以当作一款轻量级文本编辑器在命令行里直接干“增删改查”的活而且还支持条件判断和范围操作。3.1 删除d与打印p先学会做减法d命令是sed里最直白的命令删除匹配的行。删完以后这一行就不会进入输出流程了。最简单的用法是配合行号或正则sed 5d file.txt # 删除第5行 sed 1,10d file.txt # 删除前10行 sed /^$/d file.txt # 删除所有空行 sed /#/d config.conf # 删除所有含#的行删除空行这个操作我几乎每天都会用。从网页复制文本、从Excel导出数据经常会带一堆空行一行sed /^$/d就能清理干净。p命令是打印但它一般是配合-n参数用的。因为sed默认每行都输出单独用p会导致重复输出——匹配到的行会被输出两次。比如sed /error/p app.log包含error的行会在屏幕上出现两遍。所以正确姿势是sed -n /error/p app.log这样屏幕上只输出包含error的行。p命令的价值在于它可以作为整个sed流程里的“抽样检查工具”您在复杂处理时可以在命令后面临时加一个p看看中间状态确认没写错再继续。还有个组合是d和p一起用可以实现“保留/排除”的效果。比如我想保留包含keyword1或者keyword2的行其他全删掉sed -n /keyword1/p; /keyword2/p file.txt注意我在同一对引号里用分号间隔了多条命令这是sed支持的基本语法。3.2 追加a、插入i、修改c给文本做“手术”这三个命令的语法和其他sed命令不太一样它们不是直接接正则而是地址命令文本。i命令在当前行之前插入文本a命令在当前行之后追加文本c命令则是把匹配到的整行替换成新文本。看示例sed 2i This is inserted before line 2 file.txt sed 2a This is appended after line 2 file.txt sed 2c This line replaced line 2 file.txt这三个命令配合行号或正则能实现非常精细的文本编辑。比如我想在配置文件里的[server]段后面自动追加一行配置sed /\[server\]/a host 192.168.1.100 config.ini注意这里我没用-r参数所以方括号[需要转义。加上-r之后就不用转了。还有一个细节在多行文本里插入多行内容可以用\n分隔或者直接在这里包换行符。我在脚本里经常一次性插入多行比如在文件头部注入注释块sed 1i # \n# Generated by script\n# file.txtc命令还有一个用处按正则整块替换。比如把document里所有的title行都替换成新的sed -r s/title.*\/title/titleNew Title\/title/g index.html这个用s命令能做但如果要替换的行结构比较复杂直接c更省心sed -r /title/c titleNew Title/title index.html3.3 进阶辅助y转换和q退出y命令做的是字符一一对应的转换可以理解为简化版的tr命令。它按位置把第一个集合里的字符替换成第二个集合里对应位置的字符。比如把所有小写a变成大写A所有小写b变成大写Bsed y/ab/AB/ file.txt注意y命令处理的是字符级转换不是字符串匹配所以y/abc/xyz/会把所有a换成x、b换成y、c换成z而不是把abc这三个字母连起来换成xyz。这个特性适合做大小写统一、码值转换、加密暴力破解里的字符映射实际应用不多但关键时刻能救场。q命令是“满足条件就退出”。它会让sed在处理到匹配行时立即停止。这在处理大文件时特别有价值。比如我只想看看文件中第一次出现“ERROR”的位置后面不用继续扫了sed -n /ERROR/q app.log文件如果几百兆字节这个q能让sed提前退出省掉大量无谓的I/O。另一个常见用法是只打印文件头部若干行之后退出不过这个一般用head就够了q更适合需要结合sed复杂条件做早退的场景。4. 实战案例从改配置到清洗日志前两节讲了不少命令和参数光看肯定记不住。这一节我挑了几个真实遇到的场景从需求描述到命令设计完整走一遍。建议你手边开个终端跟着敲一遍效果好过单独念书。4.1 场景一批量修改配置文件有一回我在调一批测试环境的Nginx配置需要把所有监听端口从8080改成8081同时把日志路径里的/data/old_logs/统一改成/data/new_logs/。直接上手改几十个文件肯定不现实sed就派上用场了。先别急着覆盖原文件我先跑一遍不带-i的命令输出到屏幕确认结果sed -e s/8080/8081/g -e s|/data/old_logs/|/data/new_logs/|g /etc/nginx/conf.d/*.conf注意第二个替换我用的是|作为分隔符因为路径里全是/。确认输出无误后再处理一批文件并加备份sed -i.bak -e s/8080/8081/g -e s|/data/old_logs/|/data/new_logs/|g /etc/nginx/conf.d/*.conf执行完每个.conf文件旁边都会生成一个.bak备份后患全无。这个例子看起来简单但它是sed处理文件批量的标准模板区分“试运行”和“正式执行”用-e串联多个替换根据内容选择分隔符涉及重要文件时保留.bak。这套操作思路几乎可以覆盖一半以上的配置文件批量修改需求。4.2 场景二日志分析与文本清洗日志文件往往是sed最能发挥价值的战场因为日志动辄几百MB用编辑器打开都费劲但sed处理起来毫无压力。有一次排查线上接口响应慢的问题需要从access.log里把所有请求耗时超过3秒的请求行提取出来并且顺手把时间戳格式从2025-01-17T08:30:00简化成2025-01-17。我当时是这么写的sed -n /cost[0-9]\{4,\}/p; s/^\([0-9-]*\)T[0-9:]*/\1/p access.log简单解释一下第一个p命令打印所有包含cost且后面跟至少4位数字的行也就是耗时超过一定阈值的请求第二个替换命令把每行开头的ISO时间戳截断到日期部分。两个命令用分号连在一起加-n避免打印其他行。这个小需求如果没有sed得写Python脚本或者用awk而sed一行就实现了。还有一个很实用的文本清洗场景去掉文件中所有行首和行尾的空白并压缩连续多个空行。这个是我处理从Word粘贴出来的配置时几乎必用的sed -e s/^[ \t]*// -e s/[ \t]*$// -e /^$/d dirty.txt第一段去掉行首空白第二段去掉行尾空白第三段删空行。有时候我还需要把Tab统一成4个空格可以再加一段sed -e s/\t/ /g dirty.txt这种清洗组合我建议存成一个.sed脚本文档每次处理脏文本的时候直接sed -f clean.sed调用效率提升明显。4.3 场景三在shell脚本里调用sed的注意事项sed单独在命令行玩是一回事真正让它发挥威力的是嵌进shell脚本里做文本处理。这里有几个典型的坑我踩过不止一次。第一个坑在双引号里做变量替换。例如你要把配置文件里的PORT80改成变量new_port指定的值新手可能会这么写sed s/PORT80/PORT$new_port/ config.txt这样写本身没错前提是new_port里面不包含斜杠、空格、这类有特殊含义的字符。如果变量是用户输入或者从别处取来的分分钟出问题。更稳妥的做法是用|做分隔符并且把变量放进去的时候小心转义sed s|PORT80|PORT$new_port| config.txt如果变量里可能含有|或者这类特殊字符那就要用别的办法比如将变量中的特殊字符先转义。我自己一般会在脚本里写一个小的转义函数专门处理这类情况。另一个常见用法是在for循环里配合sed处理多个文件。比如批量处理同目录下所有.log文件将里面的INFO替换成DEBUG状态for f in *.log; do sed -i.bak s/INFO/DEBUG/g $f done这里强调一点文件名变量$f一定要加双引号不然文件名里有空格就炸了。类似的在shell里写sed正则里的$符号也常因为在双引号里被shell解析而产生歧义比如你想匹配行尾s/foo$//如果整个sed表达式用了双引号$就会被shell当变量符号处理。解决办法是正则部分用单引号或者对$做转义sed s/foo$// file.txt # 单引号安全 sed s/foo\$// file.txt # 双引号需要转义$还有一个典型的坑是在for循环中循环读取文件内容。很多人想用shell的for i in $(cat file)来逐行处理文本但这样遇到空格和通配符就会出各种问题。如果你已经是在用sed了就应该学会用管道和sed配合完成“逐行处理”的需求而不是依赖for循环打散文件内容。比如cat file.txt | while read line; do echo $line | sed s/foo/bar/ done这样每一行都能被sed单独加工而且不会因为空格而丢内容。等你更熟了会发现这种需求往往不需要while循环直接一条sed命令就能完成循环只是为了让流程更灵活可控。5. sed实战中的常见坑与排查方法sed用得越深踩过的坑就越多。这里的坑不是语法错误那么简单而是那种“看起来没问题跑出来结果却不是想要的”的隐蔽问题。我把自己的血泪教训整理了一些应该能帮你少走不少弯路。5.1 我踩过的高频坑先说跨平台差异。Linux上的sed是GNU sedmacOS自带的是BSD sed两者在细节上有一堆不兼容。最典型的就是-i参数GNU sed允许-i和-i.bak两种写法而BSD sed要求-i后面必须带后缀直接写sed -i s/foo/bar/ file在macOS上会直接报错。另外扩展正则参数也不一样GNU用-rBSD用-E。如果你写的脚本要在两个平台上跑建议在脚本里做检测或者统一用-EGNU sed新版本也支持-E算是两边慢慢统一了。第二个坑是-i的覆盖范围。sed -i s/foo/bar/ file grep bar file看起来是先替换再搜索但如果你在管道里用比如cat file | sed -i s/foo/bar/这会有问题因为-i是针对文件参数的。像这种想对一个流传过来的内容做替换再输出需要把-i去掉直接用普通的sed管道。我见过不少人在管道里加-i然后发现没生效就是因为对-i的“原地修改文件”语义理解不透彻。第三个坑是“最后一行没有换行符”。有些文本文件最后一行末尾没有\nsed在处理的时候会原样输出但如果你用-i写回去可能会出现文件末尾缺失换行的问题下一次脚本读入时某些工具会表现异常。解决办法是在处理前先给文件补换行符或者用awk等其他工具兜底。这个坑比较隐蔽我是因为在CI流水线里发现构建产物被改坏了一次才追查到底的。第四个坑是CRLF换行符。Windows下编辑过的文件行尾是\r\nLinux下sed匹配$行尾时\r会残留在文本里导致替换不干净或者条件匹配不上。处理这种文件最好先做一次换行符转换sed -i s/\r$// windows_file.txt这条命令就是把行尾的\r去掉转成Unix换行格式。我处理跨平台文件的第一道工序永远是这一行。5.2 排查技巧sed -n l和分步验证很多时候你写了条sed命令发现输出根本不是想的那样第一反应是搜半天语法对不对但我觉得更高效的排查思路是分步验证。第一步建议先用sed -n l看看文件的真实状态。l命令会打印当前行的“可见化版本”把\t显示成\t把行尾显示为$这样你能立刻发现不可见字符。比如你以为是空格的地方实际可能是Tab或者CRLF。我排查脏数据时第一步永远是sed -n l file.txt | head。第二步逐步缩小范围。如果你组合了多个sed命令结果不对可以用-e逐个拆开跑或者把后面的命令先注释掉看第一个命令输出的中间结果。比如你想做“替换后打印”sed -n s/foo/bar/p file.txt如果这个没输出可能不是p的锅而是替换本来就没匹配上。那就先用sed -n /foo/p file.txt确认foo真的存在再看是否大写、是否带其他不可见字符。第三步用管道配合diff对比验证。我在修改配置文件前习惯把原始文件和sed处理后的输出存下来然后用diff对比sed -e s/foo/bar/ file.txt file_new.txt diff file.txt file_new.txt这样能一目了然地看到每一处改动是否符合预期比在屏幕上滚动找差异靠谱得多。如果在自动化脚本里做重要文件修改我会在脚本里显式调用diff如果差异行数异常就中止避免误改。5.3 什么时候别用sed与awk/grep的分工学sed的同时必须有个清醒的认识它擅长的是“流式”文本转换凡是需要对字段做复杂计算、跨行聚合、多维数组操作的活交给awk更合适凡是纯过滤不需要加工grep通常更简洁。三者是配合关系不是替代关系。比如我想统计一个文件里各IP的出现次数这种累加计算用sed很别扭但awk就顺理成章awk {count[$1]} END {for (ip in count) print ip, count[ip]} access.log再比如需要按字段排序、求和、找最大值都是awk的主场。而sed最适合的领域是“按行做规则化替换、删除、插入”它没有变量、没有循环严格说g命令等可以实现循环但可读性很差本质是用模式匹配驱动的一次性变换。还有一个常见误区是用sed处理JSON、XML这类拥有嵌套结构的文本。sed按行处理不感知结构一旦格式拆成多行或者结构变化就很容易误伤。处理结构化数据正确姿势是用jqJSON或者专门的XML工具而不是硬拿sed去凑。我的经验法则是如果这个文本处理任务需要在多行之间维持状态或者递归匹配就该换工具了。sed真正擅长的是那些“一根筋”的转换一行进一行出干净利落。说回我自己的使用节奏。我通常在管道处理流程中这么分工先用grep快速过滤出需要的行再用sed做格式整理和替换最后用awk做统计或者取值。三者配合熟络以后大部分日志分析、配置批量修改、数据清洗的活都能在终端里一波流搞定不用开编辑器不用写Python脚本效率完全是另一个级别。最后再分享一个我的个人习惯。每次在脚本里用sed做那种会影响原文件的修改时我都会在修改前先自动生成带时间戳的备份并且把sed执行的日志打印出来。因为生产环境上出了问题最痛苦的不是命令写错而是找不到之前的状态。有了备份和日志出问题能随时回滚。留意这个习惯之后我因为改文件改崩而圆谎的次数明显少了很多。