Skip to content

fix(jq): follow current jq for --indent 0 and output-flag precedence - #2769

Closed
chaliy wants to merge 1 commit into
mainfrom
eve/clever-mccarthy-2aza8q-jq-indent0
Closed

chaliy wants to merge 1 commit into
mainfrom
eve/clever-mccarthy-2aza8q-jq-indent0

Conversation

@chaliy

@chaliy chaliy commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What changed

Two output-formatting behaviors now match real jq, and the differential oracle that proves it is pinned.

  1. --indent 0 pretty-prints. It emits newlines between tokens with no leading spaces, instead of collapsing to compact output. -c / --compact-output is now the only compact switch.
  2. -c, --tab and --indent N are last-flag-wins. Previously -c always won over --indent N, and --tab always won over a later --indent N. Real jq — 1.7 and 1.8 alike — takes the last of the three on the command line.
  3. The jq oracle is version-pinned in CI, the way ripgrep's already is: scripts/install-jq-ci.sh verifies a SHA-256 before the binary reaches PATH, wired into the three jobs that run these tests with JQ_VERSION beside RG_VERSION.

yq is deliberately untouched — its -I 0 means compact, which is mikefarah/yq's own documented semantics, not jq's.

Why

Nightly's asan job went red on jq::tests::differential::diff_indent_0 with no change on our side. The sanitizer was incidental.

jq 1.7 and earlier let --indent 0 imply -c; jq 1.8 removed that implication (jqlang/jq#3232). ubuntu-latest is currently rolling from 24.04 to 26.04, and the two images ship jq 1.7 and jq 1.8 respectively — nightly's job landed on 26.04 while PR jobs were still on 24.04. Since the test asserts byte-for-byte parity against whatever jq is on PATH, the suite had become a function of which image a job happened to get, and no implementation could satisfy both halves of the fleet. Pinning the oracle is the actual fix; following jq 1.8 is the right side to pin to, since it is current stable.

Checking the flag combinations while fixing that turned up #2 as a pre-existing bug. Its cause was redundant state — a compact_output: bool sitting beside an indent: Indent whose Compact variant already said the same thing, with the tie broken after parsing. One field now holds the output shape and each of the three flags overwrites it, so last-wins falls out of parse order instead of being arbitrated later.

Before / After

Oracle is jq 1.8.1, SHA-256 020468de…c2a0d, matching scripts/install-jq-ci.sh.

echo '{"a":[1,2]}' | jq <flags> .

flags before (bashkit) after (bashkit) real jq 1.8.1
--indent 0 {"a":[1,2]} {⏎"a": [⏎1,⏎2⏎]⏎} same as after
-c --indent 0 {"a":[1,2]} {⏎"a": [⏎1,⏎2⏎]⏎} same as after
--indent 0 -c {"a":[1,2]} {"a":[1,2]} same
-c --indent 4 {"a":[1,2]} 4-space indented same as after
--indent 4 -c {"a":[1,2]} {"a":[1,2]} same
-c --tab {"a":[1,2]} tab indented same as after
--tab --indent 4 tab indented 4-space indented same as after

The failing nightly assertion, before:

---- builtins::jq::tests::differential::diff_indent_0 stdout ----
assertion `left == right` failed: stdout mismatch for --indent 0 .
bashkit:
{"a":1}

real jq:
{
"a": 1
}
test result: FAILED. 3891 passed; 1 failed

I also replayed every case in the differential suite against both jq 1.7 and jq 1.8.1 to size the blast radius. --indent 0 is the only behavior the two versions disagree on anywhere:

SAME -c --indent 4          SAME --indent 4 -c       SAME --indent 0 -c
SAME -c --tab               SAME --tab -c            SAME --tab --indent 4
SAME --indent 4 --tab       SAME --indent 0 --indent 4
DIFF -c --indent 0          1.7={"a":[1,2]}   1.8={|"a": [|1,|2|]|}

That is why the precedence cases are asserted unconditionally and only --indent 0 is gated.

Risk

  • Low–Medium. --indent 0 and the -c/--indent/--tab combinations change output shape, so a caller that relied on --indent 0 meaning compact gets pretty output — exactly as it would from a real jq 1.8 upgrade. -c alone, --indent N alone and --tab alone are unchanged, and those are the forms every jq spec case in the repo uses.
  • A CI pin does not help a developer whose distro ships jq 1.7, so assert_matches_from_jq((1, 8), …) skips just the affected case on an older host oracle and prints why — the bargain minimum_bash_major already strikes for the competitor corpus. The gate is per-case, never suite-wide: a skipped suite is indistinguishable from a passing one.

Checklist

  • Tests added or updated — --indent 0 flat + nested, -c still compact, the full last-wins matrix as both unit and differential tests, and a regression test for --tab followed by --indent N
  • Backward compatibility considered — internal code, no compatibility shim; the behavior change is the point

Generated by Claude Code

`jq --indent 0` now pretty-prints with zero-width indentation -- newlines
between tokens, no leading spaces -- instead of collapsing to compact output.
`-c`/`--compact-output` is the only compact switch.

jq 1.7 and earlier let `--indent 0` imply `-c`; jq 1.8 removed that
implication (jqlang/jq#3232) and we follow current jq. The differential suite
caught it the hard way: `ubuntu-latest` is rolling from 24.04 to 26.04, the
two images ship jq 1.7 and jq 1.8, and `diff_indent_0` asserts byte-for-byte
parity against whatever `jq` is on PATH. Nightly's `asan` job landed on 26.04
and went red with no change on our side -- and no implementation could satisfy
both halves of the fleet.

So the oracle is pinned the way ripgrep's already is: `scripts/install-jq-ci.sh`
verifies a SHA-256 before the binary reaches PATH and is wired into the three
jobs that run these tests (`ci.yml`'s `test` job, `coverage.yml`, nightly's
`asan`), with `JQ_VERSION` beside `RG_VERSION`. A pin does not help a
developer whose distro ships 1.7, so `assert_matches_from_jq((1, 8), ...)`
skips just the affected case on an older host oracle and says why -- the
bargain `minimum_bash_major` already strikes for the competitor corpus.

Checking the flag combinations turned up a second, pre-existing parity bug:
`-c` always won over `--indent N` and `--tab` always won over a later
`--indent N`, but real jq -- 1.7 and 1.8 alike -- is strictly last-flag-wins.
`jq -c --indent 4` was compact here and indented in real jq. The cause was
redundant state: a `compact_output` bool beside an `indent: Indent` whose
`Compact` variant already said the same thing, with the tie broken after
parsing. One field now holds the output shape and each of the three flags
overwrites it, so last-wins falls out of the parse order instead of being
arbitrated later.

Verified against a digest-checked jq 1.8.1 that `--indent 0` is the only
behavior anywhere in the differential suite that 1.7 and 1.8 disagree on, so
the precedence cases are asserted unconditionally and only `--indent 0` is
gated.

`yq` is deliberately untouched: its `-I 0` means compact, which is
mikefarah/yq's own documented semantics, not jq's.

Claude-Session: https://claude.ai/code/session_01HLxiDLRpTGkSoWxmLkxDeG
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
bashkit b01b4a6 Commit Preview URL

Branch Preview URL
Oct 21 2026, 09:30 AM

chaliy commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Closing this in favour of #2768, which now carries this commit plus the rest of what CI needs. Not abandoned — absorbed.

Why it had to move: this branch can't reach green on its own, and neither could #2768. The ubuntu-latest roll from 24.04 onto 26.04 broke CI in three independent ways, and Audit plus Test gate every PR, so each fix was blocked by the others:

Break Where it shows Fixed by
Oct 19–20 advisories (rustls-webpki, webpki-root-certs, certifi) Audit, every PR #2768's first commit
jq 1.8 changed --indent 0; oracle unpinned Test, Generate Rust Coverage, nightly asan this commit
Python 3.9 absent on the 26.04 image Test (Python 3.9) #2768's third commit
Ubuntu 26.04 ships uutils as coreutils, so dd/join/od/pr stop reporting GNU Generate Rust Coverage, and Test once jq is fixed #2768's third commit

That last one was hidden behind this very branch's failure: cargo test aborts on the jq target before the integration binary runs, so fixing jq alone would only have moved the Test job's red from one cause to another. It surfaced in the coverage job, which got further.

Two failures on this head are worth recording rather than losing to a close:

The jq work itself is unchanged in #2768: --indent 0 follows jq 1.8, the -c/--tab/--indent N precedence bug is fixed, and scripts/install-jq-ci.sh pins the oracle by digest so the runner image stops deciding.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant