Skip to content

sed 命令使用详解 ​

简介 ​

sed 的全称是 stream editor(流编辑器)。它逐行读取输入,把当前行放入模式空间(pattern space),按脚本中的地址和命令进行处理,然后输出结果。sed 由贝尔实验室的 Lee E. McMahon 在 1973 年前后开发,后来成为 POSIX 标准的一部分。

sed 最擅长的事情是“按行做替换和编辑”:批量改配置、删除注释和空行、在匹配行前后插入内容、提取区间、做简单的日志脱敏。它不需要把整个文件读进内存,因此非常适合处理大文件和管道输入。

文本处理三剑客

本文是系列文章之一:grep 负责按行筛选,sed 负责流式编辑,awk 负责字段处理与统计。

核心模型 ​

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 sed
bash
sudo dnf install sed
bash
sudo pacman -S sed
bash
apk add sed

查看版本:

bash
sed --version
# sed (GNU sed) 4.10

macOS 自带的是 BSD sed。GNU sed 的很多扩展在 BSD sed 上不可用,需要时可以用 Homebrew 安装 GNU sed:

bash
brew install gnu-sed
gsed --version

GNU 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
EOF
bash
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把多个文件视为独立输入,GNUsed -s -n '$p' a.txt b.txt
-z输入按 NUL 分隔,GNUsed -z 's/\n/ /g'
-u无缓冲输出,GNUtail -f app.log | sed -u 's/x/y/'
--debug输出脚本执行过程,GNUsed --debug 's/a/b/' file
--posix禁用 GNU 扩展,GNUsed --posix 's/a/b/' file
--follow-symlinks原地修改时跟随符号链接,GNUsed --follow-symlinks -i 's/a/b/' link
-l N设置 l 命令的换行宽度,GNUsed -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/从开头到第一次匹配 reGNU
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 pie

0,/re/ 允许结束模式在第一行就生效:

bash
printf 'apple\nbanana\napple pie\n' | sed -n '0,/apple/p'

输出:

text
apple

0,/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.txt

d 和 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.txt

POSIX 形式要求 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.txt

w 会在 sed 启动时截断目标文件,然后写入所有被选中的模式空间。需要追加而不是覆盖时,不要用 w,可以改为把 sed 的标准输出重定向追加:

bash
sed -n '/apple/p' sed-demo.txt >> out.txt

转换字符 ​

y 命令按字符一一替换,源字符串和目标字符串长度必须相同:

bash
echo 'abc' | sed 'y/abc/xyz/'
# xyz

y 不是正则替换,不支持 & 或反向引用,适合做简单的字符映射(例如大小写转换、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 c

ta 在每次替换成功后跳回 :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 次或多次,EREs/ab+c/X/
?前一项出现 0 次或 1 次,EREs/ab?c/X/
{n,m}前一项重复 n 到 m 次,EREs/[0-9]{2,4}/X/
^行首s/^/ /
$行尾s/$/!/
[abc]字符集合s/[abc]/X/g
[^abc]排除字符集合s/[^0-9]/X/g
(...)分组,EREs/(ab)+/X/

BRE 中的「或」要写成 \|,ERE 中直接写 |:

bash
# BRE
sed 's/error\|warning/X/g' file.txt

# ERE
sed -E 's/error|warning/X/g' file.txt

POSIX 字符类 ​

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.txt

BSD 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.txt

BSD/macOS sed:

bash
# 不备份
sed -i '' 's/old/new/g' file.txt

# 备份
sed -i '.bak' 's/old/new/g' file.txt

原地修改前先备份

sed -i 会直接覆盖文件。批量修改前建议:

  1. 先用不带 -i 的命令预览输出
  2. 用 diff -u file <(sed 's/old/new/g' file) 检查差异
  3. 确认后再加 -i,并保留 .bak
  4. 处理符号链接、硬链接和权限敏感文件时要格外小心

符号链接与硬链接 ​

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}'
  • grep:筛选行
  • sed:替换、删除、格式化
  • awk:字段提取和聚合

性能建议 ​

  • 尽量一次扫描完成多个替换:用多个 -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 sedBSD/macOS sed
-ised -i 's/a/b/' filesed -i '' 's/a/b/' file
-i 备份sed -i.bak 's/a/b/' filesed -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' file

d 删除模式空间并结束本轮,因此第 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#' file

7. 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' file

13. [a-z] 受 locale 影响 ​

和 grep 一样,字符范围可能受 locale 影响。处理 ASCII 时可以用 LC_ALL=C,需要语言相关匹配时用 [[:lower:]] 等字符类。

14. -i 可能破坏符号链接和硬链接 ​

GNU sed 默认替换文件,可能把符号链接变成普通文件,并断开硬链接。需要跟随符号链接时用 --follow-symlinks;有多硬链接的文件要先确认影响。

15. e 标志会执行命令 ​

bash
sed 's/.*/date/e' file

e 会把替换结果当 Shell 命令执行。处理不可信输入时绝对不要使用,存在命令注入风险。

16. w 会截断目标文件 ​

bash
sed -n '/pattern/w out.txt' file

w 在 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、库生态简单一行命令的启动成本
jqJSON 查询和转换普通文本行处理

选择建议:

  • 只是筛选行: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 的核心可以概括为:

  1. 逐行读入模式空间
  2. 按地址选择命令
  3. 执行替换、删除、插入等操作
  4. 默认自动输出,除非使用 -n、d、q、Q
  5. 需要跨行时用 N/D/P 或保持空间

最容易出错的地方是:BRE 与 ERE 的差异、-i 的跨平台写法、-n 与 p 的配合、& 的转义,以及把 sed 当成字段/统计工具。把替换、地址、删除、插入和原地修改这几组用法掌握好,日常配置处理和批量文本编辑就足够了。

需要筛选行时用 grep,需要字段计算和聚合时用 awk。

相关文章 ​

参考 ​