---
title: "脚本编写"
description: "把每周重复的评审、整理、批处理写成 shell 脚本：四段式骨架、用退出码和结构化输出做判断、限流重试，以及权限与成本的边界。"
updated: 2026-11-13
canonical: https://docai.cc/learn/scripting-with-claude-code/
markdown_mirror: https://docai.cc/learn/scripting-with-claude-code.md
---
## 先问值不值得固化

每周整理周报，开对话、贴 diff、复制结果，十分钟。做到第四次，就该考虑写成脚本。判断标准三条：步骤固定、输入能列举、结果有客观验收方式。缺一条都先别急。

只做过一次的任务，手动跑通再说。你会看到它在哪里出岔子，哪些判断必须由人来做。反过来，一周只跑一次、每次输入都不同的活，写脚本的维护成本高于收益。

脚本只放大流程，不替你定规则。把模型看成一个工位：前面有人备料，后面有人验收。它擅长分类、提取、改写、判断，不擅长替你拍板。

## 四段式骨架

```bash
#!/usr/bin/env bash
set -euo pipefail

input="${1:?用法: review.sh <文件>}"
[ -f "$input" ] || { echo "文件不存在: $input" >&2; exit 2; }

draft=$(claude -p "审查这段代码的并发问题，按严重程度排序，每条给出位置和建议" < "$input")
printf '%s\n' "$draft" > "${input%.*}.review.md"
```

四段：校验参数、取输入、交给模型、保存结果。每段都短，坏了容易定位。别把全部逻辑压进一条管道，出错时你连哪一步坏的都说不清。

输入最好用绝对路径，或者先切到固定目录。脚本被定时任务调用时，工作目录往往不是你以为的那个。

## 让脚本自己判断

模型输出一段散文，脚本没法据此分支。两个办法：

1. 约定结构。要求「只输出 JSON」或「只输出一行 PASS/FAIL」，脚本解析字段，失败按失败处理。
2. 看退出码。调用成功返回零，失败返回非零，先判断这一步再决定是否继续。

```bash
if ! out=$(claude -p "只输出 PASS 或 FAIL，后附一句理由" < "$file"); then
  echo "调用失败：$file" >&2
  exit 1
fi
```

模型说 FAIL，也别让脚本直接删文件。脚本负责发现，人负责处置。

解析 JSON 失败时不要猜。把原始输出和错误一起记进日志，下一次改提示词才有依据。

### 用 JSON 契约代替「看着像对」

散文输出没法分支，最稳的办法是让模型只交固定字段，脚本再校验。下面这段可以整块抄进循环：

```bash
for f in docs/*.md; do
  out=$(claude -p '读取标准输入的内容，只输出一行 JSON，不要解释。字段：{"level":"high|medium|low","reason":"不超过20字"}。第一个字符是左花括号，最后一个字符是右花括号，不要代码块标记。' < "$f")

  if ! printf '%s' "$out" | jq -e '.level and .reason' >/dev/null 2>&1; then
    printf '格式不符: %s\n' "$f" >> build/failures.log
    continue
  fi

  printf '%s\n' "$out" > "build/$(basename "${f%.md}").json"
done
```

这里把正文走标准输入，提示里只写规则，两边不会混。`jq -e` 在字段缺失时返回非零，正好当校验开关。字段名、取值范围、失败时返回什么，都要在提示里写死；写「尽量输出 JSON」等于没写。

提示词里还要防一种情况：模型在 JSON 前后加解释。明确写「第一个字符是左花括号，最后一个字符是右花括号，不要代码块标记」。解析仍然失败就记日志，不要用正则去猜——猜出来的结果会把错误藏得更深。

## 批量处理的底线

- 逐条记账：文件名、耗时、输出路径，失败时知道断在哪。
- 不覆盖原件：写到新目录或新文件，保留可回滚的版本。
- 先小后大：先跑十条，确认输出形状和失败率，再放全量。

```bash
for f in docs/*.md; do
  out="build/$(basename "${f%.md}").summary.md"
  mkdir -p "$(dirname "$out")"
  claude -p "用三句话概括这篇文档，输出纯文本" < "$f" > "$out" \
    || echo "失败: $f" >> build/failures.log
done
```

先拿一条样本彩排。把写操作换成打印，确认输入取对了、提示词给出的形状符合预期、输出路径没写错。通配符没匹配到文件、变量为空、目录不存在，这些错误在单条命令里一眼可见，进了循环就会被淹没。

## 重试与预算

把上一节循环里的调用换成带重试的函数，外层遍历不用改：

```bash
prompt='用三句话概括这篇文档，输出纯文本'

run_with_retry() {
  local f="$1" out="$2" attempt
  for attempt in 1 2 3; do
    if claude -p "$prompt" < "$f" > "$out.part"; then
      mv "$out.part" "$out"
      return 0
    fi
    if [ "$attempt" -lt 3 ]; then
      sleep $((2 ** (attempt - 1)))
    fi
  done
  rm -f "$out.part"
  printf '放弃（3 次）: %s\n' "$f" >> build/failures.log
  return 1
}
```

调用时把上一节的 `claude -p ... > "$out"` 换成 `run_with_retry "$f" "$out"`；失败已经写进日志，外面不用再记一次。等待按 1 秒、2 秒递增，第三次失败就记账走人。只重试可能恢复的错误：网络抖动、限流、超时值得再试；文件不存在、提示词写错、权限不足，重试多少次都一样。日志里记下尝试次数，才能区分偶发失败和「这段输入根本处理不了」。

预算按两笔算：调用次数和 token。开跑前给这一批定一条上限，比如最多 200 次调用、总 token 不超过某个数；跑到一半核对日志，超了就停下来看失败原因，而不是让它继续跑。每次调用把输入长度、输出长度记进同一条日志，月底才知道钱花在哪。限流机制见[速率限制](/concepts/rate-limit/)，退避策略见[重试与退避](/concepts/retry-backoff/)。

把一次性的手动流程固化成脚本，和[自动化重复任务](/learn/automate-repetitive-tasks/)是同一件事的两个阶段：先手动跑通，再交给脚本。

## 权限、成本与提示词版本

无人值守时，最大的风险不是答错，是它做了不该做的动作。先划定：读什么、写什么、能不能联网。范围越小，越敢让它自己跑。默认按只读设计；要写，就限定在产物目录；要跳过确认，先确保环境隔离。别在生产仓库里图省事。

成本按调用次数、输入和输出长度走。跑批量前用一条样本估算总量，日志里记下每次调用的元数据。能先筛掉无关内容，就别整份塞进去，token 怎么算见 [Token](/concepts/token/)。

脚本里的提示词是核心逻辑，不是随手字符串。抽到单独文件，和代码一起提交；改动记原因，用固定样本回测。出问题能回滚，也能看出改动让结果变好还是变坏。权限设置也写进脚本说明：谁在什么环境下能跑、跳过确认的前提是什么。

## 什么时候别写脚本

需要人判断取舍、规则还没稳定、只做一次的流程，都不适合。脚本会把错误放大：手动跑错一次立刻发现，脚本跑错可能已经处理了几百个文件。

下一节把终端操作练顺，减少写脚本时的摩擦：[键盘快捷键](/learn/keyboard-shortcuts-reference/)。
