1. 引言:为什么想让它写 Shell 脚本

  • 背景:日常运维/部署脚本重复劳动多,想用 AI 提效
  • 预期:一句话描述需求,直接拿到可运行脚本
  • 现实:脚本能跑,但跑出了「意料之外的结果」
  • 本文定位:不是黑 AI,而是记录真实翻车点与修正方法

2. 实验环境与任务设定

  • 环境说明:操作系统、Shell 版本(bash 5.x)、Claude Code 使用方式
  • 任务目标:写一个「日志清理 + 备份 + 定时执行」的运维脚本
  • 需求描述原文(贴出给 AI 的 prompt)
  • 验收标准:幂等、可回滚、出错即停、有日志

3. 翻车现场一:rm -rf 的路径拼接陷阱

  • 现象:清理目录时误删范围超出预期
  • 根因:变量未加引号 + 空变量导致路径退化为根目录
  • 错误代码片段与执行后果
  • 修正方案:set -u、"${VAR}" 引号、路径白名单校验
  • 小结:AI 生成的删除类命令必须人工复核

4. 翻车现场二:set -e 并没有想象中可靠

  • 现象:脚本中途失败却继续执行后续步骤
  • 根因:管道、if 条件、子 shell 中 set -e 失效
  • 错误示例与真实行为对比
  • 修正方案:set -euo pipefail + 显式错误处理
  • 小结:错误处理要显式,不能只靠一行开关

5. 翻车现场三:变量与通配符的「隐形展开」

  • 现象:文件名带空格时循环处理出错
  • 根因:for f in $(ls) 的经典反模式
  • 修正方案:find -print0 + while IFS= read -r -d ''
  • 代码对比:错误写法 vs 正确写法
  • 小结:Shell 的分词规则是 AI 最容易忽略的细节

6. 翻车现场四:时间与日期计算的边界情况

  • 现象:跨月/跨年时备份文件名冲突或计算错误
  • 根因:手写日期运算、时区未统一
  • 修正方案:统一使用 date -d/date -v 或交由 find -mtime
  • 小结:时间逻辑优先用系统工具,不要自己算

7. 翻车现场五:并发与锁缺失导致重复执行

  • 现象:定时任务重叠执行,备份文件互相覆盖
  • 根因:没有加锁机制
  • 修正方案:flock 文件锁 + 幂等设计
  • 代码示例:带锁的脚本骨架
  • 小结:定时脚本必须考虑重入问题

8. 我是如何系统排查这些问题的

  • 排查方法论:bash -x、shellcheck、set -n 语法检查
  • 最小复现:把可疑片段抽出来单独跑
  • 沙箱验证:在容器/临时目录中先跑一遍
  • 流程图:从「AI 生成」到「上线」的验证链路

是

否

否

是

向 Claude Code 描述需求

生成初版脚本

shellcheck 静态检查

有告警?

人工修正并追问 AI

沙箱 dry-run 验证

结果符合预期?

灰度上线 + 日志观察

9. 让 Claude Code 少翻车的 Prompt 技巧

  • 明确声明 Shell 与版本,要求 set -euo pipefail
  • 要求「先给方案再给代码」,便于提前发现思路错误
  • 要求标注危险操作并给出回滚方式
  • 要求输出 shellcheck 友好的代码
  • 让 AI 自检:请它列出脚本的潜在风险点

10. 一份可复用的脚本模板

  • 模板结构:参数解析、日志函数、锁、错误处理、主流程
  • 完整代码示例(bash)
  • 使用说明与注意事项

下面这份模板把前面五个翻车点对应的防护措施都固化了下来,可以直接复制到 ops_template.sh 里改主流程使用。

#!/usr/bin/env bash
# ============================================================
# ops_template.sh —— 可复用的运维脚本模板
# 用法: ./ops_template.sh -s /data/logs -d /data/backup -r 7 [-n]
# ============================================================

# ---------- 0. 错误处理:出错即停、未定义变量报错、管道失败可见 ----------
set -euo pipefail
# -e 任一命令失败立即退出;-u 使用未定义变量报错;-o pipefail 管道中任一环节失败即失败
IFS=$'\n\t'   # 收紧分词,避免文件名中的空格被拆开

# ---------- 1. 参数解析:显式声明默认值 + 白名单校验 ----------
SRC_DIR=""          # 待清理的日志目录
DST_DIR=""          # 备份输出目录
KEEP_DAYS=7         # 保留天数
DRY_RUN=0           # 1 表示只打印不执行

usage() {
  cat <<'EOF'
用法: ops_template.sh -s <源目录> -d <备份目录> [-r 保留天数] [-n]
  -s  源目录(必填,必须存在且非根目录)
  -d  备份目录(必填)
  -r  保留天数,默认 7
  -n  dry-run,只打印将要执行的操作
EOF
}

while getopts ":s:d:r:nh" opt; do
  case "$opt" in
    s) SRC_DIR="$OPTARG" ;;
    d) DST_DIR="$OPTARG" ;;
    r) KEEP_DAYS="$OPTARG" ;;
    n) DRY_RUN=1 ;;
    h) usage; exit 0 ;;
    \?) echo "未知参数: -$OPTARG" >&2; usage; exit 2 ;;
    :)  echo "参数 -$OPTARG 缺少取值" >&2; exit 2 ;;
  esac
done

# 必填校验 + 危险路径白名单(对应翻车现场一)
[[ -n "$SRC_DIR" && -n "$DST_DIR" ]] || { usage; exit 2; }
[[ -d "$SRC_DIR" ]] || { echo "源目录不存在: $SRC_DIR" >&2; exit 2; }
[[ "$SRC_DIR" != "/" && "$SRC_DIR" != "." ]] || { echo "拒绝操作根目录" >&2; exit 2; }
[[ "$KEEP_DAYS" =~ ^[0-9]+$ ]] || { echo "保留天数必须是整数" >&2; exit 2; }

# ---------- 2. 日志函数:统一格式、同时落盘 ----------
LOG_FILE="${DST_DIR%/}/ops_$(date +%Y%m%d).log"
mkdir -p "$DST_DIR"

log() {
  local level="$1"; shift
  printf '[%s] [%-5s] %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$level" "$*" | tee -a "$LOG_FILE"
}
log_info()  { log "INFO"  "$@"; }
log_warn()  { log "WARN"  "$@"; }
log_error() { log "ERROR" "$@" >&2; }

# 统一错误出口:任何未捕获错误都走这里,便于排查
trap 'log_error "脚本在第 $LINENO 行异常退出,退出码 $?"' ERR

# ---------- 3. flock 文件锁:防止定时任务重叠执行 ----------
LOCK_FILE="/var/lock/$(basename "$0").lock"
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
  log_warn "已有实例在运行,本次跳过"
  exit 0
fi
# 文件描述符 9 上的锁随进程退出自动释放,无需手动解锁

# ---------- 4. 主流程:备份 -> 清理 -> 汇总 ----------
main() {
  local stamp; stamp="$(date +%Y%m%d_%H%M%S)"
  local archive="${DST_DIR%/}/logs_${stamp}.tar.gz"

  log_info "开始执行,源目录=$SRC_DIR 备份目录=$DST_DIR 保留天数=$KEEP_DAYS dry_run=$DRY_RUN"

  # 4.1 备份:先打包再清理,保证可回滚
  if [[ "$DRY_RUN" -eq 1 ]]; then
    log_info "[dry-run] 将打包 $SRC_DIR -> $archive"
  else
    tar -czf "$archive" -C "$SRC_DIR" . || { log_error "打包失败"; exit 1; }
    log_info "备份完成: $archive"
  fi

  # 4.2 清理:用 find -mtime 交给系统算时间,避免手写日期运算
  local count=0
  while IFS= read -r -d '' f; do
    if [[ "$DRY_RUN" -eq 1 ]]; then
      log_info "[dry-run] 将删除 $f"
    else
      rm -f -- "$f"
    fi
    count=$((count + 1))
  done < <(find "$SRC_DIR" -type f -name '*.log' -mtime "+$KEEP_DAYS" -print0)

  log_info "清理完成,共处理 $count 个过期文件"
  log_info "全部任务结束"
}

main "$@"

使用说明

  • 保存为 ops_template.sh,执行 chmod +x ops_template.sh 赋予执行权限。
  • 先跑一次 dry-run 确认范围:./ops_template.sh -s /data/logs -d /data/backup -r 7 -n。
  • 确认无误后去掉 -n 正式执行;日志会同时输出到终端和 $DST_DIR/ops_YYYYMMDD.log。
  • 挂定时任务时建议加 flock 兜底:*/30 * * * * flock -n /tmp/ops.lock /path/ops_template.sh -s /data/logs -d /data/backup。
  • 需要扩展时只改 main() 里的 4.1/4.2 两段,参数解析、日志、锁、错误处理都不用动。

注意事项

  • 锁文件路径:/var/lock 需要写权限,普通用户可改为 $HOME/.cache/ops.lock 或 /tmp 下带用户名的路径。
  • set -e 的盲区:if 条件、&&/|| 左侧、命令替换中的失败不会触发退出,关键步骤仍要显式判断返回值。
  • 删除操作:rm -f -- "$f" 中的 -- 防止文件名以 - 开头被当成选项;find -print0 + read -d '' 保证带空格、换行的文件名不被拆开。
  • 备份与清理顺序:务必先备份成功再清理,tar 失败时脚本会因 set -e 直接退出,不会误删。
  • 磁盘空间:打包前建议加一次 df 检查,避免备份把磁盘写满导致后续任务连锁失败。
  • 时区:date 与 find -mtime 都依赖系统时区,容器环境要确认 TZ 一致,否则「保留 7 天」可能偏差一天。
  • AI 生成后必做:shellcheck ops_template.sh 过一遍,再在临时目录里 dry-run 验证,最后才上生产。

11. 总结:AI 写脚本的正确姿势

  • AI 擅长:样板代码、命令组合、思路启发
  • AI 不擅长:边界条件、环境差异、破坏性操作
  • 核心原则:AI 生成 + 人工审查 + 沙箱验证
  • 一句话结论:把 AI 当「高级自动补全」,而不是「免审运维工程师」
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐