sed —— 流编辑器
一句话定义
sed(stream editor)读取输入(文件或 stdin),按你给的命令做替换/删除/插入,输出到 stdout(或 -i 原地改文件)。最常用的是 s/pattern/replace/(substitute),日常 99% 场景就这一个。
典型场景
训练营文档里出场 24 次。绝大多数都在做"原地改配置文件":
# 改 sshd 配置(出自 Day0 §2.3)
sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
# 改 sysctl 参数
sed -i '/^#net.ipv4.ip_forward/s/^#//' /etc/sysctl.conf
# 改 hosts 文件
sed -i '/old-host/d' /etc/hosts
sed 的本质:行流处理
sed 一行一行处理输入:
- 读入一行到"模式空间"(pattern space)
- 对这一行运行你给的命令
- 输出模式空间内容(默认)
- 读下一行
知道这个就够了——sed 不是"打开文件 → 找到 → 修改 → 保存",而是"一边读一边改一边输出"。
input.txt sed 'command' stdout
line 1 ───────► apply to "line 1" ───► modified line 1
line 2 ───────► apply to "line 2" ───► modified line 2
line 3 ───────► apply to "line 3" ───► modified line 3
最常用:s/pattern/replace/ 替换
echo "hello world" | sed 's/world/k8s/'
# hello k8s
echo "foo foo foo" | sed 's/foo/bar/' # 只换每行第一个
# bar foo foo
echo "foo foo foo" | sed 's/foo/bar/g' # g = global,全部换
# bar bar bar
echo "FOO foo" | sed 's/foo/bar/I' # I = ignore case
# bar bar
分隔符可以换
s/pat/rep/ 默认用 / 作分隔符。如果 pattern 或 replace 里有 /,会很丑:
sed 's/\/usr\/local\/bin/\/opt\/bin/' file # 太多反斜线
sed 's|/usr/local/bin|/opt/bin|' file # 换用 | 作分隔符
sed 's,/usr/local/bin,/opt/bin,' file # 用 ,
/、|、,、# 等都行(除了反斜线和换行)。改路径时记得换分隔符,代码可读性高十倍。
替换里引用匹配内容
| 引用 | 含义 |
|---|---|
& | 整个匹配内容 |
\1 \2 ... | 第 N 个捕获组(\(...\) BRE 或 (...) -E ERE) |
echo "kubelet started" | sed 's/kubelet/[&]/'
# [kubelet] started ← & 引用整个匹配
echo "ssh root@10.0.24.28" | sed -E 's/root@([0-9.]+)/admin@\1/'
# ssh admin@10.0.24.28 ← \1 是捕获的 IP
-i 原地修改(写脚本必会)
sed -i 's/old/new/' file.txt # 直接改文件,无输出
sed -i.bak 's/old/new/' file.txt # 改之前备份成 file.txt.bak
-i.bak 是最安全的写法:跑完出问题能 rollback。生产环境第一次用 sed 改文件,永远先 -i.bak。
macOS / BSD 的
sed -i行为不同:必须给后缀(哪怕空字符串):sed -i '' 's/old/new/' file.txt # macOS:'' 表示不备份 sed -i.bak 's/old/new/' file.txt # macOS / Linux 都行在跨平台脚本里统一写
sed -i.bak然后rm file.bak是最稳的。
地址(按行号或模式选行)
sed 命令前可以指定地址——只对匹配的行执行命令。
| 地址语法 | 含义 |
|---|---|
| 无 | 所有行 |
5 | 第 5 行 |
5,10 | 第 5 到 10 行 |
$ | 最后一行 |
5,$ | 第 5 行到末尾 |
/pattern/ | 匹配 pattern 的行 |
/start/,/end/ | start 行到 end 行(含两端) |
5~3 | 第 5 行开始每 3 行(5, 8, 11, ...) |
例子
sed -n '1,5p' file # 只打印第 1-5 行(-n 不默认输出,p 打印)
sed '5d' file # 删第 5 行
sed '/^#/d' file # 删所有以 # 开头的行(注释)
sed '/^$/d' file # 删空行
sed '/start/,/end/d' file # 删 start 到 end 之间的所有行(含)
sed -i '/^PasswordAuthentication/d' /etc/ssh/sshd_config
# 删 sshd_config 里以 PasswordAuthentication 开头的行
sed '/error/s/$/ <-- check/' log
# 给含 error 的行末加注释
训练营里最实用的几个套路
套路 1:注释 / 取消注释某行
# 注释(在行首加 #)
sed -i '/^kubelet/s/^/#/' /etc/somefile
# 取消注释(去掉行首 #)
sed -i '/^#kubelet/s/^#//' /etc/somefile
# 通用"含某词的行取消注释"
sed -i 's/^#\(.*kubelet.*\)/\1/' /etc/somefile
套路 2:改配置参数(已存在)
sed -i 's/^PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
^Pattern.* —— 行首 + 所有内容。这样不管原值是 yes / no / commented,都被替换成新值。
但如果原配置里没这一行呢?sed 改不出来。所以更稳的套路是:
套路 3:改或加(idempotent)
# 如果存在 → 改;不存在 → 加
if grep -q "^PasswordAuthentication" /etc/ssh/sshd_config; then
sed -i 's/^PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
else
echo "PasswordAuthentication no" >> /etc/ssh/sshd_config
fi
或者更紧凑(grep 静默 + 短路):
grep -q "^PasswordAuthentication" /etc/ssh/sshd_config \
&& sed -i 's/^PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config \
|| echo "PasswordAuthentication no" >> /etc/ssh/sshd_config
套路 4:批量 ssh 改远端
Day0 改 5 个节点的 sshd_config:
for h in m1 m2 m3 m4 m5; do
ssh "$h" "sed -i \
-e 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' \
-e 's/^#*PermitRootLogin.*/PermitRootLogin prohibit-password/' \
/etc/ssh/sshd_config"
done
-e cmd 可以叠多个命令(见下面)。
套路 5:删除某段
# 删除 /etc/hosts 里关于 old-host 的所有行
sed -i '/old-host/d' /etc/hosts
# 删除两个标记之间的内容(含标记)
sed -i '/# BEGIN MARKER/,/# END MARKER/d' /etc/somefile
套路 6:在某行后插入
sed -i '/^Match Group admin/a\ PermitRootLogin yes' /etc/ssh/sshd_config
# 在 Match Group admin 那一行后插入 " PermitRootLogin yes"
sed -i '/^# end of file/i\new line above' /etc/somefile
# 在 "# end of file" 那一行前插入
a\ = append (after),i\ = insert (before),c\ = change (replace whole line)。
多命令:-e 或 ; 或 {}
# 多个 -e
sed -e 's/foo/bar/' -e 's/baz/qux/' file
# 分号分隔
sed 's/foo/bar/; s/baz/qux/' file
# 一组命令对同一个地址生效
sed '/error/{ s/^/[ERR] /; s/$/!/ }' file
写在文件里
复杂的 sed 脚本写在文件里:
# script.sed
s/foo/bar/g
s/old/new/g
/^#/d
sed -f script.sed input.txt
-n + p 选择性输出
sed 默认每行都输出。-n 关闭默认输出,配合 p(print)只输出你要的:
sed -n '5p' file # 只输出第 5 行
sed -n '1,10p' file # 第 1-10 行(等价 head)
sed -n '/error/p' file # 含 "error" 的行(等价 grep)
sed -n '/start/,/end/p' file # 两个标记之间的内容
实际上 grep 干这个比 sed -n /pat/p 方便。但 sed 能干 grep 干不了的"两个标记之间"。
高阶:y 字符转换、G/H/N 多行处理
日常用得少,知道有就行。
echo "hello" | sed 'y/abcdefghij/ABCDEFGHIJ/' # 字符级映射(等价 tr)
# HEllo
sed 'N; s/\n/ /' file # 把每 2 行合成 1 行
多行处理在 sed 里很反人类,遇到要写就直接换 awk 或 perl。
常见踩坑
坑 1:BSD/macOS sed 和 GNU sed 不兼容
最大区别:
sed -i 's/x/y/' file # ❌ macOS 报错:extra characters at the end of i command
sed -i '' 's/x/y/' file # ✅ macOS:必须给后缀(空字符串表示不备份)
跨平台脚本永远写 sed -i.bak + 后续 rm *.bak。或者用更现代的 gsed(macOS 上 brew install gnu-sed)。
坑 2:替换串里的 & 被意外引用
echo "version 1.0" | sed 's/version/[&]/'
# [version] 1.0 ← & 被替换成 "version"
如果你不要 & 的引用:转义 \&。
坑 3:变量带 / 没换分隔符
DIR=/usr/local/bin
sed -i "s/PATH/$DIR/" /etc/profile
# ❌ 报错:unknown option to `s' ← $DIR 里的 / 把 sed 命令断了
修:换分隔符。
sed -i "s|PATH|$DIR|" /etc/profile
坑 4:-i 备份语法搞错
sed -i .bak 's/x/y/' file # ❌ Linux 不认这个(".bak" 被当成文件名)
sed -i.bak 's/x/y/' file # ✅ Linux 正确
sed -i '.bak' 's/x/y/' file # ✅ macOS 正确
注意 .bak 和 -i 之间没空格(Linux),或者有空格但带引号(macOS)。
坑 5:变量里的反斜线 / 双引号被 shell 吃了
PATTERN='^kubelet\s+'
sed "/$PATTERN/d" file # ❌ shell 先把 \s 处理掉
sed '/^kubelet\s+/d' file # ✅ 单引号
或者把变量写更严格:PATTERN='^kubelet[[:space:]]\+'。
坑 6:错把 sed -i 当 sed -n 用
sed -i '/error/p' file # ❌ 把所有行写回(因为没 -n),而且每行打 print 一次
正确:
sed -n '/error/p' file # 只看不写
-i(in-place)和 -n(no default output)是两件事,经常被打错。
坑 7:sed 在管道里输出延迟
类似 grep。要实时输出:
sed -u 's/foo/bar/g' # -u (unbuffered)
# 或 stdbuf -oL sed ...
坑 8:以为 sed 能跨行匹配
sed 's/foo\nbar/X/' file # ❌ 默认每次只处理一行,找不到跨行 foo\nbar
sed 跨行处理很难写(要 N、P、hold space)。有跨行需求直接上 perl 或 awk:
perl -0777 -pe 's/foo\nbar/X/g' file # -0777 把整个文件当一个字符串