多台服务器的配置文件要改同一个参数,是运维里反复出现的场景:换数据库地址、调整连接池大小、统一日志级别、批量改 nginx 的 upstream。手改的话,机器一多就注定出错——漏改一台、改错一个字符、tab 和空格不一致,排查起来比重改还费时间。
这篇文章讲怎么用 sed、awk 配合正则表达式,安全地批量修改配置文件。内容包括:两个工具各自的定位、正则表达式的关键语法、常见的"改一行/改一个键值"实战写法、以及怎么做到"改之前先备份、改之后可验证"。
文中所有涉及修改文件的命令,都会强调先备份、先预览、再落盘。sed -i 直接改原文件,一旦正则写错,轻则该行被改乱,重则整个配置报废,所以每个命令我都会给出不带 -i 的预演版本。文中出现的路径都是真实常见路径,请按自己环境替换。
一、sed 与 awk 的分工
先把两者的定位说清楚,用错了工具会很别扭。
sed(Stream Editor)是"流编辑器",最擅长按行做替换、删除、插入。它的经典用法是"找到匹配某模式的行,把它替换成别的"。改写配置文件里的某个参数值、注释掉一行、在某行后追加内容,都是它的主场。
awk 是"文本处理语言",最擅长按列/字段处理数据和做逻辑判断。它能计算、能按条件输出整行或部分字段、能处理多字段的结构化文本。当修改逻辑涉及"根据第三列的值决定怎么改第二列"这类条件时,awk 更合适。
| 维度 | sed | awk |
|---|---|---|
| 核心能力 | 按行替换/删除/插入 | 按字段处理+逻辑运算 |
| 处理单位 | 行 | 行 + 列(字段) |
| 条件逻辑 | 支持但较弱 | 强大,接近编程语言 |
| 典型任务 | 改键值、注释行、替换文本 | 按列提取、条件改写、统计 |
| 原地修改 | sed -i |
需 > tmp && mv |
| 学习曲线 | 正则要熟 | 语法稍多,但直观 |
| 擅长场景 | 单参数替换 | 多字段、带条件的改写 |
一句话选型:只想"改某行里的某段文字",用 sed;要"按列判断再改写",用 awk。
二、正则表达式速成
两个工具的能力都建立在正则之上,不熟正则会写出很多"看着能跑其实会误伤"的命令。下面把最常用的元字符和它们在配置文件场景里的含义列清楚。
| 元字符 | 含义 | 示例 | 匹配 |
|---|---|---|---|
^ |
行首 | ^listen |
以 listen 开头的行 |
$ |
行尾 | ;}$ |
以 ;} 结尾的行 |
. |
任意单字符 | l.g |
lag、l1g 等 |
* |
前一项 0+ 次 | ab*c |
ac、abc、abbc |
\+ |
前一项 1+ 次(BRE) | ab\+c |
abc、abbc |
? |
前一项 0 或 1 次(ERE) | colou?r |
color、colour |
[] |
字符集 | [0-9] |
任意单个数字 |
[^] |
取反字符集 | [^#] |
非 # 的字符 |
\{n,m\} |
重复 n 到 m 次 | [0-9]\{1,3\} |
1 到 3 位数字 |
\(\) |
分组(BRE) | \(ab\)\+ |
ab 重复 |
\| |
或(BRE) | cat\|dog |
cat 或 dog |
\s |
空白(GNU 扩展) | \s\+ |
一个及以上空白 |
有两个高频坑必须记住。第一,sed 默认用 BRE(基本正则),+、?、|、() 都需要转义或加 -E 才按扩展语义工作。用 sed -E 可以直接写 ERE,可读性好得多。第二,. 会匹配任意字符,配置里写 IP、版本号时如果直接写 192.168.1.1,那个 . 会误伤 192x168y1z1 这类内容,正确写法是把它转义成 192\.168\.1\.1。
三、sed 实战:改键值、注释、追加
下面是配置文件里最常遇到的几种改法。注意:每条都先给不带 -i 的预览版,确认输出无误后再加 -i 落盘。
# 场景1:改键值——把 max_connections = 100 改成 200
# 预览(只输出到屏幕,不改文件)
sed -E 's/^([[:space:]]*max_connections[[:space:]]*=[[:space:]]*).*/\1200/' /etc/app/app.conf
# 确认无误后原地修改,并先备份
cp -a /etc/app/app.conf /etc/app/app.conf.bak.$(date +%F)
sed -i -E 's/^([[:space:]]*max_connections[[:space:]]*=[[:space:]]*).*/\1200/' /etc/app/app.conf
# 场景2:注释掉某行(前面加 #)
sed -i -E 's/^([[:space:]]*)(listen[[:space:]]+80)/\1# \2/' /etc/nginx/conf.d/site.conf
# 场景3:取消注释(去掉行首的 # 和后面的空格)
sed -i -E 's/^#[[:space:]]*(server_tokens)/\1/' /etc/nginx/nginx.conf
# 场景4:在匹配行之后追加一行
sed -i '/^\[mysqld\]/a default-time-zone = "+08:00"' /etc/my.cnf
# 场景5:删除包含某个关键字的行(危险,务必先预览)
sed -n '/^#.*old_upstream/p' /etc/nginx/nginx.conf # 先看会删哪些
关于 sed -i 有个重要细节:在 macOS 上 sed -i 必须带备份后缀(sed -i ''),而 Linux 上可省略。我们的场景是 Linux 服务器,直接 -i 即可。另一个细节是 GNU sed 支持 -i.bak 直接生成备份,这比事后手动 cp 更省事:
# GNU sed(Linux)自带备份的写法
sed -i.bak -E 's/^listen 80;/listen 8080;/' /etc/nginx/conf.d/site.conf
# 会生成 site.conf.bak
正则里用 \1 引用分组,是把"匹配到的前缀"保留下来、只替换后半段的关键技巧。以场景1为例,表达式先捕获"空白 + 键名 + 空白 + = + 空白",再用 \1 原样放回,只把后面的值换成 200。这样无论原来值是多少、等号两边有多少空格,都能稳妥替换。
四、awk 实战:按列与条件改写
当修改逻辑需要"按列判断"时,sed 就开始勉强了,awk 更自然。awk 修改文件的惯用法是:输出到临时文件,校验通过后再覆盖原文件。
# 场景1:把某配置文件第 2 列的值统一改成新地址(保留其他列)
# 假设文件格式: host=db01 addr=10.0.0.5 port=3306
awk 'BEGIN{OFS=" "} {if($2 ~ /^addr=/){$2="addr=10.0.0.9"} print}' config.list > /tmp/config.new
diff config.list /tmp/config.new # 先看差异,确认只改了该改的
# 确认无误后覆盖
cp -a config.list config.list.bak && mv /tmp/config.new config.list
# 场景2:只处理匹配特定段落的内容(如 [mysqld] 段)
awk '/^\[mysqld\]/{insec=1} /^\[/{if($0!~/^\[mysqld\]/) insec=0}
insec && /^port[[:space:]]*=/ {$0="port = 3307"} {print}' /etc/my.cnf > /tmp/my.cnf.new
diff /etc/my.cnf /tmp/my.cnf.new
# 场景3:给满足条件的行加前缀(如给所有非注释的 logger 行加 #)
awk '{if($0 !~ /^#/ && $0 ~ /^logger/){print "# " $0} else {print}}' app.conf > /tmp/app.new
awk 的结构是 模式 { 动作 }。$0 是整行,$1、$2 是各列,NF 是列数,OFS 是输出分隔符。掌握这几个符号,就能写出"按条件改写某列"这类 sed 做起来别扭的逻辑。
五、把批量修改做成脚本
真实场景里要改的是多台机器,核心诉求是"安全、可重复、可回滚"。下面这份脚本把这些原则落到了实处。
#!/usr/bin/env bash
# 批量安全修改配置:备份 -> 预览 -> 修改 -> 校验
set -uo pipefail
TARGET_FILE="${1:?用法: $0 <配置文件路径>}"
BACKUP_DIR="/var/backups/conf"
TS=$(date +%Y%m%d-%H%M%S)
BACKUP="${BACKUP_DIR}/$(basename "$TARGET_FILE").${TS}"
# 1) 备份
mkdir -p "$BACKUP_DIR"
cp -a "$TARGET_FILE" "$BACKUP"
echo "[OK] 已备份到 $BACKUP"
# 2) 预览改动(不落盘)
echo "=== 改动预览(- 原 / + 新)==="
sed -E 's/^([[:space:]]*max_connections[[:space:]]*=[[:space:]]*).*/\1200/' "$TARGET_FILE" \
| diff "$TARGET_FILE" - || true
# 3) 询问确认(交互;无人值守场景可去掉,改为先跑一次 dry-run)
read -r -p "确认应用改动?(yes/no) " ans
[ "$ans" = "yes" ] || { echo "已取消"; exit 0; }
# 4) 应用
sed -i -E 's/^([[:space:]]*max_connections[[:space:]]*=[[:space:]]*).*/\1200/' "$TARGET_FILE"
# 5) 语法校验(以 nginx 为例;其他服务换成对应的 -t 命令)
if command -v nginx >/dev/null; then
if nginx -t >/dev/null 2>&1; then
echo "[OK] nginx 配置语法正确"
else
echo "[FAIL] 语法错误,回滚!"
cp -a "$BACKUP" "$TARGET_FILE"
exit 1
fi
fi
echo "[DONE] 修改完成;如需回滚: cp -a $BACKUP $TARGET_FILE"
这段脚本体现了三个关键实践。备份到统一目录并带时间戳,而不是散落在原目录,便于集中管理和保留历史。先 diff 预览再确认,用 diff 比肉眼看 sed 输出更直观。改后做语法校验,失败自动回滚——对 nginx、apache、sshd 这些支持 -t 或类似校验的服务,这一步能挡住绝大部分"改完服务起不来"的灾难。
各类服务的配置校验命令:
nginx -t # nginx 配置测试
apachectl configtest # Apache(RHEL系:httpd -t)
sshd -t # SSH 服务端配置语法测试
visudo -c # sudoers 语法测试(务必用这个)
systemd-analyze verify /etc/systemd/system/xxx.service # systemd 单元校验
六、跨主机批量修改
单机改完,推广到多机。最轻量的是用 for 循环配合 SSH,但要注意先备份、先校验。
#!/usr/bin/env bash
# 在多台主机上批量修改配置(需已配置 SSH 免密)
set -uo pipefail
HOSTS=(web01 web02 web03)
CONF=/etc/nginx/conf.d/site.conf
for h in "${HOSTS[@]}"; do
echo "==== $h ===="
ssh -o ConnectTimeout=5 -o BatchMode=yes "$h" bash -s <<'REMOTE'
set -e
CONF=/etc/nginx/conf.d/site.conf
cp -a "$CONF" "$CONF.bak.$(date +%F)"
sed -i -E 's/^([[:space:]]*listen[[:space:]]+)80;/\18080;/' "$CONF"
if nginx -t >/dev/null 2>&1; then
systemctl reload nginx && echo "[OK] reloaded"
else
echo "[FAIL] 语法错误,回滚"; cp -a "$CONF.bak."* "$CONF"; exit 1
fi
REMOTE
done
如果机器不止几台,强烈建议改用 Ansible。它自带的 lineinfile、replace、blockinfile 模块天然幂等、支持批量校验与回滚,比裸写 SSH 循环稳得多。Shell 方案适合几台到十几台的轻量场景。
七、风险提示与回滚
第一,正则写错是最常见的事故源。 写 sed 时务必先跑不带 -i 的版本,用眼睛看输出,或者配合 diff 看差异。特别是带 . 和 * 的模式,很容易误伤。
第二,sed -i 会重建文件。 它实际是"写临时文件再改名覆盖",这会改变文件的 inode、属主属组和权限(在某些实现下)。对权限敏感的文件(如 /etc/shadow、/etc/sudoers),改完要用 ls -l 和 namei -l 确认权限没变。更安全的做法是先用 diff 校验内容,必要时手动 chmod/chown 恢复。
第三,selinux 上下文。 在启用了 SELinux 的 RHEL 系上,sed -i 重建文件可能导致安全上下文丢失,进而让服务访问文件被拒。用 ls -Z 对比改前改后,必要时用 restorecon -v 文件 恢复上下文。
第四,服务重载前先校验。 任何改完配置的操作,落盘后必须 nginx -t、sshd -t 之类校验,通过再 reload(不是 restart,reload 更平滑);校验失败立即回滚。
回滚方式:因为每次修改前都用 cp -a 做了带时间戳的备份(或 sed -i.bak 生成 .bak),回滚就是把备份文件覆盖回去:cp -a /var/backups/conf/xxx.时间戳 /原路径,然后重新校验并 reload。
常见问题
Q1:sed -i 和 sed 有什么区别?
不带 -i 的 sed 只把结果输出到屏幕,不改原文件,是安全的"预览";-i 表示原地修改(in-place),直接把结果写回原文件。养成"先不带 -i 看结果,再带 -i 落盘"的习惯,能避免绝大多数误操作。
Q2:sed 报 invalid command code 或正则不生效,为什么?
最常见原因是正则语法不对:BRE 里 +、?、|、() 需要转义,或者干脆加 -E 用扩展正则。另一个常见原因是分隔符冲突——如果替换内容里含 /,把分隔符换成别的字符(如 s#...#...#)能避免转义地狱。
Q3:改完配置服务起不来了怎么办?
先看服务的语法校验命令(nginx -t、sshd -t 等)报什么错,然后立刻用备份回滚:cp -a 备份 原文件,再 systemctl reload(或 restart)。这就是为什么改前必须备份——有备份,回滚只需几秒;没备份,可能要重装服务甚至重建系统。
Q4:awk 修改文件为什么不支持 -i?
awk 本身是"读取-处理-输出"的流工具,设计上不修改输入文件,所以要用重定向写到临时文件、再 mv 覆盖。gawk 有 -i inplace 扩展,但可移植性差,标准写法还是"输出到 tmp 再 mv"。
Q5:配置文件里的缩进/空格不一致,正则怎么写更稳?
用 [[:space:]]* 匹配任意空白(含 tab 和空格),而不是写死几个空格或 \t。上面场景1的表达式 ^([[:space:]]*key[[:space:]]*=[[:space:]]*) 就是为兼容各种缩进风格设计的,能匹配 key=1、key = 1、\tkey\t=\t1 等写法。
总结
- 选型先看任务:改某行里的某段文字用
sed,按列判断再改写用awk。 - 正则的两个高频坑是
.会匹配任意字符、BRE 里+?|()需转义;用sed -E可读性更好。 - 改键值用"分组 +
\1保留前缀"的写法最稳,能兼容不同缩进和空格。 - 任何修改都遵循"先备份 → 不带
-i预览 →diff确认 → 落盘 → 语法校验 → 必要时 reload",这条链条是安全的根本。 - 校验失败立即从带时间戳的备份回滚;
sed -i会重建文件,注意权限和 SELinux 上下文;多机场景优先考虑 Ansible。
企业QQ咨询




