Claude Code 写 Shell 脚本翻车实录:从自信满满到逐行排雷
·
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 生成」到「上线」的验证链路
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 当「高级自动补全」,而不是「免审运维工程师」
更多推荐

所有评论(0)