主题
sed 命令使用详解
简介
sed 的全称是 stream editor(流编辑器)。它逐行读取输入,把当前行放入模式空间(pattern space),按脚本中的地址和命令进行处理,然后输出结果。sed 由贝尔实验室的 Lee E. McMahon 在 1973 年前后开发,后来成为 POSIX 标准的一部分。
sed 最擅长的事情是“按行做替换和编辑”:批量改配置、删除注释和空行、在匹配行前后插入内容、提取区间、做简单的日志脱敏。它不需要把整个文件读进内存,因此非常适合处理大文件和管道输入。
核心模型
sed 默认对每一行执行以下循环:
text
读取一行
│
▼
放入 pattern space
│
▼
按地址选择并执行命令
│
├── d / D:结束本轮,不自动输出
├── n / N:读取下一行,继续处理
├── q / Q:退出
│
▼
自动输出 pattern space(除非 -n)
│
▼
读取下一行除了模式空间,sed 还有一个辅助缓冲区保持空间(hold space)。h/H/g/G/x 可以在两者之间复制、追加、交换,用来实现反转行、合并多行等技巧。
适用场景
- 批量替换字符串、路径、配置项
- 删除空行、注释行、匹配行
- 在匹配行前后插入或追加内容
- 提取两个标记之间的内容
- 处理 Windows 换行、行尾空白
- 日志脱敏:隐藏 IP、邮箱、手机号
- 配合
grep、find、xargs做批量文件修改 - 在管道中做轻量的格式清洗
sed 不擅长字段计算、分组统计和复杂条件判断。这些任务交给 awk 更合适;只是筛选行则用 grep 更直接。
安装与版本
大多数 Linux 发行版已经预装:
bash
sudo apt update
sudo apt install sedbash
sudo dnf install sedbash
sudo pacman -S sedbash
apk add sed查看版本:
bash
sed --version
# sed (GNU sed) 4.10macOS 自带的是 BSD sed。GNU sed 的很多扩展在 BSD sed 上不可用,需要时可以用 Homebrew 安装 GNU sed:
bash
brew install gnu-sed
gsed --versionGNU sed 与 BSD sed 的差异
最常见的差异是 -i:
bash
# GNU sed:直接原地修改
sed -i 's/old/new/g' file.txt
# BSD/macOS sed:-i 需要一个参数,空字符串表示不备份
sed -i '' 's/old/new/g' file.txt脚本如果要跨平台,建议先检测 sed --version,或者在 macOS 上统一使用 gsed。
基本语法
bash
sed [选项] '脚本' [文件...]常见形式:
bash
# 单个命令
sed 's/old/new/' file.txt
# 多个命令,用分号分隔
sed 's/old/new/; s/foo/bar/' file.txt
# 多个命令,用多个 -e
sed -e 's/old/new/' -e 's/foo/bar/' file.txt
# 从脚本文件读取
sed -f script.sed file.txt
# 从标准输入读取
cat file.txt | sed 's/old/new/'
# 原地修改
sed -i 's/old/new/g' file.txt脚本的基本单位是「地址 + 命令」:
text
[地址]命令地址省略时,命令作用于每一行。
示例数据
后面的例子主要使用这份 sed-demo.txt:
bash
cat > sed-demo.txt <<'EOF'
line1 apple red
line2 banana yellow
line3 cherry red
line4 date brown
EOFbash
sed -n '2p' sed-demo.txt输出:
text
line2 banana yellow-n 关闭自动输出,2p 表示只打印第 2 行。如果没有 -n,p 会在自动输出之外再打印一次,导致重复。
常用选项
| 选项 | 说明 | 示例 |
|---|---|---|
-n | 关闭自动输出,只输出显式要求的内容 | sed -n '2p' file |
-e | 指定一个脚本片段,可重复 | sed -e 's/a/b/' -e 's/c/d/' |
-f | 从文件读取脚本 | sed -f script.sed file |
-i[SUFFIX] | 原地修改,GNU 支持可选备份后缀 | sed -i.bak 's/a/b/' file |
-E | 使用扩展正则,推荐 | sed -E 's/(a)(b)/\2\1/' |
-r | 使用扩展正则,GNU 旧写法 | sed -r 's/(a)(b)/\2\1/' |
-s | 把多个文件视为独立输入,GNU | sed -s -n '$p' a.txt b.txt |
-z | 输入按 NUL 分隔,GNU | sed -z 's/\n/ /g' |
-u | 无缓冲输出,GNU | tail -f app.log | sed -u 's/x/y/' |
--debug | 输出脚本执行过程,GNU | sed --debug 's/a/b/' file |
--posix | 禁用 GNU 扩展,GNU | sed --posix 's/a/b/' file |
--follow-symlinks | 原地修改时跟随符号链接,GNU | sed --follow-symlinks -i 's/a/b/' link |
-l N | 设置 l 命令的换行宽度,GNU | sed -l 80 -n 'l' file |
-i 的写法
GNU sed 的 -i 后缀必须紧贴选项:
bash
sed -i.bak 's/old/new/' file # 备份为 file.bak
sed -i 's/old/new/' file # 不备份-i .bak 在 GNU sed 中不是“带 .bak 后缀”,.bak 会被当成脚本或文件参数。BSD sed 则相反,-i 需要一个独立参数,通常写 -i '' 表示不备份。
地址
地址决定命令作用于哪些行。地址可以省略,也可以是一个或两个。
行号地址
bash
# 第 3 行
sed -n '3p' file.txt
# 最后一行
sed -n '$p' file.txt
# 第 2 到第 4 行
sed -n '2,4p' file.txt
# 从第 2 行开始,再往后 3 行(GNU)
sed -n '2,+3p' file.txt
# 奇数行(GNU)
sed -n '1~2p' file.txt
# 第 3、6、9 行(GNU)
sed -n '0~3p' file.txt正则地址
bash
# 包含 apple 的行
sed -n '/apple/p' file.txt
# 从包含 start 的行到包含 end 的行
sed -n '/start/,/end/p' file.txt
# 不包含 apple 的行
sed -n '/apple/!p' file.txt
# 地址正则忽略大小写(GNU)
sed -n '/apple/Ip' file.txt如果正则中包含 /,可以换一个分隔符:
bash
sed -n '\%/usr/local%p' file.txt特殊地址
| 地址 | 含义 | 备注 |
|---|---|---|
$ | 最后一行 | POSIX |
n | 第 n 行 | POSIX |
/re/ | 匹配正则的行 | POSIX |
/start/,/end/ | 从匹配 start 到匹配 end 的区间 | POSIX |
n,+m | 从第 n 行到第 n+m 行 | GNU |
n,~m | 从第 n 行到下一个 m 的倍数行 | GNU |
first~step | 从 first 开始,每隔 step 行 | GNU |
0,/re/ | 从开头到第一次匹配 re | GNU |
addr! | 对地址取反 | POSIX |
0,/re/ 的特殊之处
普通的 1,/re/ 会从第 1 行开始寻找结束模式,但结束模式不会在第 1 行上检查。因此如果第 1 行就匹配,它会一直找到下一次匹配:
bash
printf 'apple\nbanana\napple pie\n' | sed -n '1,/apple/p'输出:
text
apple
banana
apple pie0,/re/ 允许结束模式在第一行就生效:
bash
printf 'apple\nbanana\napple pie\n' | sed -n '0,/apple/p'输出:
text
apple0,/re/ 是 GNU 扩展,BSD/macOS sed 不支持。
地址范围与同一行
范围模式 /start/,/end/ 在开始行匹配后打开,直到结束行匹配后关闭。如果开始行也匹配结束模式,范围会立即关闭:
bash
printf '[server]\nport 80\n[client]\nport 81\n' | sed -n '/^\[server\]/,/^\[/p'输出只有:
text
[server]要提取一个 section,用状态变量或更精确的结束模式,而不是让结束模式匹配开始行。
空正则
sed 中的空正则 // 表示“重复上一次使用的正则”:
bash
sed -n '/apple/p;//p' file.txt这会用 /apple/ 再匹配一次。这个行为和 awk 不同:awk 里的 // 是空正则,匹配所有记录。写 sed 脚本时要特别注意。
常用命令
打印与删除
| 命令 | 说明 |
|---|---|
p | 打印模式空间 |
P | 打印模式空间的第一行(遇到换行停止) |
d | 删除模式空间,结束本轮,不自动输出 |
D | 删除模式空间的第一行,然后重新开始本轮(不读取新行) |
n | 自动输出当前行(除非 -n),然后读取下一行 |
N | 把下一行追加到模式空间,不输出 |
= | 输出当前行号 |
l | 以可见形式输出,显示 $、Tab 等 |
q [code] | 输出当前模式空间并退出,可指定退出码 |
Q [code] | 直接退出,不输出模式空间,GNU |
bash
# 只打印第 2 行
sed -n '2p' sed-demo.txt
# 删除第 2 行
sed '2d' sed-demo.txt
# 删除包含 apple 的行
sed '/apple/d' sed-demo.txt
# 只保留包含 apple 的行,等价于 grep
sed -n '/apple/p' sed-demo.txt
# 打印行号
sed '=' sed-demo.txt
# 行号和内容放在同一行
sed '=' sed-demo.txt | sed 'N;s/\n/\t/'
# 只打印第一行,并显示不可见字符
sed -n '1l' sed-demo.txt
# 打印到第 2 行后退出
sed '2q' sed-demo.txt
# 不输出第 2 行,直接退出
sed '2Q' sed-demo.txtd 和 D 的区别:d 删除整个模式空间并开始下一轮;D 只删除第一行,如果模式空间还有内容,就从脚本开头重新执行,不再读取新行。D 常用于多行处理。
n 和 N 的区别:n 会先输出当前行(除非 -n),再读取下一行;N 不输出,直接把下一行追加到模式空间。
替换
替换命令的格式:
text
s/正则/替换文本/标志bash
# 每行替换第一个 red
sed 's/red/green/' sed-demo.txt
# 每行替换所有 red
sed 's/red/green/g' sed-demo.txt
# 只替换第二个匹配
echo 'red red red' | sed 's/red/green/2'
# 从第二个开始全部替换(GNU)
echo 'red red red' | sed 's/red/green/2g'
# 只输出发生替换的行
sed -n 's/red/green/p' sed-demo.txt用其他分隔符避免转义斜杠:
bash
sed 's#/usr/local#/opt#g' file.txt替换标志
| 标志 | 说明 |
|---|---|
g | 替换一行中的所有匹配 |
数字 | 只替换第 N 个匹配 |
p | 如果发生替换,打印模式空间 |
I / i | 忽略大小写,GNU |
M / m | 多行模式,让 ^/$ 匹配内嵌换行,GNU |
w file | 如果发生替换,写入文件 |
e | 把替换结果当命令执行,GNU,谨慎使用 |
bash
# 忽略大小写
echo 'RED red' | sed 's/red/green/I'
# 只输出替换成功的行
sed -n 's/red/green/p' sed-demo.txt
# 替换结果写入文件
sed -n 's/red/green/w replaced.txt' sed-demo.txt& 与反向引用
替换文本中的 & 代表整个匹配:
bash
echo 'abc' | sed 's/b/[&]/'
# a[b]c要输出字面量 &,写成 \&:
bash
echo 'abc' | sed 's/b/[\&]/'
# a[&]c用 \(...\) 分组,用 \1、\2 引用:
bash
# BRE
echo 'abc123' | sed 's/\([a-z]*\)\([0-9]*\)/\2-\1/'
# 123-abc
# ERE
echo 'abc123' | sed -E 's/([a-z]+)([0-9]+)/\2-\1/'
# 123-abc大小写转换
GNU sed 支持在替换文本中转换大小写:
bash
echo 'hello world' | sed -E 's/([a-z]+) ([a-z]+)/\U\1\E \u\2/'
# HELLO World\U:后续字符转大写\L:后续字符转小写\u:下一个字符转大写\l:下一个字符转小写\E:结束转换
换行与多行替换
GNU sed 支持在替换文本中写 \n:
bash
printf 'a b\n' | sed 's/ /\n/'输出:
text
a
b但 s///g 只在一个模式空间内替换。默认模式空间是一行,所以它不能跨行。跨行替换需要先把多行读入同一个模式空间:
bash
printf 'foo\nbar\nbaz\n' | sed -E ':a;N;$!ba;s/foo\nbar/X/g'输出:
text
X
baz或者用 -z 把整个输入当成一条记录(前提是数据中没有 NUL):
bash
printf 'foo\nbar\nbaz\n' | sed -z 's/foo\nbar/X/g'贪婪匹配
sed 的 BRE/ERE 没有非贪婪量词,.* 会尽可能匹配到行尾。要匹配最短内容,通常用排除字符类:
bash
echo '<a><b>' | sed -E 's/<[^>]*>/<tag>/g'
# <tag><tag>需要真正的非贪婪匹配时,sed 本身做不到,应该换 Perl、Python 或 grep -P 的思路。
插入、追加与修改
| 命令 | 说明 |
|---|---|
i\ | 在匹配行之前插入文本 |
a\ | 在匹配行之后追加文本 |
c\ | 用新文本替换匹配行 |
r file | 在匹配行之后读入文件内容 |
R file | 每次匹配读入文件的一行,GNU |
w file | 把模式空间写入文件 |
W file | 把模式空间的第一行写入文件,GNU |
GNU sed 支持把文本写在命令同一行:
bash
# 第 1 行前插入
sed '1i header' sed-demo.txt
# 最后一行后追加
sed '$a footer' sed-demo.txt
# 匹配 apple 的行后追加
sed '/apple/a after apple' sed-demo.txt
# 匹配 apple 的行前插入
sed '/apple/i before apple' sed-demo.txt
# 替换匹配行
sed '/apple/c changed' sed-demo.txtPOSIX 形式要求 i\、a\、c\ 后面跟换行,文本从下一行开始:
bash
sed '/apple/a\
after apple' sed-demo.txt跨平台脚本建议使用这种写法,或者明确依赖 GNU sed。
c 作用于一个范围时,会用一段文本替换整个范围:
bash
sed '2,3c\
changed' sed-demo.txt输出:
text
line1 apple red
changed
line4 date brown读写文件
bash
# 在匹配行后插入 extra.txt 的内容
sed '/apple/r extra.txt' sed-demo.txt
# 把匹配行写入 out.txt
sed -n '/apple/w out.txt' sed-demo.txt
# 每次匹配读取 extra.txt 的一行(GNU)
sed '/apple/R extra.txt' sed-demo.txtw 会在 sed 启动时截断目标文件,然后写入所有被选中的模式空间。需要追加而不是覆盖时,不要用 w,可以改为把 sed 的标准输出重定向追加:
bash
sed -n '/apple/p' sed-demo.txt >> out.txt转换字符
y 命令按字符一一替换,源字符串和目标字符串长度必须相同:
bash
echo 'abc' | sed 'y/abc/xyz/'
# xyzy 不是正则替换,不支持 & 或反向引用,适合做简单的字符映射(例如大小写转换、ROT13 等)。
保持空间
| 命令 | 说明 |
|---|---|
h | 模式空间复制到保持空间 |
H | 模式空间追加到保持空间 |
g | 保持空间复制到模式空间 |
G | 保持空间追加到模式空间 |
x | 交换模式空间和保持空间 |
反转文件行序:
bash
sed -n '1!G;h;$p' sed-demo.txt输出:
text
line4 date brown
line3 cherry red
line2 banana yellow
line1 apple red分解一下:
1!G:除第一行外,把保持空间追加到当前模式空间h:把当前模式空间复制回保持空间$p:最后一行打印
每读一行,保持空间里就积累着之前的内容,最终形成倒序。
给每行加一个空行:
bash
sed 'G' sed-demo.txt分支与标签
| 命令 | 说明 |
|---|---|
:label | 定义标签 |
b label | 无条件跳转到标签 |
t label | 如果自上次读取输入以来发生过成功替换,跳转,GNU/POSIX |
T label | 如果没有发生成功替换,跳转,GNU |
把连续空格压缩成一个:
bash
printf 'a b c\n' | sed -e ':a' -e 's/ / /' -e 'ta'输出:
text
a b cta 在每次替换成功后跳回 :a,直到没有连续两个空格为止。
多行处理
N 把下一行追加到模式空间,D 删除第一行并重新开始,P 只打印第一行。它们组合起来可以处理跨行内容。
bash
# 把所有行合并成一行
sed ':a;N;$!ba;s/\n/ /g' file.txt这个脚本会一直读到文件末尾,把整个文件放入模式空间,然后把换行替换成空格。文件很大时会占用较多内存,不适合处理超大文件。
逐行处理但保留跨行状态时,可以写成循环:
bash
sed -n '
/start/ {
:loop
N
/end/!b loop
p
}
' file.txt这段脚本从 start 开始不断 N,直到遇到 end 再打印。实际使用时建议先在小文件上验证。
正则表达式
sed 默认使用 BRE,-E 使用 ERE。两者的差异和 grep 类似:\(...\)、\{m,n\} 是 POSIX BRE 语法,\+、\?、\| 是 GNU 扩展:
bash
# BRE:分组和量词需要转义
echo 'abc123' | sed 's/\([a-z]*\)\([0-9]*\)/\2-\1/'
# ERE:更直观
echo 'abc123' | sed -E 's/([a-z]+)([0-9]+)/\2-\1/'常用元字符
| 写法 | 含义 | 示例 |
|---|---|---|
. | 任意单个字符 | s/a.c/X/ |
* | 前一项重复 0 次或多次 | s/ab*c/X/ |
+ | 前一项重复 1 次或多次,ERE | s/ab+c/X/ |
? | 前一项出现 0 次或 1 次,ERE | s/ab?c/X/ |
{n,m} | 前一项重复 n 到 m 次,ERE | s/[0-9]{2,4}/X/ |
^ | 行首 | s/^/ / |
$ | 行尾 | s/$/!/ |
[abc] | 字符集合 | s/[abc]/X/g |
[^abc] | 排除字符集合 | s/[^0-9]/X/g |
(...) | 分组,ERE | s/(ab)+/X/ |
BRE 中的「或」要写成 \|,ERE 中直接写 |:
bash
# BRE
sed 's/error\|warning/X/g' file.txt
# ERE
sed -E 's/error|warning/X/g' file.txtPOSIX 字符类
bash
# 删除行首空白
sed 's/^[[:space:]]*//' file.txt
# 删除行尾空白
sed 's/[[:space:]]*$//' file.txt
# 删除空行和只含空白的行
sed '/^[[:space:]]*$/d' file.txt
# 删除注释行(允许行首缩进)
sed '/^[[:space:]]*#/d' file.txt单词边界
GNU sed 支持 \b、\<、\>:
bash
# 只匹配完整单词 cat
sed -n '/\bcat\b/p' file.txt
# 另一种写法
sed -n '/\<cat\>/p' file.txtBSD sed 对这些扩展的支持不一致,跨平台时可以用 [[:space:]]、标点等显式边界,或者改用 awk。
地址中的正则
bash
# 匹配包含 error 的行
sed -n '/error/p' app.log
# 忽略大小写
sed -n '/error/Ip' app.log
# 从 BEGIN 到 END
sed -n '/BEGIN/,/END/p' file.txt
# 从 BEGIN 到 END,但不打印 END 行
sed -n '/BEGIN/,/END/{/END/!p}' file.txt空正则与重复使用
sed 的 // 表示重复上一次正则:
bash
sed -n '/error/p;//p' app.log第一次 p 打印匹配 error 的行,第二次 //p 再次使用 error 模式。这个特性可以用来复用长正则,但也容易让脚本难以阅读。
原地修改
GNU sed:
bash
# 直接修改
sed -i 's/old/new/g' file.txt
# 修改并保留备份
sed -i.bak 's/old/new/g' file.txt
# 多个文件
sed -i 's/old/new/g' a.txt b.txtBSD/macOS sed:
bash
# 不备份
sed -i '' 's/old/new/g' file.txt
# 备份
sed -i '.bak' 's/old/new/g' file.txt原地修改前先备份
sed -i 会直接覆盖文件。批量修改前建议:
- 先用不带
-i的命令预览输出 - 用
diff -u file <(sed 's/old/new/g' file)检查差异 - 确认后再加
-i,并保留.bak - 处理符号链接、硬链接和权限敏感文件时要格外小心
符号链接与硬链接
GNU sed 的 -i 默认会创建临时文件再重命名,因此可能把符号链接替换成普通文件,硬链接也会断开。需要跟随符号链接时:
bash
sed --follow-symlinks -i 's/old/new/g' link.txt如果文件有多个硬链接,原地修改后其他链接可能仍指向旧内容。这类场景更适合用编辑器或专门工具处理。
多文件与 -s
默认情况下,sed 把多个文件当成一个连续的输入流,行号和 $ 都是全局的:
bash
sed -n '$p' a.txt b.txt # 只打印最后一个文件的最后一行-s 让每个文件独立计数(GNU):
bash
sed -s -n '$p' a.txt b.txt
# 分别打印 a.txt 和 b.txt 的最后一行配合 grep 批量修改
bash
# 先列出会修改的文件
grep -rlZF 'old.domain' . | xargs -0 -r echo
# 确认后再批量替换
grep -rlZF 'old.domain' . | xargs -0 -r sed -i 's/old\.domain/new.domain/g'grep -lZ 和 xargs -0 可以正确处理带空格、换行的文件名。替换前先跑一次不带 -i 的版本,确认输出符合预期。grep 的用法见 grep 命令使用详解。
实战案例
删除注释和空行
bash
sed '/^[[:space:]]*#/d; /^[[:space:]]*$/d' config.conf也可以写成:
bash
sed -e '/^[[:space:]]*#/d' -e '/^[[:space:]]*$/d' config.conf修改配置项
bash
# 把 port=8080 改成 port=9090
sed -i 's/^port=8080$/port=9090/' app.conf
# 允许行首空白
sed -i 's/^[[:space:]]*port=8080[[:space:]]*$/port=9090/' app.conf如果配置项可能被注释掉,可以先取消注释再改值,或者直接用更精确的正则。
给非空行加缩进
bash
sed '/^[[:space:]]*$/!s/^/ /' file.txt/^[[:space:]]*$/! 表示“不是空行或纯空白行”,然后在这些行前加四个空格。
删除行尾空白和 CR
bash
# 删除行尾空白
sed -i 's/[[:space:]]*$//' file.txt
# 删除 Windows 的 CR
sed -i 's/\r$//' file.txt处理 Windows 文件时,先确认是 CRLF 而不是只有 CR;file、dos2unix、xxd 都可以帮助判断。
提取区间
bash
# 包含 start 和 end
sed -n '/start/,/end/p' file.txt
# 不包含 end
sed -n '/start/,/end/{/end/!p}' file.txt
# 从 start 往后 2 行(GNU)
sed -n '/start/,+2p' file.txt给文件加行号
bash
sed '=' file.txt | sed 'N;s/\n/\t/'输出:
text
1 第一行
2 第二行sed '=' 先输出行号,再用第二个 sed 把行号和内容合并到一行。更简单的替代是 nl -ba file.txt。
反转行序
bash
sed -n '1!G;h;$p' file.txt适合小文件。大文件用 tac file.txt 更直接。
合并多行
bash
# 把所有行合并成一行
sed ':a;N;$!ba;s/\n/ /g' file.txt
# 把连续以反斜杠结尾的行合并
sed ':a;/\\$/N;s/\\\n//;ta' file.txt第二个脚本会不断把下一行合并进来,直到当前行不以反斜杠结尾。处理配置文件、Shell 脚本续行时很有用。
删除 HTML 标签
bash
sed 's/<[^>]*>//g' page.html这是最简单的去标签方式,不适合处理属性中包含 >、脚本内容或复杂 HTML。正式解析 HTML 应该用专门的解析器。
日志脱敏
bash
# 隐藏 IP
sed -E 's/([0-9]{1,3}\.){3}[0-9]{1,3}/[IP]/g' app.log
# 隐藏邮箱
sed -E 's/[[:alnum:]._%+-]+@[[:alnum:].-]+\.[[:alpha:]]{2,}/[EMAIL]/g' app.log
# 隐藏手机号(简单的中国大陆手机号)
sed -E 's/1[3-9][0-9]{9}/[PHONE]/g' app.log这些正则适合日志脱敏的粗略处理,不保证覆盖所有合法格式,也不替代专业的数据脱敏工具。
一次替换多个模式
bash
# 顺序执行两个替换
sed -e 's/foo/bar/g' -e 's/baz/qux/g' file.txt
# 多个模式替换成同一个值
sed -E 's/(error|warning|critical)/[LOG]/g' file.txt多个 -e 是按顺序执行的,前一个替换的结果会影响后一个替换。需要互不影响时,用一次正则或改用 awk。
注释与取消注释
bash
# 给每一行加 # 注释
sed 's/^/# /' file.txt
# 取消行首的 # 和紧随其后的空格
sed 's/^#[[:space:]]*//' file.txt
# 只取消 port= 开头的配置项注释
sed -E 's/^#[[:space:]]*(port[[:space:]]*=)/\1/' app.conf统计行数
bash
sed -n '$=' file.txt$= 打印最后一行的行号,也就是文件行数。空文件不会输出任何内容,需要的话可以先判断文件是否为空。
与 grep、awk 组合
bash
# 先筛出错误行,再隐藏时间戳
grep 'ERROR' app.log | sed -E 's/^\[[^]]+\] //'
# 先清洗字段,再统计
grep 'ERROR' app.log \
| sed 's/.*ERROR/ERROR/' \
| awk '{count[$2]++} END {for (k in count) print count[k], k}'性能建议
- 尽量一次扫描完成多个替换:用多个
-e比多次运行 sed 更高效。 - 用
-n减少输出:只输出需要的内容,避免无谓的 I/O。 - 尽早退出:只需要前几行时用
q;只需要判断是否存在时,考虑 grep。 - 避免无谓的
N:N会把多行读入内存,处理大文件时可能很慢。 -z会改变记录边界:把整个文件当成一条记录时,内存占用会上升。- 设置
LC_ALL=C:处理 ASCII 数据时通常更快,也避免 locale 带来的范围差异。 - 用
grep预过滤:先筛出少量行,再交给 sed 处理,比在 sed 里写复杂条件更直观。 - 复杂逻辑换 awk:需要字段计算、数组、统计时,awk 更合适。
- 原地修改注意磁盘:
sed -i需要写临时文件,大文件会额外占用磁盘空间。 - 用
--debug调试:GNU sed 的--debug可以看到地址匹配和命令执行过程。
GNU sed 与 BSD sed 差异
| 功能 | GNU sed | BSD/macOS sed |
|---|---|---|
-i | sed -i 's/a/b/' file | sed -i '' 's/a/b/' file |
-i 备份 | sed -i.bak 's/a/b/' file | sed -i '.bak' 's/a/b/' file |
| 扩展正则 | -E 或 -r | -E |
替换中的 \n | 支持 | 不保证支持,建议用字面换行 |
I 标志 | 支持 | 支持情况不一 |
0,/re/ | 支持 | 不支持 |
first~step、addr,+N | 支持 | 不支持 |
Q、T、z、F、e、R、W | 支持 | 不支持 |
单行 i/a/c text | 支持 | 可能要求 i\ 后换行 |
-s、-z | 支持 | 不支持 |
--follow-symlinks | 支持 | 不支持 |
\+、\?、\b、\w、\s | 支持 | 支持情况不一 |
--debug、--posix | 支持 | 不支持 |
跨平台脚本的原则:
- 优先使用 POSIX 语法
-i单独处理- 不依赖 GNU 扩展,或者显式检测
gsed - 复杂脚本直接用 awk/Perl/Python
常见坑
1. -i 在 GNU 和 BSD 上写法不同
bash
# GNU
sed -i 's/old/new/g' file
# BSD/macOS
sed -i '' 's/old/new/g' file在 macOS 上直接运行 GNU 写法,-i 会把后面的脚本当成备份后缀,导致命令行为不符合预期。
2. BRE 中 +、?、| 需要转义
bash
echo 'aaab' | sed 's/a+b/X/' # 不匹配
echo 'aaab' | sed -E 's/a+b/X/' # 匹配日常优先使用 -E。
3. -n 与 p 的配合
bash
sed -n '2p' file # 只输出第 2 行
sed '2p' file # 第 2 行会输出两次p 只是“额外打印”,不会自动关闭默认输出。需要只输出匹配行时必须加 -n。
4. d 之后不会再自动输出
bash
sed '2d' filed 删除模式空间并结束本轮,因此第 2 行不会出现在输出中。不要在 d 后面再写 p 期望打印。
5. 替换文本中的 & 有特殊含义
bash
echo 'abc' | sed 's/b/[&]/' # a[b]c
echo 'abc' | sed 's/b/[\&]/' # a[&]c需要字面量 & 时必须转义。
6. 分隔符冲突
bash
# 不优雅:需要转义所有斜杠
sed 's/\/usr\/local\/bin/\/opt\/bin/' file
# 更清晰:换分隔符
sed 's#/usr/local/bin#/opt/bin#' file7. Shell 变量不会在单引号中展开
bash
old=foo
new=bar
sed 's/$old/$new/g' file # 错误:awk/sed 看到的是字面量 $old
sed "s/$old/$new/g" file # 可以,但要注意正则特殊字符更安全的做法是用 -e 拼接,或者用 awk 的 -v 传变量。变量内容包含 /、&、\ 时尤其要小心。
8. 地址范围可能在同一行关闭
bash
sed -n '/^\[server\]/,/^\[/p' nginx.conf如果开始行也匹配结束模式,范围会立即关闭。提取 section 时要用状态变量或更精确的边界。
9. 空正则 // 会复用上一次正则
bash
sed 's/foo/bar/; s//baz/' file第二个 s//baz/ 会复用 foo。如果不了解这个行为,很容易得到意外结果。
10. s///g 只在一行内生效
bash
sed 's/foo/bar/g' file如果 foo 跨行(例如 foo\nbar),默认模式空间只包含一行,替换不会命中。需要 N、-z 或换工具。
11. 贪婪匹配没有非贪婪选项
bash
echo '<a><b>' | sed -E 's/<.*>/<tag>/'
# <tag>.* 会匹配到最后一个 >。要匹配每个标签,用 <[^>]*>;需要真正的非贪婪时用 Perl/Python。
12. \d 不可移植
sed 的 BRE/ERE 不支持 \d,用 [0-9] 或 [[:digit:]]:
bash
sed -E 's/[0-9]+/X/g' file13. [a-z] 受 locale 影响
和 grep 一样,字符范围可能受 locale 影响。处理 ASCII 时可以用 LC_ALL=C,需要语言相关匹配时用 [[:lower:]] 等字符类。
14. -i 可能破坏符号链接和硬链接
GNU sed 默认替换文件,可能把符号链接变成普通文件,并断开硬链接。需要跟随符号链接时用 --follow-symlinks;有多硬链接的文件要先确认影响。
15. e 标志会执行命令
bash
sed 's/.*/date/e' filee 会把替换结果当 Shell 命令执行。处理不可信输入时绝对不要使用,存在命令注入风险。
16. w 会截断目标文件
bash
sed -n '/pattern/w out.txt' filew 在 sed 启动时截断 out.txt。如果想追加,应使用 Shell 的 >>,或者换一种处理方式。
17. i/a/c 的跨平台写法
GNU sed 支持 sed '1i header',但 BSD sed 可能要求:
bash
sed '1i\
header' file写可移植脚本时使用 POSIX 形式。
18. sed 不是字段处理工具
bash
# 不推荐:用 sed 取第二列
sed 's/^[^ ]* //' file按列处理、求和、分组时用 awk 更清晰、更可靠。sed 只适合做行级文本变换。
19. 二进制和 NUL 数据
sed 默认按行处理,遇到 NUL 或二进制数据可能行为异常。需要按 NUL 分隔时用 -z,但要注意内存和输出格式。
20. 不要用 sed 解析 JSON/XML/HTML
sed 只能做简单的文本替换。结构化数据应该用 jq、XML/HTML 解析器或 Python,而不是堆叠复杂的 sed 正则。
与其他工具对比
| 工具 | 擅长 | 不擅长 |
|---|---|---|
| grep | 按行筛选、正则搜索、递归查找 | 字段计算、替换、聚合 |
| sed | 流式替换、删除、插入、按行编辑 | 数组、统计、复杂逻辑 |
| awk | 字段处理、过滤、统计、报表 | JSON/XML、复杂 CSV |
cut | 按固定分隔符取列 | 条件判断、正则、计算 |
tr | 字符替换、删除、压缩 | 按行/字段处理 |
perl / python | 复杂正则、JSON/CSV、库生态 | 简单一行命令的启动成本 |
jq | JSON 查询和转换 | 普通文本行处理 |
选择建议:
- 只是筛选行:grep
- 只是替换、删除、插入文本:sed
- 需要按列判断、求和、分组、生成报表:awk
- 需要复杂正则、非贪婪、环视:Perl/Python 或
grep -P - 需要解析 JSON:jq
典型流水线:
bash
grep 'ERROR' app.log \
| sed -E 's/^\[[^]]+\] //' \
| awk '{count[$2]++} END {for (k in count) print count[k], k}'- grep:筛出错误行
- sed:去掉时间戳等前缀
- awk:按字段统计
速查表
bash
# 只打印第 2 行
sed -n '2p' file.txt
# 打印最后一行
sed -n '$p' file.txt
# 打印 2 到 4 行
sed -n '2,4p' file.txt
# 打印包含 pattern 的行
sed -n '/pattern/p' file.txt
# 删除包含 pattern 的行
sed '/pattern/d' file.txt
# 删除空行
sed '/^[[:space:]]*$/d' file.txt
# 删除注释行
sed '/^[[:space:]]*#/d' file.txt
# 替换第一个匹配
sed 's/old/new/' file.txt
# 替换全部匹配
sed 's/old/new/g' file.txt
# 忽略大小写替换
sed 's/old/new/I' file.txt
# 只输出替换成功的行
sed -n 's/old/new/p' file.txt
# 使用其他分隔符
sed 's#/old/path#/new/path#g' file.txt
# 分组与反向引用
sed -E 's/([a-z]+)([0-9]+)/\2-\1/' file.txt
# 在匹配行前插入
sed '/pattern/i before' file.txt
# 在匹配行后追加
sed '/pattern/a after' file.txt
# 替换匹配行
sed '/pattern/c replacement' file.txt
# 在第 1 行前插入
sed '1i header' file.txt
# 在最后一行后追加
sed '$a footer' file.txt
# 删除行尾空白
sed 's/[[:space:]]*$//' file.txt
# 删除 Windows CR
sed 's/\r$//' file.txt
# 提取区间
sed -n '/start/,/end/p' file.txt
# 提取区间但不包含结束行
sed -n '/start/,/end/{/end/!p}' file.txt
# 原地修改
sed -i 's/old/new/g' file.txt
# 原地修改并备份
sed -i.bak 's/old/new/g' file.txt
# 反转行序
sed -n '1!G;h;$p' file.txt
# 合并所有行
sed ':a;N;$!ba;s/\n/ /g' file.txt
# 行号加内容
sed '=' file.txt | sed 'N;s/\n/\t/'
# 统计行数
sed -n '$=' file.txt总结
sed 的核心可以概括为:
- 逐行读入模式空间
- 按地址选择命令
- 执行替换、删除、插入等操作
- 默认自动输出,除非使用
-n、d、q、Q - 需要跨行时用
N/D/P或保持空间
最容易出错的地方是:BRE 与 ERE 的差异、-i 的跨平台写法、-n 与 p 的配合、& 的转义,以及把 sed 当成字段/统计工具。把替换、地址、删除、插入和原地修改这几组用法掌握好,日常配置处理和批量文本编辑就足够了。