这个命令是干啥的
sed 是 stream editor 的缩写,流式编辑器。它的核心能力是逐行处理文本,支持查找替换、插入删除、行号定位等各种操作。最常用的场景是文本批量替换。
我第一次接触 sed 是被要求把线上几百个配置文件里的旧域名全部改成新域名。手动改是不可能的,用 sed 一行命令就搞定了。从此我就爱上了这个命令。
sed 的工作方式是这样的:它读取文件的一行,执行你给的命令,输出处理后的结果。默认不会修改原文件,只输出到终端。想改原文件得加 -i 参数。
基本用法(3分钟上手)
最简单的替换
# 把文件里的 old 替换成 new,输出到终端
sed 's/old/new/' file.txt
# 替换所有匹配(不加 g 只替换每行第一个)
sed 's/old/new/g' file.txt
# 忽略大小写替换
sed 's/old/new/gi' file.txt
s/old/new/g 是 sed 最经典的用法。s 是 substitute(替换),g 是 global(全局替换)。不加 g 的话,每行只替换第一个匹配。
修改原文件(-i)
# 修改原文件(覆盖保存)
sed -i 's/old/new/g' file.txt
# 修改前先备份(生成 file.txt.bak)
sed -i.bak 's/old/new/g' file.txt
-i 后面可以直接跟备份后缀,比如 -i.bak 会在修改前把原文件备份成 file.txt.bak。我第一次用 sed 做批量替换的时候没加备份,结果替换错了,想恢复都恢复不了。加个 .bak 后缀,安全有保障。
进阶骚操作
1. 行号定位
# 只替换第3行的 old
sed '3s/old/new/g' file.txt
# 替换第5行到第10行的 old
sed '5,10s/old/new/g' file.txt
# 替换第5行到最后一行的 old
sed '5,$s/old/new/g' file.txt
行号定位在修改配置文件时非常有用。比如我只想修改配置文件的第 12 行端口号,不想动其他地方。
2. 范围匹配
# 从匹配 "BEGIN" 的行开始到匹配 "END" 的行结束,替换中间的 old
sed '/BEGIN/,/END/s/old/new/g' file.txt
# 从匹配 "[database]" 的行开始到空行结束
sed '/^\[database\]/,/^$/s/port=3306/port=3307/' config.ini
范围匹配非常强大。我以前在处理配置文件时,同一个配置项可能在不同 section 出现多次,用范围匹配就能精确控制只在特定 section 内替换。
3. 删除行
# 删除第3行
sed '3d' file.txt
# 删除第5到第10行
sed '5,10d' file.txt
# 删除空行
sed '/^$/d' file.txt
# 删除包含注释的行
sed '/^#/d' config.conf
# 删除匹配 "DEBUG" 的行
sed '/DEBUG/d' app.log
d 是 delete。删除空行和注释行是我处理配置文件时最常用的。比如把 sshd_config 里的注释行和空行都删掉,只看有效配置:
sed -i '/^#/d; /^$/d' /etc/ssh/sshd_config
4. 在指定行前后插入
# 在第3行之前插入一行
sed '3i\这是插入的内容' file.txt
# 在第3行之后插入一行
sed '3a\这是追加的内容' file.txt
# 在匹配 "server {" 的行之前插入
sed '/server {/i\ listen 443 ssl;' nginx.conf
i 是 insert(之前插入),a 是 append(之后追加)。修改 nginx 配置文件的时候,在特定 server 块里插入 listen 指令特别好用。
5. 对匹配的行执行多条命令
# 对包含 "ERROR" 的行执行替换和删除
sed '/ERROR/{
s/ERROR/WARNING/g
s/CRITICAL/HIGH/g
}' app.log
# 或者用分号分隔
sed '/ERROR/{s/ERROR/WARNING/g; s/CRITICAL/HIGH/g}' app.log
6. 打印特定行
# 打印第10行
sed -n '10p' file.txt
# 打印第20到第30行
sed -n '20,30p' file.txt
# 打印包含 "root" 的行
sed -n '/root/p' /etc/passwd
-n 参数加上 p 命令相当于 grep 的增强版。-n 关闭默认输出(不打印所有行),p 命令只打印匹配的行。sed -n '20,30p' 可以查看文件的某一段,比 head 和 tail 组合着用方便。
避坑指南
坑1:macOS 的 -i 和 Linux 不一样
这是最大的坑。macOS 的 sed 和 GNU sed 行为不同:
# Linux (GNU sed) - 可以
sed -i 's/old/new/g' file.txt
# macOS (BSD sed) - 这样不行!会报错
sed -i 's/old/new/g' file.txt
# macOS 必须给 -i 一个参数(哪怕为空)
sed -i '' 's/old/new/g' file.txt # macOS
sed -i.bak 's/old/new/g' file.txt # macOS 和 Linux 都兼容
macOS 的 BSD sed 要求 -i 后面必须跟一个参数(备份后缀)。如果想不生成备份文件,得写成 sed -i '' 's/old/new/g' file.txt。而 Linux 的 GNU sed 允许 -i 后面不跟参数。写跨平台脚本时,我统一用 -i.bak 这样两边都兼容。
坑2:替换路径分隔符
# 替换路径时可能出问题
sed 's/\/usr\/local\/nginx/\/opt\/nginx/g' file.txt
# 用其他分隔符更清晰
sed 's#/usr/local/nginx#/opt/nginx#g' file.txt
sed 's|/usr/local/nginx|/opt/nginx|g' file.txt
替换路径的时候,用 # 或 | 做分隔符比转义反斜杠清爽多了。我习惯用 #,少打很多字符。
坑3:正则里的特殊字符
# 替换 IP 地址,. 在正则里匹配任意字符
sed 's/192.168.1.1/10.0.0.1/g' file.txt
# 这会误匹配 "192x168y1z1"
# 正确做法:转义
sed 's/192\.168\.1\.1/10\.0\.0\.1/g' file.txt
# 或者用 -E 配合转义
sed -E 's/192\.168\.1\.1/10\.0\.0\.1/g' file.txt
. 在正则里是通配符,匹配任意字符。替换 IP 地址或域名时,别忘转义。
坑4:sed 不会自动备份巨大文件
# 这个会直接修改原文件,没有回滚机会
sed -i 's/重要数据/错误数据/g' hugefile.txt
默认 -i 不会备份。即使加了 -i.bak,如果文件巨大,备份过程也占用大量磁盘空间。我的建议是:
# 先用 -n 预览替换效果
sed -n 's/旧内容/新内容/gp' file.txt
# 确认无误后再 -i
sed -i.bak 's/旧内容/新内容/g' file.txt
-n 's/.../.../gp' 只会显示被替换的行,可以直观确认替换效果对不对。
坑5:sed 的多行匹配
# sed 默认逐行处理,不能匹配跨行的模式
# 比如想把两行的 "foo\nbar" 替换成 "baz" 就做不到
如果需要跨行匹配,可以用 awk 或者 perl。perl -i -pe 's/foo\nbar/baz/' 可以处理换行符。
实战场景(重点!结合真实运维场景)
场景1:批量修改配置文件
线上 50 台服务器,需要把所有 Nginx 配置文件里的旧域名改成新域名:
#!/bin/bash
# 批量替换 Nginx 配置中的域名
OLD_DOMAIN="old.example.com"
NEW_DOMAIN="new.example.com"
NGINX_DIR="/etc/nginx"
# 先查看有哪些文件包含旧域名
echo "=== 将要修改的文件列表 ==="
grep -rl "$OLD_DOMAIN" "$NGINX_DIR" --include="*.conf"
echo ""
echo "=== 是否继续?(y/n) ==="
read -r confirm
if [ "$confirm" = "y" ]; then
# 批量替换,加 .bak 备份
find "$NGINX_DIR" -name "*.conf" -exec \
sed -i.bak "s/$OLD_DOMAIN/$NEW_DOMAIN/g" {} \;
# 验证替换结果
echo "=== 替换后的结果检查 ==="
grep -r "$NEW_DOMAIN" "$NGINX_DIR" --include="*.conf" | head -10
echo ""
echo "=== 检查是否还有漏网的旧域名 ==="
grep -r "$OLD_DOMAIN" "$NGINX_DIR" --include="*.conf" || echo "全部替换完成"
fi
这个脚本我每换一次域名就跑一遍。关键步骤:先用 grep -rl 确认要改哪些文件,再用 find + sed 批量替换,最后再用 grep 双重验证。
场景2:日志脱敏
生产环境的日志里可能包含敏感信息,需要脱敏后再分享:
#!/bin/bash
# 日志脱敏脚本
INPUT_LOG="/var/log/myapp/app.log"
OUTPUT_LOG="/tmp/app_desensitized.log"
# 复制一份日志
cp "$INPUT_LOG" "$OUTPUT_LOG"
# 脱敏 IP 地址(保留最后一段)
sed -i 's/[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}\.[0-9]\{1,3\}/xxx.xxx.xxx.&/g' "$OUTPUT_LOG"
# 脱敏手机号
sed -i 's/1[3-9][0-9]\{9\}/138xxxx0000/g' "$OUTPUT_LOG"
# 脱敏邮箱
sed -i 's/[a-zA-Z0-9._%+-]\+@[a-zA-Z0-9.-]\+\.[a-zA-Z]\{2,\}/redacted@email.com/g' "$OUTPUT_LOG"
# 脱敏密码字段
sed -i 's/password=[^&]*/password=******/g' "$OUTPUT_LOG"
echo "脱敏完成,输出文件:$OUTPUT_LOG"
场景3:快速修改 Docker Compose 配置
#!/bin/bash
# 修改 docker-compose.yml 中的镜像版本
# 把 myapp 的镜像版本从 latest 改成 v2.1.0
sed -i '/myapp:/,/^[^ ]/{s/image: myapp:.*/image: myapp:v2.1.0/}' docker-compose.yml
# 范围解释:
# '/myapp:/' 匹配到包含 "myapp:" 的行
# ',/^[^ ]/' 到这个块的下一个顶格行
# 在上述范围内执行替换
场景4:清理配置文件中的空行和注释
#!/bin/bash
# 清理 nginx 配置文件,生成纯净版
CONFIG_FILE="/etc/nginx/nginx.conf"
CLEAN_FILE="/tmp/nginx_clean.conf"
# 删除空行和注释行,并压缩连续空行
sed \
-e '/^#/d' \
-e '/^[[:space:]]*#/d' \
-e '/^$/d' \
-e '/^[[:space:]]*$/d' \
"$CONFIG_FILE" > "$CLEAN_FILE"
echo "原始配置 $(wc -l < "$CONFIG_FILE") 行 -> 清理后 $(wc -l < "$CLEAN_FILE") 行"
场景5:批量替换多个文件中的版本号
项目发布时,需要把多个文件里的版本号统一更新:
#!/bin/bash
# 版本号批量更新脚本
OLD_VERSION="1.2.3"
NEW_VERSION="2.0.0"
FILES=(
"package.json"
"README.md"
"src/version.ts"
"deploy/docker-compose.yml"
)
echo "=== 版本号更新:$OLD_VERSION -> $NEW_VERSION ==="
for file in "${FILES[@]}"; do
if [ -f "$file" ]; then
# 先看看匹配情况
count=$(grep -c "$OLD_VERSION" "$file")
echo "文件 $file: 匹配到 $count 处"
# 加备份进行替换
sed -i.bak "s/$OLD_VERSION/$NEW_VERSION/g" "$file"
fi
done
echo "=== 更新完成 ==="
echo "备份文件列表:"
find . -name "*.bak" -exec echo " 备份: {}" \;
这个脚本我每次发版都要用。以前手动改 5 个文件经常漏改某个文件,用脚本跑一遍心里踏实。
今日作业
你的 Nginx 配置文件 /etc/nginx/sites-enabled/default 里有以下内容(示例):
server {
listen 80;
server_name old-site.com;
root /var/www/old-site;
location / {
proxy_pass http://127.0.0.1:8080;
}
}
请写一条 sed 命令(不要备份)完成以下改动:
1. 把 listen 80; 改成 listen 443 ssl;
2. 把 server_name old-site.com; 改成 server_name new-site.com;
3. 把 root /var/www/old-site; 改成 root /var/www/new-site;
要求:全部用一条命令完成,用范围匹配保证只修改这个 server 块(如果有多个 server 块,只改第一个)。
提示:用 -e 参数串联多条替换命令,或者分号 ; 连接多个替换。用 1,/^server/ 或行号范围控制修改范围。