本教程教你把 Claude Code 当成一台无人值守的批量迁移机:用 headless 的 -p 模式接进 Unix 管道,让 Agent 逐个消化 100+ 个文件,自动拆任务、改代码、跑测试、原子提交,失败了还能断点续跑。适合有 CLI 基础、想用 Agent 自动化处理「量大、机械、但每个文件又要看上下文」的迁移与重构工作的开发者——例如统一 API 签名、框架升级、SQL 参数化、日志规范化。
读完本文,你能独立搭出一条「清单驱动 + 自拆解 + 可恢复 + 可观测」的批量迁移流水线,并理解其中每个旋钮为什么这样拧。
claude -p "hi"。bash、git、管道、退出码。sed)、结构化生成(用脚本)、语义级迁移(交给 Agent)。核心判断:如果
sed能安全搞定,就别请 Agent。迁移机的价值在于每个文件的改动都需要读上下文才能决定怎么改——这正是正则的盲区、Agent 的主场。
claude -p(即 --print)是非交互模式:读一次输入、干完活、把结果打到 stdout、然后退出。它读 stdin、写 stdout,因此可以像 cat、grep 一样被塞进任何 Unix 管道。
# 最朴素的形态:把一个文件喂给 Agent,让它迁移
cat src/users/repo.ts | claude -p "把这个文件里所有 db.query(sql) 改成 db.execute(sql, params),把字符串拼接的变量抽成参数绑定。只输出改后的完整文件,不要解释。"
关键事实(直接影响你怎么设计流水线):
> 重定向、用 tee 同时落盘与上屏、用 jq 解析。一个"只读 + 吐结果"的最小验证,确认环境就绪:
echo "返回 JSON:{\"ok\":true}" | claude -p --output-format json "原样回显输入" | jq -r .result
# 预期输出:返回 JSON:{"ok":true}
如果这一步出不来结果,先别往下走——多半是登录、网络或版本问题。
批量迁移的失败,多半不是单文件改错,而是Agent 在第 40 个文件时忘了最初约定的规则。解法:用 --append-system-prompt 把"迁移规约"钉死在每个进程的系统提示里,再用 TodoWrite 让 Agent 把一个文件拆成有序子步骤。
把规约写成文件(比命令行字符串更易维护、可 diff、可入 git):
# migration-rules.md
你是 SQL 参数化迁移执行器。对每个目标文件:
1. 用 Read 读取完整文件。
2. 用 TodoWrite 拆出子任务:定位调用点 → 改写 → 自检 → 运行类型检查。
3. 每个非查询语句(INSERT/UPDATE/DELETE)必须参数化:db.query(`...${x}...`) → db.execute('... ?', [x])。
4. 纯 SELECT 且无外部拼接可保留,但在 TodoWrite 里标注"已评估,保留"。
5. 禁止改测试快照文件(*.snap)。禁止改迁移脚本本身。
6. 改完跑 `pnpm tsc --noEmit`,有错就修,修不掉就回滚该文件并在最终输出标记 SKIPPED。
7. 最后一行只输出一个 JSON:{"file":"<path>","status":"done|skipped","changes":<int>}。
启动单个文件迁移时挂上它:
claude -p "迁移 src/users/repo.ts" \
--append-system-prompt-file ./migration-rules.md \
--allowedTools "Read,Edit,Bash(pnpm tsc --noEmit)" \
--max-turns 30
三个旋钮的意义:
--append-system-prompt-file:规约进系统层,每轮都看得见,抗"长任务漂移"。--allowedTools:白名单,只放行迁移必需的工具。这里连通用 Bash 都不给,只放行一条精确到命令的 pnpm tsc,把破坏面压到最小。--max-turns 30:单文件迁移不该无限循环,设硬上限防"原地打转烧 token"。现在把单文件能力组装成批处理。核心思路:先扫出清单,再逐项消费,每个文件独立成一个 git 提交。这样任何一个文件改坏,都能 git revert <commit> 精准回滚,不会污染其它文件。
#!/usr/bin/env bash
# migrate.sh — 清单驱动的批量迁移机
set -euo pipefail
MANIFEST="manifest.txt" # 目标文件清单,一行一个路径
LOG="migrate.log" # 运行日志
RULES="migration-rules.md"
# 1) 生成清单:用 grep 初筛候选,Agent 只看"可能要改"的文件
git grep -l "db\.query(" -- 'src/**/*.ts' > "$MANIFEST" || true
echo "[plan] 共 $(wc -l < "$MANIFEST") 个候选文件"
# 2) 逐文件消费
while IFS= read -r file; do
[ -z "$file" ] && continue
echo "==== $(date +%T) $file ====" | tee -a "$LOG"
# 跑迁移:输出最后一行 JSON 摘要
summary=$(claude -p "迁移 $file" \
--append-system-prompt-file "$RULES" \
--allowedTools "Read,Edit,Bash(pnpm tsc --noEmit)" \
--max-turns 30 2>>"$LOG" | tail -n 1)
status=$(echo "$summary" | jq -r .status 2>/dev/null || echo "parse-error")
if [ "$status" = "done" ]; then
# 原子提交:一个文件一个 commit,message 自带文件名便于回滚
git add "$file"
git commit -m "refactor(sql): 参数化 $file" -q
echo "[ok] $file → committed" | tee -a "$LOG"
else
# 失败/跳过:丢弃该文件的全部改动,保持工作区干净
git checkout -- "$file" 2>/dev/null || true
echo "[skip] $file ($status)" | tee -a "$LOG"
fi
done < "$MANIFEST"
echo "[done] 完成。日志见 $LOG"
设计要点拆解:
git grep 做粗筛,把 Agent 的注意力收窄到候选集。120 个文件可能只有 80 个真要改,省下的 40 个就是纯利润。git checkout -- 保证工作区永远是干净的已知态,下一轮不背历史包袱。jq 解析、便于日志统计、便于断点续跑时知道停在哪。--allowedTools 是软白名单。还有更强的手段:权限模式。迁移机推荐用 acceptEdits(自动接受文件编辑,但 Bash 仍需授权)配合精确的 --allowedTools,而不是图省事上 --dangerously-skip-permissions(关掉一切确认)。后者在自动化里一旦 Agent 走偏,破坏不可控。
正式跑前的 dry-run(干跑):把规约里的"改写"换成"只报告不改",先让 Agent 全仓库走一遍,产出一份"将要改什么"的清单给你审。审完再开真正写入。
# dry-run:allowedTools 里不给 Edit,Agent 只能读和报告
claude -p "评估 src/users/repo.ts 需要哪些参数化改动,只读不改,输出改动清单 JSON" \
--append-system-prompt-file ./migration-rules.md \
--allowedTools "Read" \
--max-turns 20 > dryrun/src-users-repo.json
dry-run 产物汇总后,人工抽查 5~10 个文件,确认规约被正确理解,再放开 Edit。这一步能挡掉 80% 的"批量翻车"。
100 个文件跑到第 60 个断了网/被 Ctrl-C,怎么办?两条路:
路线 A(推荐,工程化):脚本自身可重入。上面的 migrate.sh 已经具备——因为它基于"清单 + 干净工作区 + 逐文件提交",重跑时只要从清单里剔除已经提交过的文件即可:
# 续跑:去掉已迁移(已出现在 commit message 里)的文件
grep -vxFf <(git log --oneline | grep 'refactor(sql)' | sed 's/.*参数化 //') "$MANIFEST" > todo.txt
mv todo.txt "$MANIFEST"
./migrate.sh
路线 B(会话级续命):用 --resume <session-id> / --continue。--continue 接最近一次会话,--resume 接指定 session。适合"一个 Agent 跨多次调用追同一个上下文"的场景,但批量迁移通常是N 个独立短会话,路线 A 更稳、更可观测。
# 记下 session-id,事后可续
SID=$(claude -p "迁移 src/x.ts" --append-system-prompt-file ./migration-rules.md \
--output-format json | jq -r .session_id)
# 之后
claude -p "继续刚才没改完的部分" --resume "$SID"
判断标准:任务是无状态的 N 个独立单元 → 路线 A;任务是单个长链条需保留中间推理 → 路线 B。
跑 100 个文件,光看 tail -f migrate.log 不够,你想要的是每个文件里 Agent 真正在干什么(读了哪些行、改了哪几处、是否在空转)。切到流式输出:
claude -p "迁移 $file" \
--append-system-prompt-file "$RULES" \
--allowedTools "Read,Edit,Bash(pnpm tsc --noEmit)" \
--output-format stream-json --verbose --include-partial-messages \
--max-turns 30 >> "stream/$file.jsonl"
stream-json 是逐行的 JSON 事件流:工具调用、工具结果、思考片段、文本增量都是独立一行。你可以:
# 只看某个文件迁移过程中发生了哪些工具调用
jq -r 'select(.type=="tool_use") | .name + " " + (.input|tostring|.[0:80])' "stream/src-users-repo.jsonl"
这把"黑盒 Agent"变成了可审计的事件日志——出问题时能精确回放第几个 turn 走歪了。
一个常被忽略的坑:headless 模式下,若 Agent 起了后台进程(dev server、watch 构建),它会在 Agent 给出最终结果、stdin 关闭后约 5 秒被回收(v2.1.163 起)。若你让 Agent 起了后台子代理/工作流,
claude -p会等它完成——从 v2.1.182 起这个等待默认封顶 10 分钟,可用环境变量CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS调整,设 0 表示无限等。批量迁移里别让 Agent 起常驻进程,免得一个文件卡死整条线。
场景:一个 Node/TypeScript 单体仓,120 个 .ts 文件里散落着 db.query(\SELECT ... ${userInput} ...`)式的字符串拼接,存在 SQL 注入风险。要求全部迁移到db.execute(sql, params)` 参数绑定。
为什么必须用 Agent 而不是 sed:每个调用点的拼接方式都不一样——有的嵌在模板字符串里、有的先 + 拼成变量再传入、有的在循环里动态拼 WHERE;参数顺序、类型、是否需转义都依赖上下文判断。正则会误伤注释和字符串字面量,Agent 能读懂语义。
落地步骤:
git checkout -b refactor/sql-parametrize,保证主干安全。git grep -l "db\.query(" -- 'src/**/*.ts' > manifest.txt,得 120 行。migration-rules.md,重点钉死"非查询必参数化、快照文件禁改、改完过 tsc"。./migrate.sh,边跑边 tail -f migrate.log 看 ok/skip 计数。pnpm tsc --noEmit && pnpm test,红的是个别漏网,逐个修。git log --oneline | grep 'refactor(sql)' 随机挑 10 个 commit 人工 code review。预期结果:120 个文件,约 95 个一次过,10~15 个被标 SKIPPED(多是边界情况,如动态表名无法参数化,规约要求保留并标注),总耗时约 1~2 小时无人值守,token 成本远低于让人手改一天的工时,且每个改动都可单点回滚。
flowchart TD
A[主干建分支] --> B[git grep 粗筛候选]
B --> C[生成 manifest.txt 清单]
C --> D{清单还有文件?}
D -- 是 --> E[取下一个文件]
E --> F["claude -p 单文件迁移<br/>规约+白名单+max-turns"]
F --> G[Agent: Read→TodoWrite拆解→Edit→tsc自检]
G --> H{输出 status?}
H -- done --> I[git add + 原子提交]
H -- skipped/err --> J[git checkout 回滚该文件]
I --> K[stream-json 事件落盘]
J --> K
K --> D
D -- 否 --> L[全局 tsc + test 验证]
L --> M[抽样 code review]
M --> N[开 PR 合并]
J -.失败可续.-> P["续跑:剔除已提交文件<br/>重跑脚本即可"]
--max-turns 被截断:调高上限,或把规约里"自检+修错"拆成两段——先纯改、再单独跑一轮"修 tsc 错误",避免单文件无限循环。Read 工具读。--append-system-prompt-file,而非塞在普通 prompt 里。git grep 模式收窄候选集。--allowedTools 收到极致(只 Read+Edit+一条固定 test 命令),绝不给通用 Bash;靠"一文件一提交"做最终兜底。-p、stdin、stream-json、后台进程行为)--continue / --resume / --fork)-p、stdin 10MB 上限、stream-json、后台进程 5 秒回收与 CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS 等关键行为。--allowedTools、--max-turns、--append-system-prompt-file、--settings、--mcp-config。--continue / --resume <session-id> / --fork 会话续命与分叉机制。claude -p + stream-json 编排封装,批量流水线设计可借鉴。[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
