首页 › tutorials › 把-p模式织入Unix管道

概述

本教程面向已经能在交互式 REPL 里用 Claude Code 完成单次任务、但想把"调用一次 Claude"变成"流水线上的一颗螺丝钉"的开发者。我们将把 -p(headless/print)模式当作一个标准的 Unix 过滤器来设计——它有 stdin、stdout、exit code,会吐出机器可读的事件流,能在断点处续跑。读完之后,你能够用 jq、xargs、fswatch 把 Claude Code 编进任何 Shell 管道里,并实时观测每一步的 token 与耗时。

前置条件

第一步:把 -p 当成 Unix 过滤器

交互式 REPL 是一个"对话",而 -p 是一个"函数调用"。它的设计哲学完全是 Unix 式的:从 stdin 读、向 stdout 写、用 exit code 报告成败。掌握这一点,你就掌握了它和 sed、awk、jq 同级的可组合性。

最朴素的三种调用:

# 1. 提示词作为参数,纯文本输出
claude -p "用一句话解释什么是 PDU 会话"

# 2. 提示词从 stdin 读入(管道左侧喂入"数据")
cat error.log | claude -p "总结这批日志的根因,只输出一句"

# 3. 文件内容重定向进 stdin,参数里只放"指令"
claude -p "为下面的函数补上类型注解,原样输出代码,不要解释" < utils.py

预期结果:三条命令都不会进入 REPL,执行完直接回到 Shell 提示符,答案打印在标准输出。

核心心智模型:把"指令"和"数据"分离。指令写在参数里(稳定、可复用),数据从 stdin 流入(每次不同)。这正是 Unix 过滤器的味道——grep PATTERN 里 PATTERN 是指令,文件流是数据。

常见问题:

现象 原因 解决
输出里夹了"好的,这是结果……"等寒暄 默认系统提示词允许对话语气 用 --append-system-prompt 强约束:"只输出最终答案,不要寒暄"
多行提示词被 Shell 拆成多个参数 没有正确引用 用单引号包裹,或改走 stdin
想把答案写文件却连提示符也写进去了 直接 claude -p ... > out.txt 会把 stderr 一起混入 用 2>/dev/null 或 --output-format 拿干净输出

第二步:掌握三种输出格式与 stream-json 事件协议

-p 的灵魂在于 --output-format。它有三个取值,对应三个完全不同的使用场景:

# text —— 给人看,纯文本答案
claude -p --output-format text "列出三种排序算法"

# json —— 给脚本用,整个会话压成一个 JSON 对象(阻塞到结束才输出)
claude -p --output-format json "列出三种排序算法" | jq .

# stream-json —— 给实时观测用,逐行吐出事件流(NDJSON)
claude -p --output-format stream-json "列出三种排序算法" \
  | jq -c 'select(.type=="assistant")'

stream-json 是最值得深挖的一个。它按行输出(Newline-Delimited JSON),每一行是一个事件对象。理解这四种 type,你就能像读一段协议那样读懂 Claude 的整个思考过程:

sequenceDiagram
    participant U as 你的脚本(stdin)
    participant C as claude -p
    participant M as Claude 模型
    U->>C: 提示词 + 数据
    C->>U: {type:"system", subtype:"init", session_id, tools, model, permission_mode}
    loop 每一轮 agent turn
        C->>M: 历史 + 工具结果
        M-->>C: assistant 消息(content: text | tool_use)
        C->>U: {type:"assistant", message:{content:[...]}}
        alt 包含工具调用
            C->>C: 执行工具(读文件/跑命令)
            C->>U: {type:"user", message:{content:[{type:"tool_result",...}]}}
        end
    end
    C->>U: {type:"result", subtype:"success", total_cost_usd, num_turns, duration_ms, session_id}
    C-->>U: exit code 0

system/init 事件里藏着 session_id——立刻把它存下来,这是稍后续跑的钥匙。result 事件里有四个关键字段:num_turns(轮数)、total_cost_usd(花费)、duration_ms(耗时)、is_error(是否出错)。一行 jq 就能把它们抽出来做账:

claude -p --output-format stream-json "重构 utils.py" \
  | jq -c 'select(.type=="result") | {turns:.num_turns, cost:.total_cost_usd, ms:.duration_ms, sid:.session_id}'

预期结果:结束时打印一行形如 {"turns":4,"cost":0.0123,"ms":8421,"sid":"abc123…"} 的 JSON。

常见问题:

第三步:会话状态机 —— resume / continue / fork

交互式会话天然有状态,但 -p 看似是"一次性"的——其实不然。Claude Code 把每一次 -p 调用都持久化为一条会话记录,配上三个标志位,它就变成了一个可中途暂停、可分叉、可回放的状态机:

# 拿到上一轮的 session_id(从 init 或 result 事件里取)
SID=$(claude -p --output-format stream-json "第一步:分析需求" \
        | jq -r 'select(.type=="result") | .session_id')

# continue —— 续接"当前目录下最近的一次"会话,无需记 id
claude --continue -p "第二步:基于刚才的分析出方案"

# resume —— 精确续接指定 session,适合并行多分支任务
claude --resume "$SID" -p "第三步:把方案拆成 TODO"

# fork —— 从某会话分叉出一个新分支,原会话不被污染
claude --fork "$SID" -p "换个思路,试试另一套方案"

这三者的差别很关键:

stateDiagram-v2
    [*] --> S1: claude -p "分析需求"
    S1 --> S2: --resume S1 "出方案"
    S2 --> S3: --resume S2 "拆 TODO"
    S2 --> B1: --fork S2 "方案B"
    B1 --> B2: --resume B1 "..."
    S3 --> [*]: --resume S3 "执行"
    note right of S1: session_id 是状态机的游标

常见问题:

第四步:资源边界 —— 给无人值守的 Agent 套上笼子

-p 跑在脚本里,意味着没有人在旁边盯。三个标志位构成了你的"护栏三件套":

claude -p \
  --max-turns 8 \
  --permission-mode acceptEdits \
  --allowedTools "Read,Edit,Bash(npm run *)" \
  --append-system-prompt "改完必须跑 npm test,测试不过就回滚,不要狡辩。" \
  "修复 src/auth.ts 里的空指针,提交前自测"

预期结果:上面这条命令会自动改文件、跑测试,若 8 轮内没收敛就以非零码退出,不会无限烧 token。

常见问题:

第五步:Unix 管道组合 —— jq / xargs / fswatch

单次 -p 是函数,组合起来才是工程。三个经典 Unix 模式把 Claude 变成管道公民:

模式 A:流式解析——实时观测与过滤

# 只看模型每一次要调用的工具,像 tail -f 一样盯着它干活
claude -p --output-format stream-json "梳理这个仓库的依赖" \
  | jq -r 'select(.type=="assistant") | .message.content[]? | select(.type=="tool_use") | "调用:" + .name + " " + (.input | tostring)'

模式 B:扇出——一个结果喂给 N 个子任务

# 先让 Claude 列出需要加测试的文件,再对每个文件并发派发子任务
claude -p --output-format text \
  --append-system-prompt "只输出文件名,每行一个,不要任何解释" \
  "src 目录下哪些模块最缺单元测试?" \
  | xargs -P4 -I{} claude -p --max-turns 6 \
      --permission-mode acceptEdits \
      "为 {} 补三个单元测试"

-P4 让 4 个子任务并发跑。每个 -p 都是独立的进程、独立的会话,天然隔离——这比手写线程池简单得多。

模式 C:监听——文件一变就触发

# 用 entr 监听:只要被测文件改动,就让 Claude 重跑测试并自动修
ls src/*.ts | entr -c sh -c '
  claude -p --output-format stream-json --max-turns 10 \
    --permission-mode acceptEdits \
    "跑 npm test,把第一个失败的用例修到通过" \
    | jq -c "select(.type==\"result\") | {ok:(.is_error|not), turns:.num_turns}"
'

macOS 上把 entr 换成 fswatch -o src/ | xargs -I{} ... 即可。

常见问题:

第六步:可观测性 —— 把事件流变成实时仪表盘

stream-json 的真正威力是"白盒"。下面这段脚本用一个 tee 把事件流一分为二:一份喂给业务处理,一份喂给一个纯 Shell 写的实时仪表盘,边跑边打印轮数、累计 token、累计花费:

#!/usr/bin/env bash
# dashboard.sh —— 边跑 Claude 边看实时开销
FIFO=/tmp/cc_events_$$
mkfifo "$FIFO"
# 后台:把事件流分类统计,实时刷新
(
  turns=0; cost=0; itok=0; otok=0
  jq -c '.' < "$FIFO" | while read -r ev; do
    t=$(echo "$ev" | jq -r '.type')
    case $t in
      assistant) echo "$ev" | jq -e '.message.content[]? | select(.type=="tool_use")' >/dev/null \
                   && turns=$((turns+1));;
      result)    cost=$(echo "$ev"  | jq -r '.total_cost_usd');
                 itok=$(echo "$ev"  | jq -r '.usage.input_tokens');
                 otok=$(echo "$ev"  | jq -r '.usage.output_tokens');;
    esac
    printf '\rturns=%-3s cost=$%-7s in=%-7s out=%-7s' "$turns" "$cost" "$itok" "$otok"
  done
) &
DASH=$!
# 前台:真正执行任务,事件流 tee 给仪表盘
claude -p --output-format stream-json "$@" > "$FIFO"
kill $DASH 2>/dev/null; rm -f "$FIFO"
echo

跑起来:./dashboard.sh "把 src 下所有 console.log 删掉",你会看到一行不断刷新的指标条。把这段思路接上 curl 推到 Prometheus/Grafana,就是一套现成的 Agent 观测管线。

常见问题:

实战案例:半结构化日志 → 结构化告警的自愈管道

把前面六步串成一个真实项目。目标:监听一份不断追加的 app.log,每当出现新的 ERROR,让 Claude 把它的半结构化文本解析成 JSON、判定严重程度、对 P0 自动尝试修复并 PR,全程可观测。

目录约定:.sid/ 存每个告警分支的会话游标;alerts/ 存结构化结果;dash.log 存实时指标。

#!/usr/bin/env bash
# selfheal.sh —— 日志自愈管道主控
set -euo pipefail
mkdir -p .sid alerts

follow_errors() {
  # 模式 C:监听日志新增行,过滤出 ERROR,按指纹去重后触发处理
  tail -n0 -F app.log \
    | grep --line-buffered 'ERROR' \
    | while IFS= read -r line; do
        FID=$(echo -n "$line" | shasum | cut -c1-8)
        [ -f ".sid/$FID.done" ] && continue        # 同一问题只处理一次
        handle "$FID" "$line" &
      done
}

handle() {
  local fid="$1" line="$2"
  # 步骤1:解析 + 定级(只读,快,max-turns 收窄)
  local parsed sid
  parsed=$(echo "$line" | claude -p --output-format json --max-turns 4 \
    --append-system-prompt "只输出 JSON:{severity:P0|P1|P2, component, msg, suggested_fix}" \
    "把这条日志解析成结构化字段")
  sid=$(echo "$parsed" | jq -r '.session_id')
  echo "$sid" > ".sid/$fid"
  echo "$parsed" | jq -r '.result | fromjson' > "alerts/$fid.json"
  echo "[$(date +%T)] $fid 定级完成 sid=$sid"

  # 步骤2:仅 P0 才尝试自愈(acceptEdits + 白名单,max-turns 兜底)
  if [ "$(jq -r .severity "alerts/$fid.json")" = "P0" ]; then
    claude --resume "$sid" -p --max-turns 10 \
      --permission-mode acceptEdits \
      --allowedTools "Read,Edit,Bash(npm run *),Bash(git *)" \
      --output-format stream-json \
      --append-system-prompt "修完必须 npm test,绿了再 git commit,最后总结一行。" \
      "按 suggested_fix 修复并验证" \
      | tee -a dash.log | jq -c 'select(.type=="result") | {fid:"'"$fid"'", ok:(.is_error|not), turns:.num_turns, cost:.total_cost_usd}'
  fi
  touch ".sid/$fid.done"
}

follow_errors

整体架构与数据流:

flowchart LR
    LOG[(app.log)] -->|tail -F grep ERROR| DEDUP{指纹去重<br/>shasum}
    DEDUP -->|新问题| PARSE[claude -p --max-turns 4<br/>解析+定级]
    DEDUP -->|已处理| SKIP[跳过]
    PARSE -->|session_id| SID[(.sid/ 指纹→游标)]
    PARSE -->|JSON| ALERTS[(alerts/ 结构化)]
    PARSE -->|severity| GATE{P0?}
    GATE -->|是| HEAL[claude --resume sid<br/>--acceptEdits --max-turns 10<br/>修复+自测+提交]
    GATE -->|否| DONE([归档])
    HEAL -->|stream-json tee| DASH[dash.log<br/>实时轮数/花费/成败]
    HEAL --> SID2[复用同一游标<br/>保持上下文]

这个案例同时用上了:第二步的 stream-json 协议解析、第三步的 --resume 复用游标(定级和自愈共享上下文)、第四步的 --max-turns + --permission-mode + --allowedTools 三重护栏、第五步的 tail/while/& 并发管道、第六步的 tee 实时观测。每一段都能单独抽出来复用。

验收:手动 echo "[ERROR] db connection refused" >> app.log,几秒内应看到 alerts/<fid>.json 生成;若是 P0(取决于你的系统提示约定),会看到 dash.log 里滚出 result 事件和一行汇总。

常见问题:

延伸阅读

参考来源

  1. Run Claude Code programmatically — Claude Code Docs — -p/--print 头部模式、--output-format、--input-format、--resume/--continue 的官方语义与示例。
  2. CLI reference — Claude Code Docs — 全部命令与标志位(--fork、--max-turns、--permission-mode、--allowedTools、--append-system-prompt、--mcp-config)的分类速查。
  3. Claude Code stream-json: the output format that changes everything — text/json/stream-json 三种格式的对比与事件流解析实战。
  4. CLI Output & Headless Mode — DeepWiki — --print 标志如何启用头部模式与结构化输出,含事件类型拆解。
  5. Headless Mode and CI/CD — Cheatsheet (SFEIR Institute) — 头部模式在 CI/CD 管道中的速查命令清单。

把每一次 -p 调用都当作可组合、可观测、可续跑的工程原语——Claude Code 就从"一个聊天工具"变成"你 Shell 工具箱里最锋利的那把瑞士军刀"。

[!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 等场景全覆盖。

51学通信知识星球海报-扫码加入