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直接内置,而不是要求用户安装外部二进制,带来了三个直接收益:
- 零外部依赖:只要运行 Crush,
jq就可用,无需apt install jq或 brew 安装; - 行为可控:内置实现与标准 jq 在细节上有明确差异(见下文),Agent 可以依赖这些稳定的行为;
- 可被中断:内置实现与 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 中的name与description注入模型上下文(见 internal/skills/skills.go 的ToPromptXML)。也就是说,本文讲解的这份 SKILL.md 本身就是 Crush 向模型传达「何时应使用 jq」这一能力的载体。
二、内置 jq 的注册与可用范围
jq并不是一个外部进程,而是注册在 Crush shell 解释器内的builtin 命令。注册发生在 internal/shell/builtins_registry.go:
func init() { RegisterBuiltin("jq", handleJQ) }RegisterBuiltin将handleJQ(定义于 internal/shell/jq.go)注册进进程内的 builtin 表,mvdan.cc/sh/v3解释器在执行命令时直接命中该处理器,全程无进程启动开销。因此:
jq在bash 工具中开箱即用;- 不依赖系统的
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-input | 以null作为输入(忽略 stdin) |
-e,--exit-status | 若最后输出为false或null,退出码为 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.Parse或gojq.Compile失败);5:filter 执行期错误(迭代器返回 error);1:仅在-e模式下,最后输出值为false或null;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、--jsonargs、input_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():
- 入口快速失败:ctx 已取消时立即返回,不为注定失败的请求付出 flag 解析与 gojq 编译的开销;
- 迭代循环内:每产出(或丢弃)一个值都检查一次 ctx,因此像
range(10000000)这类会生成海量值的 filter 能被及时打断; - 读取期间:
ctxReader包装底层 reader,在每次Read调用前检查 ctx,使io.ReadAll在大输入源上也能按块边界取消;同时值累积循环(raw-input 行切分、JSON 流解码)也逐轮检查。
被取消时返回的是ctx.Err()本身(context.Canceled或context.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.jsonNull 输入构造 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_COMMAND做select/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),仅供参考