Linux服务器如何批量修改配置文件?sed、awk与正则表达式实战

多台服务器的配置文件要改同一个参数,是运维里反复出现的场景:换数据库地址、调整连接池大小、统一日志级别、批量改 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。