Crush 内置 jq 命令:零依赖的 JSON 处理技能与源码级实现解析
2026/9/20 12:16:56 网站建设 项目流程

Crush 内置 jq 命令:零依赖的 JSON 处理技能与源码级实现解析

【免费下载链接】crushGlamourous agentic coding for all 💘项目地址: https://gitcode.com/gh_mirrors/crush3/crush

导读

Crush 在自身 shell 环境中内置了一个完整的jq命令,基于纯 Go 的 gojq 实现,无需安装任何外部二进制即可完成 JSON 的查询、过滤、重塑与构造。本文以 internal/skills/builtin/jq/SKILL.md 为骨架,完整讲解其支持的命令行参数、与标准 jq 的行为差异、常用实战模式,并结合 internal/shell/jq.go 的源码与 internal/shell/jq_test.go 的测试,深入剖析其注册机制、上下文取消语义与超时处理原理。读完本文,你将掌握在 Crush 中高效处理 API 响应、配置文件、日志输出等结构化数据的完整技能,并能理解如何将 jq 用于 hook 脚本等自动化场景。

一、为什么 Crush 要内置 jq

在 Agent 工作流中,模型需要频繁地对工具输出做结构化处理:从 API 响应中提取字段、过滤数组、重塑对象、构造 JSON 参数。Crush 选择将jq直接内置,而不是要求用户安装外部二进制,带来了三个直接收益:

  1. 零外部依赖:只要运行 Crush,jq就可用,无需apt install jq或 brew 安装;
  2. 行为可控:内置实现与标准 jq 在细节上有明确差异(见下文),Agent 可以依赖这些稳定的行为;
  3. 可被中断:内置实现与 Crush 的 hook 超时机制深度集成,长查询可以被 context 取消(这一设计在 internal/shell/jq.go 中通过ctx轮询实现,详见第五节)。

从源码结构看,这个技能文件位于internal/skills/builtin/jq/SKILL.md,属于 Crush 的builtin skill体系——技能正文通过go:embed嵌入二进制,启动时由 internal/skills/embed.go 的DiscoverBuiltinWithStates扫描发现,并以其 frontmatter 中的namedescription注入模型上下文(见 internal/skills/skills.go 的ToPromptXML)。也就是说,本文讲解的这份 SKILL.md 本身就是 Crush 向模型传达「何时应使用 jq」这一能力的载体。

二、内置 jq 的注册与可用范围

jq并不是一个外部进程,而是注册在 Crush shell 解释器内的builtin 命令。注册发生在 internal/shell/builtins_registry.go:

func init() { RegisterBuiltin("jq", handleJQ) }

RegisterBuiltinhandleJQ(定义于 internal/shell/jq.go)注册进进程内的 builtin 表,mvdan.cc/sh/v3解释器在执行命令时直接命中该处理器,全程无进程启动开销。因此:

  • jqbash 工具中开箱即用;
  • 不依赖系统的jq二进制,脚本迁移到没有 jq 的环境也能运行;
  • 它同样可用于PreToolUse 等 hook 脚本中(见第七节实战)。

输入来源支持两种:标准输入流(stdin)和文件参数,即jq '.foo' file.json的形式同样受支持。当未提供任何 filter 时,默认 filter 为.(原样输出输入),这一点在 internal/shell/jq.go 的queryStr == ""分支中实现。

三、支持的命令行参数(Flags)

内置 jq 支持以下参数,与标准 jq 的常用子集保持一致:

Flag说明
-r,--raw-output字符串直接输出,不加引号
-j,--join-output-r,但输出末尾不加换行符
-c,--compact-output单行紧凑 JSON 输出
-s,--slurp将所有输入读入一个数组
-n,--null-inputnull作为输入(忽略 stdin)
-e,--exit-status若最后输出为falsenull,退出码为 1
-R,--raw-input将每一行作为字符串而非 JSON 读取
--arg name value$name绑定为字符串值
--argjson name value$name绑定为解析后的 JSON 值

file 参数放在 filter 之后同样受支持:jq '.foo' file.json

参数解析的源码细节

结合 internal/shell/jq.go 的参数解析循环,有几点值得注意:

  • -j-r的超集:解析--join-output时同时置位rawOutput,差异仅体现在输出末尾是否追加换行(由writeValue中的join分支决定)。
  • --argjson会校验 JSON 合法性--argjson name value中 value 必须能通过json.Unmarshal,否则报错并以退出码 2 返回(见 internal/shell/jq.go 第 101-113 行)。
  • --之后全部视为文件参数,避免文件名以-开头时的歧义。
  • 未知选项:filter 已解析后若再出现-开头的 token,会报 "unknown option" 并以退出码 2 退出。
  • 退出码约定(从 internal/shell/jq.go 可确认):
    • 2:参数错误或输入解析错误(如--argjson非法 JSON);
    • 3:filter 编译错误(gojq.Parsegojq.Compile失败);
    • 5:filter 执行期错误(迭代器返回 error);
    • 1:仅在-e模式下,最后输出值为falsenull
    • 0:正常执行。

四、与标准 jq 的差异

内置 jq 使用 gojq(github.com/itchyny/gojq,纯 Go 实现),与标准 jq 存在以下关键差异,编写 filter 时需要特别注意:

  • 对象键不做排序:键默认按字典序排列;keys_unsorted-S参数不可用。
  • 任意精度整数:大整数保留完整精度(加法、减法、乘法、取模以及整除时的除法运算均不损失精度)。
  • 字符串索引返回子串"abcde"[2]返回"c"(字符串),而非标准 jq 中的字符编码数字。
  • 不支持的参数/特性--ascii-output--seq--stream--stream-errors-f/--from-file--slurpfile--rawfile--args--jsonargsinput_line_number$__loc__,以及部分正则特性(反向引用 backreferences、环视 look-around)。
  • YAML:gojq 本身支持--yaml-input/--yaml-output,但 Crush 内置 jq 当前并未暴露这两个 flag。

其中「任意精度整数」是 gojq 相对标准 jq 的一个亮点:处理订单号、ID、时间戳等超出float64精确表示范围的值时不会出现尾数失真。

五、源码级原理:输入读取、输出与取消

5.1 输入读取流程(readInputs)

internal/shell/jq.go 的readInputs负责把所有输入(stdin 或多个文件)统一读入内存,再交给 gojq 执行:

  • -n优先:直接返回[]any{nil},完全不触碰 stdin;
  • 文件优先于 stdin:有文件参数时逐文件打开读取,否则读 stdin;
  • -R模式:按行切分为字符串;配合-s时将所有行用\n拼接为一个整体字符串;
  • JSON 流模式:使用json.Decoder连续解码,支持一个输入流中包含多个 JSON 值(这正是jq -s '.'能 slurp 多个值的基础);解码失败报 "parse error" 并以退出码 2 返回;
  • 空输入兜底:一个值都没读到(如空文件)时返回[]any{nil},保证 filter 至少执行一次。

5.2 输出编码(writeValue)

writeValue按以下规则输出每个值:

  • rawOutput且值为字符串:直接写字符串;joinOutput时不再追加换行,否则追加\n
  • compact模式:使用gojq.Marshal输出单行紧凑 JSON;
  • 默认模式:使用json.MarshalIndent(v, "", " ")输出带两级缩进的漂亮 JSON。

5.3 上下文取消:hook 超时也能中断长查询

这是内置 jq 最值得注意的工程细节。handleJQ在三个位置轮询ctx.Err()

  1. 入口快速失败:ctx 已取消时立即返回,不为注定失败的请求付出 flag 解析与 gojq 编译的开销;
  2. 迭代循环内:每产出(或丢弃)一个值都检查一次 ctx,因此像range(10000000)这类会生成海量值的 filter 能被及时打断;
  3. 读取期间ctxReader包装底层 reader,在每次Read调用前检查 ctx,使io.ReadAll在大输入源上也能按块边界取消;同时值累积循环(raw-input 行切分、JSON 流解码)也逐轮检查。

被取消时返回的是ctx.Err()本身(context.Canceledcontext.DeadlineExceeded),而不是interp.ExitStatus。这保证了调用方(如 hook runner)能区分「filter 正常退出非零」与「我们超时了」两种截然不同的情况。

internal/shell/jq_test.go 中的四个取消相关测试完整验证了这一设计:

  • TestJQ_CtxCancel:执行range(10000000)前即取消 ctx,断言返回context.Canceled
  • TestJQ_CtxCancel_DuringFilter:50ms 超时下运行range(100000000),断言在 1 秒内被中断而非跑完 1 亿次迭代;
  • TestJQ_CtxCancel_MidReadAll:用 512 字节/5ms 的慢速 reader 喂 64MiB 原始输入,证明ctxReader能在流中段观察到取消(若被取消则context.Canceled;若被完整读完则失败);
  • TestJQ_CtxCancel_PreCancel:用「一读就 fail」的 reader 证明入口快速失败路径在进入io.ReadAll之前就生效。

正如 internal/shell/jq.go 的注释所述,如果 reader 本身永久阻塞(如未关闭的管道),内置 jq 仍可能熬过 ctx;此时 hook runner 的 abandon-goroutine 兜底路径(见 internal/hooks/runner.go)才是最终执行者。

六、常用模式实战

以下示例均来自 internal/skills/builtin/jq/SKILL.md,可直接复制运行。

提取字段

echo '{"name":"crush"}' | jq '.name'

过滤数组

echo '[1,2,3,4,5]' | jq '[.[] | select(. > 3)]'

重塑对象

echo '{"first":"Ada","last":"Lovelace"}' | jq '{full: (.first + " " + .last)}'

使用变量(字符串与 JSON 值)

echo '{}' | jq --arg host localhost --argjson port 8080 '{host: $host, port: $port}'

Slurp 多个 JSON 值

echo '{"a":1}{"b":2}' | jq -s '.'

紧凑输出便于管道传递

echo '{"a":1}' | jq -c '.a += 1'

原始字符串输出

echo '["one","two","three"]' | jq -r '.[]'

处理文件

jq '.dependencies | keys' package.json

Null 输入构造 JSON

jq -n --arg msg hello '{"message": $msg}'

七、实战:在 hook 脚本中用 jq 构造结构化输出

内置 jq 最典型的高级用法出现在 Crush 的 hook 生态中。以仓库自带的 docs/hooks/examples/rtk-rewrite.sh 为例,该 PreToolUse hook 会把 bash 命令重写为 rtk 命令以节省 token,其最后一步正是用jq -n --arg构造 hook 返回给 Crush 的结构化 JSON:

REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) && EXIT_CODE=0 || EXIT_CODE=$? case $EXIT_CODE in 0 | 3) # Rewrite found. If identical, the command already uses rtk. [ "$CMD" = "$REWRITTEN" ] && exit 0 jq -n --arg cmd "$REWRITTEN" \ '{decision: "allow", updated_input: ({command: $cmd} | tostring)}' ;; *) # No rewrite (1), deny (2), or unexpected — pass through. exit 0 ;; esac

这里jq -n --arg cmd "$REWRITTEN"用到了本技能的核心组合能力:-n忽略 stdin(hook 脚本没有 JSON 输入),--arg将 shell 变量安全地注入 JSON 字符串(自动完成引号转义),随后{decision: ...}重塑出 Crush hook 协议要求的allow决策与更新后的工具输入。这与 hook 体系(docs/hooks/README.md)中「重写工具输入」的用途完全吻合——$CRUSH_TOOL_INPUT_COMMAND等环境变量由 internal/hooks/runner.go 的BuildEnv注入,脚本内再用 jq 加工后输出。

类似的模式还可以用于:

  • 提取字段供后续命令使用jq -r '.url' data.json | xargs curl
  • 在 hook 中过滤与统计工具调用日志:对$CRUSH_TOOL_INPUT_COMMANDselect/test匹配;
  • 自动审批安全命令:对命令 JSON 做条件判断后输出{decision: "allow"}

八、实用技巧速查

  • 将 jq 输出管道给其他命令:jq -r '.url' data.json | xargs curl
  • filter 内部用|串联多个变换,不要用 shell 管道(shell 管道会丢失 JSON 上下文);
  • try抑制缺失键导致的报错:jq 'try .foo.bar'
  • // "default"提供兜底值:jq '.name // "unknown"'
  • 善用格式化字符串函数:@csv@tsv@base64@html@uri,可将数组/对象直接编码为 CSV、TSV、Base64、HTML 或 URI 组件格式。

九、小结

Crush 内置 jq 是「零依赖工具链」设计的一个缩影:纯 Go 的 gojq 实现通过 builtin 注册机制嵌入 shell,既有标准 jq 的核心能力,又针对 Agent 场景做了取舍——上下文可中断、参数行为明确、退出码语义清晰,还能无缝嵌入 hook 脚本构造结构化输出。对模型而言,internal/skills/builtin/jq/SKILL.md 提供了何时调用与如何调用的完整指引;对开发者而言,internal/shell/jq.go 与其测试则是理解其边界与可靠性的最佳入口。

【免费下载链接】crushGlamourous agentic coding for all 💘项目地址: https://gitcode.com/gh_mirrors/crush3/crush

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询