9929 字
50 分钟
Linux基础(11):sed命令从入门到精通完全指南

Linux运维必备神器:sed命令从入门到精通完全指南#

作为一名Linux运维工程师,每天都要和大量的文本文件打交道——配置文件、日志文件、脚本文件、数据文件等等。在这些工作中,文本处理能力直接决定了你的工作效率。而在Linux众多的文本处理工具中,sed(Stream Editor,流编辑器)无疑是最强大、最常用的工具之一。

很多运维人员对sed的了解只停留在简单的替换操作上,但实际上,sed的功能远不止于此。它可以实现复杂的文本转换、批量修改、数据提取、日志分析等任务,是每个运维工程师必须精通的”瑞士军刀”。

这篇博客将带你从sed的基础概念开始,一步步深入到高级技巧,最后通过大量真实的运维实战案例,让你真正掌握sed命令,成为文本处理高手。

一、sed基础:认识流编辑器#

1.1 什么是sed#

sed是一个非交互式的流编辑器。它不像vim那样需要你打开文件进行交互式编辑,而是从标准输入或文件中读取数据,按照指定的编辑命令对数据进行处理,然后将结果输出到标准输出。

sed核心特点
  • 流处理:逐行读取数据,处理一行输出一行,内存占用极低,可处理超大文件。
  • 非交互式:可以在脚本中自动化执行,无需人工干预,适合集成到自动化流程。
  • 强大的正则表达式支持:可以处理复杂的文本模式匹配和转换。
  • 轻量级:几乎所有Linux发行版都默认安装,无需额外配置,可即插即用。

1.2 sed的工作原理#

理解sed的工作原理是掌握sed的关键。sed的工作流程可以概括为以下几个步骤:

  1. 读取:从输入流(文件或标准输入)中读取一行数据,存入模式空间(Pattern Space)。
  2. 执行:按照sed命令的顺序,对模式空间中的内容进行处理。
  3. 输出:将处理后的模式空间内容输出到标准输出(除非指定了-n选项)。
  4. 重复:清空模式空间,读取下一行,重复上述过程,直到文件结束。
模式空间与保持空间的关键区别

模式空间是sed主要的工作区,用于存放当前正在处理的行;保持空间是一个辅助存储区,初始为空,用于保存临时数据。两者配合可以实现复杂的多行处理操作,这是sed高级功能的核心。

1.3 sed的基本语法#

sed的基本语法格式如下:

sed_basic_syntax.sh
sed [选项] '地址 命令' 输入文件
# 实际使用示例:将文件中的"old"替换为"new",并输出结果
sed 's/old/new/g' test.txt
# 更复杂的例子:指定地址范围处理
sed '1,10 s/foo/bar/g' test.txt

各组成部分详解:

  • 选项:控制sed的整体行为(如-n、-i、-r等)
  • 地址:指定要处理的行范围(行号、正则表达式或地址范围)
  • 命令:指定要执行的编辑操作(s替换、d删除、p打印等)
  • 输入文件:要处理的文件,省略时从标准输入读取

二、sed常用选项详解#

sed提供了多个选项来控制其行为,以下是运维工作中最常用的几个选项:

2.1 -n:静默模式(关闭默认输出)#

默认情况下,sed会将模式空间的内容输出到标准输出。使用-n选项可以关闭默认输出,只输出被命令明确指定的行。这在你只关心匹配结果时非常有用。

sed_n_demo.sh
# 【对比示例】显示默认行为的差异
# 不使用 -n:输出所有行,但第5行会被打印两次(默认输出+p命令输出)
sed '5p' test.txt
# 使用 -n:只显示文件的第5行,且只输出一次
sed -n '5p' test.txt
# 只显示包含"error"的行,忽略其他所有行
sed -n '/error/p' test.log
# 显示第2行到第5行之间包含"warning"的所有行
sed -n '2,5{/warning/p}' test.log
什么时候使用-n

当你只关心特定行的内容(如日志查询、配置提取)时,使用-n可以避免冗余输出,提高脚本效率。

2.2 -i:原地修改文件(直接修改源文件)#

默认情况下,sed不会修改原文件,只是将处理结果输出到标准输出。使用-i选项可以直接修改原文件。这是一个强大但危险的功能,必须谨慎使用。

原地修改的高危风险

使用-i选项时一定要小心,因为它会直接修改原文件,且没有撤销功能。如果发生误操作可能会导致配置丢失!强烈建议使用:spoiler[-i.bak]选项自动创建备份文件,这样可以在出错时恢复。

sed_i_demo.sh
# 【最危险的方式】直接修改原文件,无备份
sed -i 's/old/new/g' test.txt
# 【推荐的安全方式】修改原文件,并自动创建备份文件 test.txt.bak
# 如果sed命令执行出错,原文件保存在.bak中,可以恢复
sed -i.bak 's/old/new/g' test.txt
# 【批量修改配置文件】修改目录中所有.conf文件,保存备份
for file in *.conf; do
sed -i.backup "s/localhost/0.0.0.0/g" "$file"
done
# 【修改后验证】执行完-i操作后,应该立即验证改动是否正确
# 比较修改前后的差异
diff test.txt.bak test.txt

2.3 -e & -f:多命令执行与脚本加载#

如果需要执行多个编辑命令,可以使用-e选项来拼接多个命令,或者使用-f选项读取独立的脚本文件。对于复杂的sed脚本,建议使用-f选项,这样可以提高可维护性。

sed_multi_commands.sh
# 【方式一】使用 -e 选项:多个编辑命令依次执行
# 先删除空行,再将"old"替换为"new"
sed -e '/^$/d' -e 's/old/new/g' -e 's/foo/bar/g' test.txt
# 【方式二】使用 -f 选项:从脚本文件读取所有命令
cat > sed_commands.sed << 'EOF'
# 删除空行
/^$/d
# 替换old为new(全局)
s/old/new/g
# 替换foo为bar(全局,忽略大小写)
s/foo/bar/gi
# 删除以#开头的注释行
/^#/d
EOF
# 执行脚本文件中定义的所有命令
sed -f sed_commands.sed test.txt
# 【实用技巧】结合-i选项,原地修改多个文件
sed -i.bak -f sed_commands.sed file1.txt file2.txt file3.txt

2.4 -r 或 -E:使用扩展正则表达式#

默认情况下,sed使用基础正则表达式。使用-r(GNU sed)或-E(BSD sed)选项可以使用扩展正则表达式,减少转义的麻烦。

sed_regex_demo.sh
# 【基础正则 vs 扩展正则】对比
# 基础正则表达式:需要转义+号
sed -n '/[0-9]\+/p' test.txt
# 扩展正则表达式:无需转义+号,更直观
sed -rn '/[0-9]+/p' test.txt
# 【复杂匹配示例】提取邮箱地址
# 基础:需要大量转义,容易出错
sed -n 's/.*\([a-zA-Z0-9._-]*@[a-zA-Z0-9.-]*\).*/\1/p' text.txt
# 扩展:更清晰直观
sed -rn 's/.*([a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+).*/\1/p' text.txt
# 【提取IP地址】
sed -rn 's/.*([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}).*/\1/p' test.log

三、sed寻址:精准选择要处理的行#

我们先从根上建立认知:sed 的寻址(Address),就是指定「后面的命令要作用在哪些行上」。

之前写的 /^$/d 里,/^$/ 就是寻址部分,d 是动作部分。不写寻址时,默认对所有行执行动作。

sed 处理每一行的固定流程:

  1. 读取一行到「模式空间」
  2. 用寻址规则判断:这一行要不要处理?
  3. 匹配 → 执行后面的编辑动作
  4. 输出结果,读取下一行,循环往复

下面我们把所有寻址规则按类型讲透,每个都配语法+例子+说明。

3.1 单地址寻址:只匹配「某一行」#

只写一个地址,代表只对匹配到的那一行执行动作。分两种写法:

1. 行号寻址(数字地址)#

用数字精确指定第几行。

  • 语法:n(n 为正整数)
  • 特殊符号:$ 代表最后一行

示例:

sed_single_address.sh
sed '1d' test.txt # 删除第 1 行
sed '$d' test.txt # 删除最后一行
sed -n '5p' test.txt # 只打印第 5 行(-n 关闭默认输出)

2. 正则寻址(模式地址)#

用正则表达式匹配,行内容满足正则就命中。这就是你最开始接触的 /.../ 写法。

  • 语法:/正则表达式/

示例:

sed_regex_address.sh
sed '/^$/d' test.txt # 匹配空行 → 删除(你最开始的命令)
sed '/^#/d' test.txt # 匹配 # 开头的注释行 → 删除
sed '/error/p' test.txt # 匹配包含 error 的行 → 打印
正则寻址匹配规则

关键注意:正则寻址默认是「包含匹配」——只要行里出现了对应字符串就算命中。 只有加了 ^ 和 $ 锚定,才是「整行匹配」,比如 /^abc$/ 才表示整行就只有 abc。

3.2 范围寻址:匹配「从 A 到 B 的连续行」#

用逗号 , 分隔两个地址,代表:从匹配地址1的行开始生效,到匹配地址2的行结束生效,中间所有行(含起止行)都会执行动作。

这是最容易绕晕的部分,也是 sed 寻址的核心用法。

1. 三种组合形式#

组合类型语法示例作用
行号 + 行号2,5d删除第 2 到第 5 行(包含第 2、5 行)
正则 + 正则/start/,/end/d从含 start 的行开始删,到含 end 的行停止
行号 + 正则混合3,/^$/d从第 3 行开始删,一直删到第一个空行

2. 正则范围寻址的执行逻辑(重点)#

以 sed '/start/,/end/d' file 为例:

  • 逐行扫描,第一次遇到 start → 开启删除动作
  • 继续往后,遇到 end → 执行完这行的删除后,关闭动作
  • 如果后面又出现 start,会再次开启,循环往复

举个实际文本例子:

source_text.txt
line1
start
line3
line4
end
line6
start
line8
end
line10

执行后,两组 start~end 都会被删掉,最终剩下:

output_text.txt
line1
line6
line10
起止全包

记住:范围寻址是「起止全包」,开头行和结尾行都会被执行动作。

3.3 特殊寻址语法#

1. 取反符 !:不匹配的行才执行#

语法:地址!动作 意思完全反过来:不满足该地址的行,才执行后面的命令。

示例:

sed_reverse_address.sh
sed '/^$/!d' test.txt # 不是空行的就删掉 → 只保留空行(和 /^$/d 正好相反)
sed '1!d' test.txt # 不是第 1 行就删掉 → 只保留第 1 行
sed '/^#/!p' -n test.txt # 只打印不是 # 开头的行

2. 步进寻址 ~:按步长跳行#

GNU sed(Linux 默认版本)特有,按固定间隔选中行。

  • 语法:起始行~步长

示例:

sed_step_address.sh
sed '1~2d' test.txt # 从第1行开始,每隔2行删一行 → 删除所有奇数行
sed '0~2d' test.txt # 从第0行开始,每隔2行删一行 → 删除所有偶数行
sed '3~4p' -n test.txt # 打印第 3、7、11... 行

3. 空地址:作用于全部行#

完全不写地址,比如 sed 'd' file,代表匹配所有行,全部删除。

3.4 一个地址执行多条命令:{} 命令组#

sed 默认的语法规则是:一个寻址条件,只控制紧跟在它后面的 1 条命令。 如果后面的其他命令不加寻址,就会变成「无寻址」状态,也就是对所有行生效。

{} 的作用,就是把多条命令打包成一个整体,让它们全部共用前面同一个寻址条件。我们通过一个反面例子来看看不加 {} 会发生什么。

1. 易踩坑:不加 {} 会发生什么?#

假设有一个文件 fruit.txt 内容如下,我们的需求是:只在包含「hello」的行里,把 apple 换成 苹果,把 cherry 换成 樱桃。

fruit.txt
第一行 apple cherry
第二行 hello apple cherry
第三行 apple cherry

错误写法(不加 {}):

sed_wrong_group.sh
sed '/hello/s/apple/苹果/;s/cherry/樱桃/' fruit.txt

我们拆解这条命令:

  • /hello/ 是寻址条件。
  • 它只管住了紧跟的第一条命令:s/apple/苹果/。
  • 第二条命令 s/cherry/樱桃/ 前面没有寻址,所以是全局生效,所有行都会执行。

执行结果会变成这样,没有 hello 的行也被替换了 cherry:

wrong_output.txt
第一行 apple 樱桃 ← 没有hello,但cherry还是被替换了(全局生效)
第二行 hello 苹果 樱桃 ← 两个替换都执行了
第三行 apple 樱桃 ← 没有hello,但cherry还是被替换了

这就是最容易踩的坑:你以为两条命令都受寻址控制,实际上只有第一条受控制。

2. 正确写法:用 {} 打包多条命令#

想让两条替换都只作用在包含 hello 的行上,就要用 {} 把两条命令包起来:

sed_correct_group.sh
sed '/hello/{s/apple/苹果/;s/cherry/樱桃/}' fruit.txt

语法拆解:

  • 大括号里的所有命令,都属于前面 /hello/ 这个寻址。
  • 命令之间用分号 ; 分隔。
  • 只有匹配到 hello 的行,才会依次执行括号里的两条命令。

执行结果完美符合预期:

correct_output.txt
第一行 apple cherry ← 没匹配hello,完全不处理
第二行 hello 苹果 樱桃 ← 两个替换都执行
第三行 apple cherry ← 没匹配hello,完全不处理

3. 两种书写格式#

  • 单行格式(适合短命令):命令之间用分号隔开,写在一行里。
sed_single_line.sh
sed -n '/^#/{s/^# //;s/ $//;p}' test.txt

意思:匹配 # 开头的行 → 先去掉开头的 # → 再去掉结尾的空格 → 打印出来。

  • 多行格式(适合长脚本,更易读):左括号 { 结尾换行,里面每条命令占一行,最后用 } 收尾。
sed_multi_line.sh
sed -n '/^#/{
s/^# //
s/ $//
p
}' test.txt

两种写法功能完全一样,只是排版不同。命令多的时候推荐多行写法,不容易看错。

4. 进阶搭配:范围寻址与多类型命令#

大括号不仅能跟单地址,也能跟「范围寻址」搭配,而且里面不局限于替换命令 s,删除 d、打印 p、插入 i、追加 a 都可以放进去。

示例一:结合范围寻址 给第 2 到第 4 行,同时做两件事:行首加 // 注释,行尾加 // 注释标记。

sed_range_group.sh
sed '2,4{
s/^/\/\/ /
s/$/ \/\/ 注释标记/
}' test.txt

示例二:组合多种不同命令 匹配到 error 行 → 行首加【错误】前缀 → 在这行后面追加一行分隔线。

sed_mixed_commands.sh
sed '/error/{
s/^/【错误】/
a\--- 以上是报错信息 ---
}' log.txt
命令组一句话总结与细节
  • 核心口诀:1条命令 → 直接写在寻址后面;≥2条命令 → 必须用 {} 包起来,共用同一个寻址。
  • 嵌套说明:语法上支持 {} 嵌套,但日常使用完全没必要,会大幅降低可读性。
  • 书写规范:寻址和 { 之间可以加空格,但不要换行(部分严格版本的 sed 会报错),规范写法是挨在一起或者加一个空格。

3.5 最容易踩的 4 个坑#

  1. $ 的双重身份

    • 在行号寻址里:$ 表示最后一行,比如 $d
    • 在正则里:$ 表示行尾,比如 /end$/ 不要写 /$/d,每一行都有行尾,会把所有行都删掉。
  2. 正则是包含匹配,不是全等匹配 /abc/ 会匹配 abc、xabc、abc123;只有 /^abc$/ 才是整行恰好等于 abc。

  3. 范围寻址会循环触发 文件里有多组 start/end 时,每一组都会被处理,不是只处理第一次。

  4. 寻址只作用于紧跟它的一条命令 要多条命令共用一个寻址,必须用 {} 包裹。

3.6 补充小技巧:更换正则分隔符#

sed 正则寻址默认用 / 当边界符号,比如 /^$/d,两个 / 就是用来框住正则内容的分隔符。

但如果你要匹配的内容本身就带大量 /(比如文件路径 /etc/nginx/conf.d),那路径里的每个 / 都得加反斜杠转义,写出来会非常乱(俗称“牙签地狱”):

sed_regex_delimiter.sh
# 传统写法:需要大量转义,容易写错也难读
sed '/\/etc\/nginx\/conf/d' file

自定义分隔符就是为了解决这个问题:换一个不会和内容冲突的符号(比如 @、#、%)当边界,里面的 / 就不用转义了。

1. 核心规则:寻址换分隔符,第一个必须加反斜杠#

sed 标准语法规定:在正则寻址里更换分隔符,必须在「第一个新分隔符」前面加一个反斜杠 \。

它的作用是告诉 sed:接下来这个字符不是正则内容,是我新指定的边界分隔符,后面第二个相同字符就是结束边界。

  • 标准格式:\c正则内容c(c 代表你选的新分隔符,如 @、#)
sed_custom_delimiter.sh
# 优雅写法:自定义分隔符(第一个字符前加反斜杠)
sed '\@/etc/nginx/conf@d' file

逐段拆解:

  • \@:标记新分隔符为 @
  • /etc/nginx/conf:正则内容(内部的 / 完全不用转义)
  • @:结束分隔符
  • d:动作

2. 90% 的人困惑的根源:两种分隔符规则不一样#

sed 里有两个地方能换分隔符,语法规则完全不同,很容易搞混:

场景原生语法换分隔符要不要加前导反斜杠自定义写法示例
正则寻址(选哪些行)/正则/必须加\@/etc/@d
s 替换命令(替换内容)s/旧/新/不用加,直接跟在 s 后面s#/etc#/opt#g

对比感受一下:

sed_delimiter_compare.sh
# s 命令换分隔符:s 后面直接跟 #,不用加 \
sed 's#/usr/bin#/bin#g' file
# 寻址换分隔符:第一个分隔符前必须加 \
sed '\#/usr/bin#d' file

为什么不一样?

  • s 是一个明确的命令关键字,sed 看到 s 就知道后面紧跟的字符是分隔符。
  • 正则寻址没有前置关键字,所以必须用 \ 做标记,不然 sed 会把 @、# 当成正则里的普通字符去匹配。

3. 补充细节#

  1. 分隔符任选:几乎所有单字符都能当分隔符,只要不和正则内容冲突就行,日常常用 @、#、%、|。
  2. 标准兼容:前导反斜杠是 POSIX 标准语法,所有版本 sed 都通用;建议养成加的习惯,换环境不容易报错。
  3. 范围寻址也能用:比如从匹配 /start/path 的行到 /end/path 的行,两个地址都要单独加反斜杠:
    sed_range_delimiter.sh
    sed '\@/start/path@,\@/end/path@d' file
分隔符规则速记
  • 用来选行的正则寻址换分隔符 → 第一个分隔符前必须加 \
  • 用来替换内容的 s 命令换分隔符 → 直接跟在 s 后面,不用加 \

四、sed基本编辑命令:增删改查四大操作#

4.1 查询和删除:p(打印)/ d(删除)#

p命令用于打印行,通常配合-n选项使用,实现”只显示特定行”的功能。d命令用于删除匹配的行,是数据清理的利器。

sed_p_d.sh
# 【p命令示例】打印查询
# 查询/etc/passwd中包含"root"的行
sed -n '/root/p' /etc/passwd
# 显示有效用户(排除#注释和空行)
sed -n '/^[^#]/p' /etc/passwd
# 打印第10到20行,并显示行号
sed -n '10,20p' test.txt
# 【d命令示例】删除行
# 删除第2行到第5行
sed '2,5d' test.txt
# 删除空行(^$表示行首直接是行尾)
sed '/^$/d' test.txt
# 删除以#开头的注释行
sed '/^#/d' config.conf
# 删除最后一行
sed '$d' test.txt
# 【批量清理日志】删除所有空行、注释行和特定模式行
sed -i -e '/^$/d' -e '/^#/d' -e '/^\s*$/d' -e '/@deprecated/d' config.txt

4.2 核心命令:s(替换)—— sed最常用的功能#

s命令是sed中最常用的命令,用于替换文本中的字符串。语法格式为s/pattern/replacement/flags。

s命令的关键标志位详解
  • g:全局替换,替换一行中所有匹配的字符串(默认只替换第一个)
  • i:忽略大小写进行匹配
  • n:只替换第n个匹配的字符串(n=2表示替换第2个)
  • p:打印被替换的行(通常配合-n选项使用)
  • w filename:将替换后的行写入指定文件
  • 分隔符:可以使用任意字符作为分隔符(如#、@),避免路径转义混乱
sed_replace.sh
# 【基础替换】只替换每行中的第一个"old"
sed 's/old/new/' test.txt
# 【全局替换】替换一行中的所有"old"
sed 's/old/new/g' test.txt
# 【忽略大小写】将所有"old"替换为"new"(不论大小写)
sed 's/old/new/gi' test.txt
# 【替换指定位置】只替换每行中的第2个"old"
sed 's/old/new/2' test.txt
# 【特殊符号:&】代表匹配到的**整个字符串**
# 在所有数字前后加上括号
sed 's/[0-9]\+/(&)/g' test.txt
# 输入:user 123 admin 456
# 输出:user (123) admin (456)
# 【特殊符号:\1, \2...】代表正则**分组**匹配到的第n个子串
# 将"first last"格式转换为"last, first"
sed 's/\([a-z]\+\) \([a-z]\+\)/\2, \1/' test.txt
# 输入:John Doe
# 输出:Doe, John
# 【多组分组例子】提取日志中的IP和时间戳
sed 's/^\([0-9.]\+\).*\[\([^]]*\)\].*/IP:\1 TIME:\2/' access.log
s命令的分隔符自定义技巧

当要替换的字符串包含/时(例如文件路径),如果继续使用/作为分隔符就需要大量的\转义。sed允许使用任意字符作为分隔符,这大大简化了复杂替换的编写。

sed_delimiter.sh
# 【分隔符冲突示例】
# 糟糕的写法:处理文件路径时大量转义,可读性极差
sed 's/\/usr\/local\/bin/\/opt\/bin/g' test.txt
# 优雅的写法一:使用 '#' 作为分隔符
sed 's#/usr/local/bin#/opt/bin#g' test.txt
# 优雅的写法二:使用 '@' 作为分隔符
sed 's@/usr/local/bin@/opt/bin@g' test.txt
# 【实际应用】修改Nginx配置文件中的路径
sed -i 's#/var/www/html#/data/www#g' /etc/nginx/nginx.conf

4.3 行操作:a(追加)/ i(插入)/ c(替换整行)#

这三个命令用于增加新行或修改现有行的内容。区别在于:i在行前插入,a在行后追加,c替换整行。

sed_insert.sh
# 【a命令:追加】在匹配行的后面插入新行
# 在包含"error"的行后面追加一行
sed '/error/ a\
--- ERROR OCCURRED ---' test.log
# 在文件最后一行后面追加版本号
sed '$ a\
Version: 1.0.0' config.txt
# 【i命令:插入】在匹配行的前面插入新行
# 在文件开头插入标题
sed '1 i\
===== System Configuration =====' config.txt
# 在所有"server"配置块前面添加分隔注释
sed '/^server {/ i\
# ===== New Server Block =====' nginx.conf
# 【c命令:替换整行】将匹配的行替换为新内容
# 将包含"old"的行替换为"new line"
sed '/old/ c\
new line' test.txt
# 【多行追加】使用转义新行在一条sed命令中追加多行
sed '/END_OF_HEADER/ a\
new line 1\
new line 2\
new line 3' test.txt
# 【实用场景】向配置文件添加新的配置块
sed '/\[database\]/ a\
host = 127.0.0.1\
port = 3306\
user = root' config.ini

4.4 y(字符转换)—— 按位映射转换字符#

y命令进行字符级转换,类似于tr命令。它将第一个集合中的字符一一映射到第二个集合中的字符。

sed_y.sh
# 【基础示例】将小写字母转换为大写字母
sed 'y/abcdefghijklmnopqrstuvwxyz/ABCDEFGHIJKLMNOPQRSTUVWXYZ/' test.txt
# 【实用例】简单的密码加密(凯撒密码)
# 将字母a-z转换为b-z,a(循环移位)
sed 'y/abcdefghijklmnopqrstuvwxyz/bcdefghijklmnopqrstuvwxyza/' message.txt
# 【数据转换】将分隔符从,转换为|
sed 'y/,/|/' data.csv
# 【反向映射】将大写转小写
sed 'y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/' test.txt

五、进阶核心:sed 反向引用(分组捕获)详解#

反向引用是 sed 进阶必学的核心能力,一句话讲透本质:

先用括号 () 把正则里的某段内容「捕获存起来」,再用 \1、\2 这种语法把存起来的内容拿出来复用。

它最常用在两个场景:

  1. 替换时保留部分原内容(比如提取字段、调换顺序、加前后缀)
  2. 匹配重复出现的内容(比如找连续重复的单词)

5.1 基础语法规则#

1. 捕获分组:用括号存内容#

在正则里用一对括号,把你想留住的内容包起来,这就叫一个「捕获分组」。 sed 会自动给分组编号,从左到右数左括号 (,第1个括号就是第1组,对应 \1,第2个对应 \2,以此类推,最多支持 9 个分组(\1 ~ \9)。

2. 反向引用:用 \N 取内容#

  • \1:引用第 1 个分组捕获到的内容
  • \2:引用第 2 个分组捕获到的内容
  • …
  • \9:引用第 9 个分组捕获到的内容

5.2 90% 新手踩的第一个坑:BRE 与 ERE 的括号转义#

这是最容易写错的地方,必须先讲清楚:sed 默认用基础正则(BRE),括号必须加反斜杠转义才是「分组」;加了 -r 参数才是扩展正则(ERE),括号直接写就行。

模式分组写法对应参数
基础正则 BRE(默认)\(内容\)无参数
扩展正则 ERE(内容)-r(GNU sed) / -E(BSD/macOS)

举个最简单的对比,功能完全一样:

sed_group_compare.sh
# 默认 BRE:括号必须转义
echo "hello123world" | sed 's/hello\([0-9]*\)world/\1/'
# 加 -r 扩展正则:括号不用转义
echo "hello123world" | sed -r 's/hello([0-9]*)world/\1/'

两条命令输出都是 123,只是写法不同。

最佳实践

建议日常写脚本优先加 -r 或 -E 启用扩展正则,可读性高,不容易写错转义。

5.3 从入门到进阶:4 个经典场景#

场景1:提取字段(最常用)#

这是最常用的用法:把整行都匹配掉,只保留括号里抓出来的内容。

需求:从 ip addr 风格的行里,提取出 inet 后面的 IP 地址。

sed_extract_ip.sh
# 输入文本
echo "inet 192.168.1.100/24 brd 192.168.1.255" | sed -r 's/^inet ([0-9.]+)\/.*$/\1/'
# 输出:192.168.1.100

拆解:

  1. ^inet :匹配行开头的 inet
  2. ([0-9.]+):第1组,捕获连续的数字和小数点(也就是IP)
  3. \/.*$:匹配斜杠和后面所有内容到行尾
  4. \1:整行替换成第1组捕获的IP

你也可以用 # 当分隔符,不用转义斜杠:s#^inet ([0-9.]+)/.*$#\1#

场景2:调换字段顺序#

多分组的经典用法:把捕获的内容调换位置。

需求:把 张三,男 这种「姓名,性别」格式,改成 性别:张三。

sed_swap_fields.sh
echo "张三,男" | sed -r 's/(.*),(.*)/\2:\1/'
# 输出:男:张三

拆解:

  • 第1组 (.*):捕获逗号前面的「张三」
  • 第2组 (.*):捕获逗号后面的「男」
  • 替换成 \2:\1:先放第2组,加冒号,再放第1组

场景3:给匹配内容加前后缀#

不用把整行替换掉,只在匹配到的内容周围加东西。

需求:把文中所有的数字,用 【】 包起来。

sed_add_prefix.sh
echo "订单号2024001已发货" | sed -r 's/([0-9]+)/【\1】/'
# 输出:订单号【2024001】已发货

场景4:在「查找正则」里用反向引用#

反向引用不只能用在替换部分,也能用在查找正则里,用来匹配重复出现的内容。

需求:找出连续出现两次 ab 的行,也就是匹配 abab。

sed_find_duplicate.sh
echo "abab hello" | sed -r '/(ab)\1/p' -n

拆解:

  • (ab):第1组捕获 ab
  • \1:引用第1组的内容,也就是再匹配一次 ab
  • 合起来就是匹配 abab

5.4 补充:特殊反向引用 &#

有一个高频简写:& 代表整个正则匹配到的全部内容,相当于不用写括号的「全部分组」。

比如上面给数字加括号的例子,用 & 写更简洁:

sed_ampersand.sh
# 两种写法效果完全一样
echo "订单号2024001已发货" | sed -r 's/[0-9]+/【&】/'
echo "订单号2024001已发货" | sed -r 's/([0-9]+)/【\1】/'
使用建议

只需要复用整个匹配结果时,优先用 &,代码更干净。

5.5 易错点与注意事项#

反向引用常见坑点
  1. 分组编号从 1 开始:sed 里没有 \0(部分版本支持但不通用),要取全部匹配内容用 &。
  2. 嵌套分组按左括号数:比如 ((a)(b)),从左数左括号:
    • 第1个左括号 → \1 = ab
    • 第2个左括号 → \2 = a
    • 第3个左括号 → \3 = b (日常尽量别写嵌套分组,可读性很差。)
  3. 作用域限制:\1 只能引用同一条 s 命令里的分组,跨命令、跨行都无效。
  4. 分隔符无关:不管你用 /、#、@ 当分隔符,\1 的写法永远不变。

六、sed高级技巧:模式空间与保持空间的力量#

前面我们提到了sed有两个工作空间:模式空间和保持空间。掌握这两个空间的交互是sed进阶的关键,可以实现Shell和其他工具难以完成的复杂文本处理。

6.1 模式空间与保持空间的交互命令#

空间交互命令速记
  • h:将模式空间内容复制到保持空间(覆盖)
  • H:将模式空间内容追加到保持空间(追加)
  • g:将保持空间内容获取到模式空间(覆盖)
  • G:在模式空间后面追加保持空间内容
  • x:交换模式空间和保持空间的内容
  • N:读取下一行到模式空间(用于多行处理)
  • d:删除模式空间,开始处理下一行
  • D:删除模式空间中第一个换行符前的部分

6.2 多行处理与高级文本操作#

使用模式空间和保持空间,我们可以实现复杂的多行处理,这在处理跨行日志、HTML、JSON等多行数据时非常有用。

sed_multiline.sh
# 【示例1】反转文件中的行顺序(模拟tac命令)
sed '1!G;h;$!d' test.txt
# 详细解释:
# 1!G - 第一行外,将保持空间追加到模式空间
# h - 复制当前行到保持空间
# $!d - 除了最后一行,删除模式空间(跳过输出),处理下一行
# 效果:最后一行被输出,倒数第二行被输出...实现行反转
# 【示例2】将多行内容合并为单行
sed ':a;N;$!ba;s/\n/ /g' test.txt
# 详细解释:
# :a - 定义标签a(用于分支跳转)
# N - 将下一行追加到模式空间
# $!ba - 如果不是最后一行,跳转回标签a(循环读取)
# s/\n/ /g - 最后将所有换行符替换为空格
# 效果:三行文本合并为一行
# 【示例3】删除包含"error"的行及其后面的2行
sed '/error/{N;N;d}' test.log
# 详细解释:
# /error/{...} - 如果行包含"error"
# N;N - 追加后续2行到模式空间
# d - 删除整个模式空间(3行内容全部删除)
# 【示例4】配对删除(删除成对的标记行)
# 删除从<block>到</block>的所有内容
sed '/<block>/,/<\/block>/d' test.html
# 【示例5】奇偶行互换
sed 'N;s/\(.*\)\n\(.*\)/\2\n\1/' test.txt
# 详细解释:
# N - 读下一行,在模式空间中有两行用\n分隔
# s/.../\2\n\1/ - 第二行\2,换行,第一行\1,实现互换

6.3 分支与标签:复杂流程控制#

sed支持:label(标签)和t/b(分支)命令,可以实现复杂的流程控制,虽然用得较少,但在处理复杂逻辑时非常强大。

sed_branch.sh
# 【基础分支】删除所有HTML标签
# 使用循环分支去除嵌套标签
sed -e :a -e 's/<[^>]*>//g;ta' test.html
# 详细解释:
# :a - 定义标签a
# s/... - 删除一个HTML标签
# ta - 如果替换成功,跳转回标签a(继续删除下一个标签)
# 效果:不断循环删除,直到没有标签为止
# 【条件分支】根据内容选择性处理
sed -e ':process' -e '/complete/!{N;b process;}' -e 's/\n/ /g' test.txt
# 效果:只在遇到"complete"关键词时合并多行

七、运维实战:sed在日常工作中的威力#

7.1 日志分析与数据提取#

日志分析是运维工程师的日常工作,sed可以快速提取、聚合和转换日志数据。

log_analysis.sh
# 【Nginx访问日志分析】
# 统计访问量最高的10个IP
sed -n 's/^\([0-9.]\+\).*/\1/p' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head -10
# 提取所有404错误的请求资源(IP、请求方法、路径)
sed -n 's/^\([0-9.]\+\).* 404 .* "\(GET\|POST\|PUT\|DELETE\) \([^ ]\+\) .*/\1 \2 \3/p' /var/log/nginx/access.log
# 统计某一小时内的访问量(10:00-10:59)
sed -n '/10\/May\/2026:10:[0-5][0-9]:/p' /var/log/nginx/access.log | wc -l
# 【应用日志错误统计】
# 提取所有ERROR级别的日志,并统计错误类型
sed -n 's/.*ERROR.*\[\([^]]*\)\].*/\1/p' app.log | sort | uniq -c | sort -rn
# 提取错误日志的时间戳和错误信息
sed -n 's/^\([^ ]*\).*ERROR:\(.*\)/\1: \2/p' error.log
# 【数据库日志分析】
# 统计执行时间超过1秒的SQL语句
sed -n 's/.*Query time: \([0-9.]*\).*SET timestamp=.* \(.*\);$/\1: \2/;T;s/\([0-9.]*\)/\1/;s/^[01].*//;T;p' slow.log

7.2 配置文件批量修改与安全变更#

配置修改中,使用sed可以快速实现自动化变更。这是运维自动化的重要组成部分。

config_update.sh
# 【安全的配置修改流程】
# 第一步:修改前备份
cp /etc/my.cnf /etc/my.cnf.backup_$(date +%Y%m%d_%H%M%S)
# 第二步:使用sed修改配置,留下.bak备份
sed -i.bak 's/^max_connections = .*/max_connections = 1000/' /etc/mysql/my.cnf
# 第三步:验证修改
grep "max_connections" /etc/mysql/my.cnf
# 【修改Nginx配置】
# 修改监听端口
sed -i 's/^\s*listen\s\+80;/ listen 8080;/' /etc/nginx/nginx.conf
# 修改日志位置
sed -i 's|/var/log/nginx|/data/log/nginx|g' /etc/nginx/nginx.conf
# 【批量修改服务器配置】
# 为所有.conf文件中的localhost改为0.0.0.0
for file in /etc/app/*.conf; do
sed -i.backup 's/localhost/0.0.0.0/g' "$file"
done
# 【修改应用配置】
# 修改数据库连接字符串中的IP地址
sed -i 's#jdbc:mysql://192.168.1.100:3306#jdbc:mysql://192.168.1.200:3306#g' application.properties

以修改SSH配置文件为例,实际效果相当于执行了如下内容变更:

/etc/ssh/sshd_config
# Authentication:
#PermitRootLogin yes
PermitRootLogin no
#StrictModes yes

实现这个变更的sed命令:

ssh_update.sh
# 修改SSH配置,禁止root登录
sed -i 's/^#PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
# 验证修改
grep -n "PermitRootLogin" /etc/ssh/sshd_config
# 应用配置(需要root权限)
systemctl reload sshd

7.3 批量数据清洗与格式转换#

在处理大量数据时,sed是数据清洗和格式转换的利器。

data_clean.sh
# 【Windows到Unix格式转换】
# 批量将文件中的DOS换行符(\r)转换为Unix换行符
sed -i 's/\r$//' *.txt
# 或者使用更直观的方式
sed -i 's/$//' *.txt # 删除回车符
# 【CSV到JSON格式转换】
# 将CSV文件转换为JSON格式数组
sed -e '1i [' -e '$s/$/]/' -e 's/^\(.*\)$/ {"name": "\1"},/' -e '$s/,$//' data.csv
# 【清理重复空格】
# 将多个空格压缩为一个
sed 's/ */ /g' messy.txt
# 【删除前后空格】
# 删除行首和行尾的空格
sed 's/^\s*//;s/\s*$//' whitespace.txt
# 【规范化数据格式】
# 将日期格式从"2026-05-14"改为"2026/05/14"
sed 's/\([0-9]\{4\}\)-\([0-9]\{2\}\)-\([0-9]\{2\}\)/\1\/\2\/\3/' dates.txt
# 【提取特定列】
# 从日志中提取用户名和访问时间
sed 's/^\([a-zA-Z0-9]*\).*\[\([^]]*\)\].*/User: \1, Time: \2/' access.log

八、常见问题与避坑指南#

8.1 变量解析陷阱:单引号 vs 双引号#

Shell变量无法解析的常见错误

在shell脚本中使用sed替换变量时,必须使用双引号而不是单引号。单引号会阻断Shell的变量展开机制,导致变量被当成字面值处理!

var_replace.sh
# 场景:需要将配置文件中的旧域名替换为新域名
old_domain="old.example.com"
new_domain="new.example.com"
# ❌ 错误做法:使用单引号
# 结果:sed会尝试将文字"$old"替换为"$new",完全错误!
sed 's/$old/$new/g' config.txt
# ✅ 正确做法:使用双引号
# 结果:变量被正常展开,命令变为 sed 's/old.example.com/new.example.com/g'
sed "s/$old_domain/$new_domain/g" config.txt
# 【更安全的做法】使用不同的分隔符,避免特殊字符冲突
sed "s#$old_domain#$new_domain#g" config.txt
# 【在脚本中使用变量的完整示例】
#!/bin/bash
config_file="/etc/app/config.conf"
new_ip="192.168.1.200"
# 修改配置文件中的数据库IP
sed -i "s/db_host=[^ ]*/db_host=$new_ip/" "$config_file"
# 验证修改
echo "新的数据库配置:"
grep "db_host" "$config_file"

8.2 正则表达式转义混乱#

sed中的转义规则

sed使用三层转义:

  1. Shell层:Shell会处理\和引号
  2. sed层:sed会处理正则表达式中的特殊字符
  3. 正则层:正则表达式的特殊含义

这导致有时需要多层转义,容易出错。使用扩展正则(-r)可以大幅简化。

escape_demo.sh
# 【转义混乱的例子】匹配文件路径中的点号
# 基础正则:需要三层转义,容易出错
sed 's/file\.txt/file.bak/' test.txt
# 在分组时更容易出错
# 基础:需要\\( \\)来表示分组
sed 's/\([0-9]\)\.\([0-9]\)/\1,\2/' version.txt
# 扩展正则:更清晰直观
sed -r 's/([0-9])\.([0-9])/\1,\2/' version.txt
# 【实际对比】提取IPv4地址
# 基础正则(繁琐的转义)
sed -n 's/.*\([0-9]\{1,3\}\)\.\([0-9]\{1,3\}\)\.\([0-9]\{1,3\}\)\.\([0-9]\{1,3\}\).*/\1.\2.\3.\4/p'
# 扩展正则(清晰易懂)
sed -rn 's/.*([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3}).*/\1.\2.\3.\4/p'

8.3 性能问题与优化#

当处理超大文件时,sed的性能可能成为瓶颈。这些优化技巧可以显著提升处理速度。

performance_tips.sh
# 【避免多次读文件】
# ❌ 低效:多次读文件,需要多次磁盘IO
sed 's/old/new/g' file.txt > file.tmp && mv file.tmp file.txt
sed 's/foo/bar/g' file.txt > file.tmp && mv file.tmp file.txt
# ✅ 高效:使用-e选项合并命令,只需一次磁盘IO
sed -e 's/old/new/g' -e 's/foo/bar/g' file.txt > output.txt
# 【避免贪心匹配】
# ❌ 低效:贪心匹配导致引擎回溯,性能差
sed 's/.*\(pattern\).*/\1/' file.txt
# ✅ 高效:使用非贪心匹配或精确模式
sed 's/[^<]*\(pattern\)[^>]*/\1/' file.txt
# 【使用-n避免无谓输出】
# ❌ 低效:输出整个文件,再用p打印特定行
sed '5p' large_file.txt > /dev/null
# ✅ 高效:只输出需要的行
sed -n '5p' large_file.txt > /dev/null
# 【处理超大文件时的分块处理】
# 避免一次性加载整个文件到内存
split -l 100000 large_file.txt chunk_
for f in chunk_*; do
sed 's/old/new/g' "$f" > "${f}.out"
done
cat chunk_*.out > result.txt

九、sed vs awk vs grep:工具选择指南#

作为运维工程师,经常会在sed、awk、grep之间选择。它们各有优势,理解区别才能选择最适合的工具。

三大文本处理工具的对比
工具适用场景学习难度执行效率
grep简单的行匹配、过滤最简单最快
sed流编辑、行级替换、删除、格式转换中等中等
awk复杂的数据处理、按列提取、统计汇总较复杂较慢

选择建议:

  • 只需查找行内容 → 用grep
  • 需要行级编辑(替换、删除、插入)→ 用sed
  • 需要按字段处理、统计聚合 → 用awk
tool_comparison.sh
# 【任务1:查找包含"error"的行】
grep "error" test.log # 最简单高效
# 【任务2:删除所有空行】
sed '/^$/d' test.txt # sed最合适
grep -v '^$' test.txt # grep也可以
# 【任务3:将每行第一个old替换为new】
sed 's/old/new/' test.txt # sed最简洁
# 【任务4:提取第3个字段(空格分隔)】
awk '{print $3}' test.txt # awk最自然
sed 's/ [^ ]* [^ ]* \([^ ]*\).*/\1/' test.txt # sed会很复杂
# 【任务5:统计访问最频繁的IP】
awk '{print $1}' access.log | sort | uniq -c | sort -rn | head -5 # awk
sed -n 's/^\([0-9.]*\).*/\1/p' access.log | sort | uniq -c | sort -rn | head -5 # sed

十、总结与进阶资源#

sed是Linux运维人员不可或缺的文本处理利器。虽然它的基础语法简单,但结合正则表达式与模式空间、保持空间机制后,却能爆发出惊人的数据处理能力。在执行危险操作前务必遵循黄金三原则:

sed操作的黄金规则
  1. 先测试打印:使用-n和p命令确认你的地址和模式是正确的
  2. 后原地修改:确认无误后才使用-i选项直接修改文件
  3. 多做备份:使用-i.bak选项自动备份,在出错时可以快速恢复
safe_sed_workflow.sh
# 【安全的sed使用流程】
# 步骤1:在终端测试,确保地址和模式正确
sed -n '/pattern/p' test.txt
# 步骤2:测试替换效果,不修改文件
sed 's/old/new/g' test.txt | head -20
# 步骤3:确认无误后,使用-i.bak备份原文件并修改
sed -i.bak 's/old/new/g' test.txt
# 步骤4:验证修改结果
diff test.txt.bak test.txt
grep "new" test.txt

进阶学习资源推荐:

  • GNU官方手册:man sed是最权威的速查手册,包含所有选项和命令详解
  • 在线调试工具:::github{repo=“aureliojargas/sedsed”},可视化展示Pattern Space和Hold Space的变化过程,非常有助于理解高级技巧
  • 经典书籍:《sed与awk》(第二版)是深入理解流式文本处理哲学的必读神作,强烈推荐
  • 实战练习:在自己的服务器上大量练习日志分析、配置修改等真实场景,是掌握sed的最快方式
Linux基础(11):sed命令从入门到精通完全指南
https://www.6ixblog.site/posts/linux-basic-11/
作者
Licwic
发布于
2025-02-14
许可协议
CC BY-NC-SA 4.0