搞 Linux 的人早晚会跟 sed 打上交道。这玩意儿被叫做流编辑器最初我接触它的时候也觉得别扭——命令格式跟普通命令不太一样写错了不报错结果就是悄悄把活儿干错。但真正用顺了以后你会发现它处理文本的效率远超图形工具甚至比很多脚本语言都利落。这篇文章我就把自己这些年总结的 sed 常用命令、容易踩的坑、还有能直接抄的实战场景一次讲清楚希望能帮你少走点弯路。1. 先搞懂 sed 的工作方式才能理解所有命令1.1 流式编辑到底是个什么东西sed 全称是 Stream Editor核心思路就是“逐行处理”。它不像 vim 那样打开整个文件让你编辑而是像流水线一样把输入的数据一行一行送进来处理完一行再送下一行全程只保留当前正在处理的那一行所以内存占用非常小哪怕处理几个 GB 的日志文件也不会卡死。这个设计思路跟管道配合起来特别舒服。比如cat access.log | sed s/旧/新/g左边产生的输出直接变成 sed 的输入处理完扔给下一个命令整个过程数据像水流一样流过各个处理环节不用生成中间临时文件。我刚开始总把它跟 awk 搞混后来记住一句话就分清了sed 重在“改”awk 重在“取”。当然 sed 也能取内容只是它的看家本领是批量修改。1.2 模式空间看不见的临时缓存区sed 处理每一行时都会把这一行内容放进一个叫“模式空间”的内部缓冲区所有操作都在这个空间里完成处理完了再输出。这个机制听起来简单却解释了很多诡异现象。比如你写sed s/foo/bar/ file默认情况下修改后的内容会直接输出到屏幕但文件本身完全不变。这就是因为 sed 只是把模式空间里的内容改了然后打印出来并没有写回原文件。很多新手在这里栽跟头以为命令执行没成功其实是输出方向理解错了。理解了模式空间后面讲多行处理、保持空间的时候你就不会懵。顺带提一句sed 还有另一个缓冲区叫“保持空间”默认是个空行用途是暂时存放数据方便跨行拼接。这两个空间配合起来可以实现相当复杂的文本重组第四节我会详细展开。1.3 sed 的执行循环读一行改一行出结果sed 对每一行的处理流程本质上是个固定循环从输入流文件或管道读取一行去掉末尾的换行符放入模式空间。依次执行脚本里指定的每一条命令。把模式空间的内容加上换行符输出到标准输出除非用了 -n 参数。清空模式空间回到第 1 步处理下一行。这个循环是理解 sed 一切行为的地基。举个例子你就明白了为什么sed s/foo//只替换每行第一个匹配而不是全部因为每一行进来都是只读一次模式空间里只有一行命令只执行一次。想要全部替换就必须加g标志global全局标志让替换操作在当前行内不断循环查找。还有个细节很多书籍不会提如果文件最后一行没有换行符sed 处理完这一行会照样输出内容但不会再额外加一个换行。这个行为在拼接文本、生成配置文件时偶尔会坑你一下后面常见问题里我会再说。2. 最常用的几个 sed 命令逐个拆开讲透2.1 替换命令 ssed 的招牌动作替换是 sed 使用频率最高的操作基本语法是sed s/被替换的内容/替换成的内容/标志这里的“标志”最常见的有三个g全局替换把这一行里所有匹配都换掉。i忽略大小写匹配时不分大小学。数字比如s/foo/bar/2只替换每行第二个匹配。我来写两个实际例子# 把文件里所有的 old 换成 new包括行内多个匹配 sed s/old/new/g file.txt # 忽略大小写把 user 换成 account sed s/user/account/gi file.txt这里有个非常重要的技巧分隔符可以换。如果被替换的内容本身包含斜杠比如路径/usr/local/bin你写s//usr/local/bin//new/就会很混乱。解决办法是换成别的分隔符比如#或者|echo /usr/local/bin | sed s#/usr/local/bin#/opt/bin# echo a|b|c | sed s|b|BB|g这个技巧我在处理配置文件时几乎必用因为路径里到处都是斜杠。记住一句话s 命令后面的第一个字符就是分隔符用哪个都行选一个跟内容不冲突的就好。2.2 替换命令的高级玩法捕获组与反向引用sed 的替换不只是简单的字符串对换它还支持正则表达式里的捕获组用\(和\)包裹然后在替换内容里用\1、\2去引用。这是 sed 最强大的能力之一。举个例子把2024-01-15这种日期格式改成15/01/2024echo 2024-01-15 | sed s/\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\)/\3\/\2\/\1/输出结果就是15/01/2024。这里三个捕获组分别抓住了年、月、日然后在替换位置重新排列。注意[0-9]\{4\}里的{4\}是“重复前一个字符 4 次”的意思大括号在基础正则表达式里需要加反斜杠转义。新手容易犯的错是记不住转义规则。基础正则BRE里捕获组用\(和\)花括号用\{和\}加号用\问号用\?。如果你觉得这些反斜杠太烦人可以在 sed 后面加-E参数切换到扩展正则ERE这样括号就不需要反斜杠了echo 2024-01-15 | sed -E s/([0-9]{4})-([0-9]{2})-([0-9]{2})/\3\/\2\/\1/同样的逻辑写法清爽不少。我的习惯是命令简单就用默认的基础正则涉及捕获组、量词之类的复杂规则一律加-E减少转义出错概率。2.3 删除命令 d 和打印命令 p组合起来威力巨大删除命令 d 的语法是sed 地址d意思是“删除匹配到的每一行”。没有地址的话就是把所有行都删掉等于不输出任何东西。# 删除第 3 行 sed 3d file.txt # 删除第 3 到第 5 行 sed 3,5d file.txt # 删除包含 error 的所有行 sed /error/d file.txt打印命令 p 单独用的时候要注意因为 sed 默认会输出所有行所以sed p file.txt会把每一行打印两遍。它通常配合-n参数使用-n的作用是关闭默认输出只输出你明确要求的内容# 只打印第 3 行 sed -n 3p file.txt # 只打印第 3 到第 5 行 sed -n 3,5p file.txt # 只打印包含 error 的行 sed -n /error/p file.txt这招在日志大文件里抓取关键信息特别好用。比如一个 2GB 的日志文件你用sed -n /2024-01-15 10:00:/p app.log就能精确抽出一个时间窗口内的所有日志而且是流式处理不会一下吃光内存。d和p看起来是一对相反操作但配合地址范围可以实现“取区间”的效果。比如打印从 BEGIN 到 END 之间的内容sed -n /BEGIN/,/END/p file.txt这个区间匹配是 sed 的一个重要特性地址不一定是行号也可以是正则匹配。只要一行包含 BEGIN 就开始打印直到出现 END 才停止中间所有行都输出。2.4 行插入与追加a 命令和 i 命令aappend追加和 iinsert插入分别表示在某行后面和前面增加内容。基本写法# 在第 2 行后面追加一行 hello sed 2a hello file.txt # 在第 2 行前面插入一行 hello sed 2i hello file.txt注意写法2a后面跟空格再加内容没有逗号。如果是向匹配的行追加写法是# 在包含 user 的每一行后面追加新行 sed /user/a new_line_content file.txt # 在文件末尾追加注意 $ 表示最后一行 sed $a 这是最后追加的内容 file.txt # 在文件开头插入1 表示第一行 sed 1i 这是开头插入的内容 file.txt多行内容的插入稍微麻烦一点用反斜杠换行来续行sed 2a line1\ line2\ line3 file.txt我在生成配置文件时经常用这个特性。比如要给所有虚拟机配置文件末尾统一加上一段监控配置一条sed -i $a ...就能解决几十台机器的问题配合循环脚本效率极高。2.5 修改命令 c整行替换cchange更改命令比 s 命令更“霸道”它把整行直接替换成新内容不管这行本来写的是什么# 把第 5 行替换成新内容 sed 5c 这是新内容 file.txt # 把包含 error 的行全部替换成 --- 异常行已隐藏 --- sed /error/c --- 异常行已隐藏 --- file.txt这个命令处理日志脱敏特别好用。我在给客户做数据脱敏演示时经常用一条命令把所有包含身份证号、手机号的行整行替换成占位符比用 s 命令逐个字段替换干净利落得多。3. 常用参数和地址定址技巧3.1 必知的 sed 参数-n、-i、-e、-f有几个参数是你几乎天天碰到的-n安静模式上面说过关闭默认打印配合 p 命令用。还有个隐藏用途就是和 s 命令搭配只输出被修改的行sed -n s/foo/bar/p file.txt这个写法只打印发生了替换的那些行日常检查替换效果非常方便。-i原地修改直接修改文件内容而不是输出到屏幕。这是 sed 最危险的参数没有之一。建议任何正式操作前先不加 -i 跑一遍看输出确认无误后再加 -i。更稳妥的做法是加后缀生成备份文件# 直接修改文件不备份危险 sed -i s/old/new/g file.txt # 修改前生成 file.txt.bak 备份文件 sed -i.bak s/old/new/g file.txt我把备份后缀视作生产环境的标配。曾经有一次我批量替换线上配置文件少写了一个转义符如果没备份那批配置就全废了。有了 .bak一秒就能回滚。-e多点编辑可以在一条命令里执行多个 sed 操作按顺序执行sed -e s/foo/bar/ -e /^$/d file.txt等价于分号写法sed s/foo/bar/; /^$/d file.txt个人更推荐分号写法短一些。但是分号在引号里的处理偶尔会跟 shell 语法打架所以脚本里我习惯用 -e更清晰。-f脚本文件把 sed 命令写进一个文件然后sed -f script.sed file.txt执行。适合复用复杂规则集。比如我维护了一套日志清洗规则写进文件后每次处理日志只要一行命令调用改规则也只改一个文件。3.2 地址定址行号、正则、范围与步进sed 的命令前面都可以加“地址”来限定作用范围地址有几种形态行号地址直接写数字。5代表第 5 行$代表最后一行。5,$代表从第 5 行到结尾。# 从第 10 行开始处理到最后一行 sed 10,$s/foo/bar/g file.txt # 删除最后一行 sed $d file.txt正则地址用斜杠包起来的正则表达式。/^$/匹配空行/^#/匹配注释行/error/匹配包含 error 的行。可以组合# 删除所有空行和注释行-E 用于扩展正则^ 表示开头 sed -E /^$/d; /^#/d file.txt # 从第一个出现 BEGIN 的行开始到出现 END 的行结束替换 foo sed /BEGIN/,/END/s/foo/bar/g file.txt范围地址逗号连接两个地址形成区间。上面例子已经用过3,10p、/BEGIN/,/END/p都是范围形式。这里有个细节容易踩坑当范围起点的正则匹配不到时范围不生效当终点匹配不到时会一直处理到文件结尾。步进地址first~step形式1~2表示从第 1 行开始隔 2 行取一次也就是奇数行2~2取偶数行。这个在处理表格数据时挺有用# 只打印奇数行 sed -n 1~2p file.txt # 在每个偶数行前插入分隔线 sed 2~2i ---------------- file.txt3.3 多命令组合与执行顺序sed 的命令在引号里可以写多个用分号或者-e连接。执行顺序是从左到右前面命令的输出状态不会传给后面的命令但模式空间是同一个所以后面的命令能看到前面命令修改后的内容。举个例子sed s/foo/bar/; s/bar/baz/ file.txt它会先把 foo 替换成 bar然后又在同一个模式空间里把新生成的 bar 替换成 baz。所以原本 foo 变成 baz原本就是 bar 的内容也会变成 baz。如果你没意识到这层顺序关系写出的结果可能出乎意料。这就是我为什么强调先理解“模式空间里到底在干嘛”。你想象有一个工作台一行文字放在上面你拿橡皮擦改完一个词又在改完的那行文字上改另一个词——所有操作都是连续的、在同一行上发生的。4. 实战场景与综合案例4.1 批量修改配置文件的完整流程最典型的场景是改配置文件。比如某运行环境的 nginx 配置里所有域名要从old.example.com换成new.example.com而且需要先把.conf文件全部备份# 第一步找出现有配置 grep -rl old.example.com /etc/nginx/conf.d/ # 第二步批量替换并生成备份 find /etc/nginx/conf.d/ -name *.conf -exec sed -i.bak s/old\.example\.com/new.example.com/g {} \;注意这里正则里old.example.com的.我写成了\.因为.在正则里表示任意字符不转义的话可能误伤oldxexamplexcom这种并不存在的域名。这个坑非常隐蔽尤其是你替换的字符串里带点号、加号、括号这些正则元字符时一定要先问自己这些字符有没有被转义再举一个稍微复杂的场景。假设有份配置文件里面有多行参数想统一缩进对齐# 在所有以 key 开头的行前面加两个空格 sed -i s/^key/ key/ app.conf如果配置里有些行前面已有空格得先去掉再重新加sed -i -E s/^[[:space:]]*key/ key/ app.conf[[:space:]]*匹配任意数量的空白字符包括空格和制表符这是 POSIX 字符类在 sed 里通用。用*表示零次或多次所以不管原来有没有缩进都能命中。4.2 日志提取与数据清洗的真实操作日志处理大概是 sed 最“物尽其用”的场景。我经常要在一堆日志里提取某个时间段的内容# 提取 10:00:00 到 10:30:00 之间的日志 sed -n /2024-01-15 10:00:00/,/2024-01-15 10:30:00/p app.log window.log这里正则里的空格需要用引号包住否则 shell 会拆词。批量清洗时我习惯写一个 sed 脚本文件把规则集中管理。假设有一个log_clean.sed# 去掉 ANSI 颜色转义序列 s/\x1B\[[0-9;]*[a-zA-Z]//g # 去掉行首时间戳只保留正文 s/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2},[0-9]{3} // # 把 IP 地址脱敏只保留前两段 s/([0-9]{1,3}\.)([0-9]{1,3}\.)[0-9]{1,3}\.[0-9]{1,3}/\1\2xxx.xxx/g执行的时候sed -f log_clean.sed app.log cleaned.log这套规则可以反复用在每天的日志上而且 sed 脚本文件是纯文本放进代码仓库做版本管理完全没问题。我强烈建议你把常用的清洗规则沉淀成 .sed 文件否则每次临时敲命令规则不统一出来的结果前后对不上排查问题会非常痛苦。脱敏这块我再补充一个技巧脱敏日志里的手机号可以用捕获组保留前三后四sed -E s/(1[0-9]{2})[0-9]{4}([0-9]{4})/\1****\2/g data.txt处理完成前我会先用sed -n s/.../.../p验证替换命中的行数确认没有误伤数据再正式写入。4.3 多行模式 N 与保持空间的妙用sed 默认每次处理一行但有些需求必须跨行看。比如要合并两行或者按多行模式匹配。N 命令的作用是把下一行追加到当前模式空间中间用换行符隔开然后继续执行后面的命令。我用一个拼接姓名的例子说明假设数据文件每两行是一个人的姓和名张 三 李 四想合并成张三、李四sed -E N; s/\n// names.txt流程是这样的N 把下一行也读进来模式空间变成张\n三然后s/\n//把换行符删掉变成张三。处理完输出接着再读新的两行继续同样的操作。保持空间 H 和交换 G 则能做更复杂的重组。比如想把文件倒序输出# 第一版用 cat 实现 cat -r file.txt # 有的系统支持 -r有的不支持 # 更通用的做法是 tac tac file.txt # 或者用 sed 硬来 sed 1!G; h; $!d file.txt最后这条 sed 命令值得拆解一下它展示了模式空间和保持空间的配合1!G如果不是第 1 行就把保持空间内容追加到模式空间后面。h把当前模式空间复制到保持空间。$!d如果不是最后一行删除模式空间不输出。整体效果第一行先存进保持空间第二行来的时候把保持空间里已有的内容追加到自己后面于是模式空间变成“第二行第一行”再存进保持空间。第三行来的时候把“第二行第一行”追加到第三行后面……以此类推到最后一行时模式空间里已经攒了整份文件的逆序内容最后一行会正常输出就实现了倒序。这个操作理解起来有点绕但一旦想明白对 sed 的理解会质变。还有一层要提醒$!d里的 d 会清空模式空间并进入下一轮循环所以不是最后一行时内容不会输出。最后一个$对应的行不执行 d就会带着全部累积内容输出。这是 sed 里“攒内容到最后输出”的经典套路写脚本时思路完全可以借鉴。4.4 处理 CSV 与表格文本的实战sed 处理 CSV 也是常用场景。比如 CSV 里某些字段带引号想简单地按逗号切列并重组# 把所有行的第二个字段和第三个字段交换 sed -E s/^([^,]*),([^,]*),([^,]*),/\1,\3,\2,/ data.csv这个规则用到了[^,]*表示“尽可能多但不是逗号的字符”然后捕获成组再重新排列顺序。CSV 里如果字段本身包含逗号比如引号包裹的文本就不能这么简单处理了那种情况建议老老实实用 Python 的 csv 模块不要硬用 sed 强解。工具选型也是经验的一部分sed 不是万能的简单、结构化、规则固定的文本是它的主场一旦数据内部结构复杂换更专业的工具才是明智选择。5. 常见问题与避坑指南5.1 shell 引号与转义最大的坑在这里sed 命令经常写在 shell 脚本里引号的坑五花八门。核心原则单引号内是纯文本sed 的双引号内更容易和 shell 变量混用但也会被 shell 解释一部分字符。使用单引号时如果内容里还要用单引号比如匹配英文缩写得这样处理# 错误单引号内不能直接再放单引号 sed s/s/ is/ file.txt # 正确用双引号把单引号包起来 sed s/s/ is/ file.txt但双引号里$、\、反引号都会被 shell 优先解释。如果我的匹配内容里有$符号比如替换所有$HOME文本# 危险$HOME 会被 shell 展开成路径 sed s/\$HOME/\/data\/home/g file.txt # 推荐先用单引号写变量用双引号拼接 sed s/$HOME/\data\/home/g file.txt这里的转义确实烦人。我的兜底策略是尽量用单引号包裹 sed 脚本如果确实需要嵌入 shell 变量就把 sed 脚本拆成双引号和单引号拼接的片段或者干脆用环境变量。还有一个好习惯先在命令行里用一个极小的测试文件验证输出确认无误再正式跑。每次因为转义出问题都是因为省了这一步。5.2 -i 原地修改的坑与备份习惯-i在不同系统上行为有差异。Linux 的 GNU sed 支持sed -i s/foo/bar/g file不需要备份后缀也能跑但 macOS 自带的 BSD sed 要求必须写-i 空字符串表示不备份否则会报错# Linux 写法 sed -i s/foo/bar/g file.txt # macOS 写法 sed -i s/foo/bar/g file.txt这种差异在跨平台脚本里特别容易出问题。我的脚本里一般会先检测系统或者统一用带备份后缀的写法既能兼容两边又留了回滚余地sed -i.bak s/foo/bar/g file.txt另外-i和模式空间没有关系它只是把“输出到屏幕”改成“写回原文件”。所以你在-i之前跑一遍不带-i的版本看输出是完全模拟真实操作的这一步一定不能省。5.3 空行、特殊字符与编码问题的处理处理空行和文件格式时有几个细节要注意。删除空行最简单的是sed /^$/d file.txt但这里的$配合^表示“行首紧接着行尾”也就是空行。如果文件里有些行只有空格或制表符^$匹配不到要用sed -E /^[[:space:]]*$/d file.txt[[:space:]]匹配空格、制表符等所有空白字符*表示零个或多个。Windows 和 Linux 文件混用时行尾的\r会让匹配出问题。比如一个 Windows 下编辑的文件每行结尾是\r\n$匹配到的位置其实在\r前面很容易踩坑。处理办法是先把\r干掉tr -d \r win.txt unix.txt或者直接用 sedsed -i s/\r$// win.txt中文编码方面sed 本身不关心编码它只按字节流处理。如果你用 UTF-8 匹配中文字符要注意终端和文件的编码一致否则sed s/你好/您好/g可能匹配不上。通常我处理之前会先file命令看一下文件编码再决定要不要转码。最后补充一个 GUN sed 独有的扩展\U和\L可以在替换内容里转换大小写。比如把整行转大写sed -E s/(.*)/\U\1/ file.txt这个在需要生成统一格式的报告时比较有用。但注意 BSD sed 不认\U跨平台脚本慎用。6. 我对 sed 的使用体会用了这么多年我的体会是sed 的学习曲线不是陡而是“反直觉”。它跟普通命令的思维模式不一样你得先接受“一行一行流式处理”的底层逻辑再学命令就不觉得乱了。我见过不少同事用 sed 只记几个固定模板一旦需求稍有变化就卡壳其实只要把模式空间、地址定址、捕获组这三样东西吃透绝大多数场景都能当场推出来。还有一点建议sed 是管道文化的一环别孤立地学。它跟 grep、awk、sort、uniq 配合起来才是真正的高效流水线。比如经典的“统计日志里每个 IP 出现次数”sed -E s/^([0-9]\.[0-9]\.[0-9]\.[0-9]).*/\1/ app.log | sort | uniq -c | sort -rnsed 先抽出 IP 字段sort 排序uniq -c 计数sort -rn 按次数降序排列一行完成整个统计链路。这种组合拳才是 sed 真正的威力所在。最后分享一个小习惯我在写复杂的 sed 之前会先构造一个只有几行的测试文件把命令替换到文件里跑一遍确认输出没问题再上真实数据。包括用-i批量改文件前一定先找一到两个样本文件做演练。这个习惯帮我挡住了不知道多少潜在的生产事故。你如果刚好也在学 sed不妨也把这个流程固定下来——测试先行批量操作留备份正则里该转义的一个都别省。sed 这个东西一旦上手你会越来越离不开它。遇到任何批量的文本修改需求你的第一反应就不再是打开编辑器手动改而是想这条 sed 命令怎么写。到那个时候你就真正入门了。