Skip to content

feat(yq): --properties-separator, --properties-array-brackets and --csv-separator for -o - #6940

Merged
newhoggy merged 2 commits into
mainfrom
issue-6922-yq-props-flags-csv-separator
Nov 11, 2026
Merged

newhoggy merged 2 commits into
mainfrom
issue-6922-yq-props-flags-csv-separator

Conversation

@newhoggy

Copy link
Copy Markdown
Contributor

Summary

Adds the three option flags of yq's props and csv output formats to the -o encoders merged with the first output-format slice: --properties-separator (an empty value keeps the default =), --properties-array-brackets (a[0] for an array element below the root) and --csv-separator (one byte; yq's own refusals of a longer value and of ", a line break and NUL are reproduced, including its object branch dropping the delimiter error). -o=tsv ignores --csv-separator, as yq does.

This is only part of issue 6922: the @props and @csv operators still ignore the flags here, and a props comment is still not written, so the issue stays open.

Test plan

  • cargo test --features cli,simd,regex,serde (14,592 passed before the last two review fixes; tests/yq_cli/i6922.rs rerun after them), both clippy invocations, cargo check --no-default-features, cargo test --no-default-features --lib, rustdoc -D warnings, fmt, changelog and doc-lines checks
  • differential against yq v4.53.3: 1,152 combinations of flag set, document and filter over props, csv and tsv; the only differences are the known "output before an encoder error" class
  • code review (medium): NUL added to the refused delimiters, more rows pinned

@github-actions

Copy link
Copy Markdown

Coverage

Total: 92.98% ⚪ 0 pp vs main

Comparing 3521337..42c4c9d (merge-base → PR head)

No per-file coverage changes vs main.

🔇 0 ignored region(s), 682 tolerated region(s)

ignore removes the lines from both reports; tolerate keeps them in the reported percentage but scores them against the baseline, so a cross-run flip cannot move a delta. Regions are read from each revision's own source.

File Kind Lines Rev Reason
src/bin/succinctly/jq_path_mode.rs tolerate 81 both unreachable: the same filter compiled above
src/bin/succinctly/jq_path_mode.rs tolerate 89-92 both defensive: reached only if the interpreter panics, which no test provokes
src/bin/succinctly/jq_runner.rs tolerate 1930 both unreachable: widening the shadow-candidate set never rejects a program the first parse accepted, for the identical reason run_jq's own analogous fallback (#2395) is tolerated -- see that line's own comment (#2950)
src/bin/succinctly/jq_runner.rs tolerate 2319 both unreachable while the walk and the loader agree on what fails: both resolve with resolve_module_in/resolve_data_file_in, parse with parse_program and name a cycle by canonical file, which the fuzz in #3573 held over thousands of programs; kept so drift prints the loader's failure and not 0 compile errors
src/bin/succinctly/jq_runner.rs tolerate 3046-3058 both unreachable: try_parse_meta_op only fires under ParserMode::Yq (src/jq/parser.rs), and rewrite_namespaced_calls is only reached via ModuleProcessor::process_program, which jq_runner's own jq-mode run is the sole caller of -- so a MetaAssign node can never reach this function (#798)
src/bin/succinctly/jq_runner.rs tolerate 3662 both unreachable by construction: every error this wrapper receives today is a decode or nesting-depth failure the evaluator raised itself, never error(v); kept so a future one is rendered rather than dropped (#2999)
src/bin/succinctly/jq_runner.rs tolerate 3663 both see the arm above (#2999)
src/bin/succinctly/jq_runner.rs tolerate 4092 both unreachable in practice today: def is only None when occurrences.module_def() is None, which (given origin is Some) would require an open run whose innermost frame has no def set at a check point that isn't itself inside a module-level def body -- but every module run is a strict chain of def nodes (the loader's own defs, each wrapping its dependency stubs INSIDE its own body via wrap_defs/dep_stubs_for) terminated by the run's own end marker, so a diagnosable call/var/break site is always reached either inside a module-level def's body (module_def Some) or outside every run (origin None) -- and even when def is Some, self.defs is always ModuleSource::read's own re-parse of the exact same file run_id_for interned this origin's id from, so collect_def_sites always finds the matching (name, arity, ordinal) span. Kept as a defensive fallback rather than a panic/unwrap in case that invariant is ever violated (#3085)
src/bin/succinctly/jq_runner.rs tolerate 4341-4344 both unreachable in a single-process run by construction: run_id_for (the sole source of an origin id) always inserts a run_origins entry for the id it hands back -- from a real load's canonical path, or its own literal-path fallback on a resolve failure -- and a def body only ever gets stamped with an origin after its module loaded successfully, so at always names a file that existed and was readable moments earlier. Reaching this arm needs that same file to vanish (or become unreadable) in the narrow window between that load and this re-read, entirely outside this process's control (#2964)
src/bin/succinctly/jq_runner.rs tolerate 4769 both unreachable: widening the shadow-candidate set never rejects a program the first parse accepted -- a newly covered name only wraps an already-successful dedicated parse, and a failing one would have propagated its error in the first parse too, so the retry budget is charged at the identical sites in both (#2395)
src/bin/succinctly/jq_runner.rs tolerate 5339-5348 both defensive: the splitter stops only at a fault jq's parser also meets, so slurp_fault names one; kept so a disagreement between them prints the old line-0 message rather than none (#5555)
src/bin/succinctly/jq_runner.rs tolerate 5850-5858 both defensive: the splitter stops only at a fault jq's parser also meets, so jq_stream_fault names one; kept so a disagreement between them prints the old line placement rather than none (#5555)
src/bin/succinctly/jq_runner.rs tolerate 5950-5954 both defensive: the only malformed document get_inputs returns here is a slurped stream, which carries the read that met its fault unless slurp_fault_at finds none (unreachable, see there); kept as the line-0 placement every other malformed document gets (#5555)
src/bin/succinctly/jq_runner.rs tolerate 6976 both untestable: stdin failing part-way
src/bin/succinctly/jq_runner.rs tolerate 8564 both unreachable: the prefix parse stopped at value parsed, which the splitter either delimits or stopped at, so a read is found; kept so a disagreement falls back to line 0 (#5555)
src/bin/succinctly/jq_runner.rs tolerate 10711 both untestable: stdin failing part-way
src/bin/succinctly/jq_runner.rs tolerate 12225-12232 both unreachable: the splitter is the more lenient of the two and the prefix parse materializes from its spans, so it cannot find fewer values -- the same internal-error guard as the non-resuming branch below (#1064), kept per segment (#4311)
src/bin/succinctly/jq_runner.rs tolerate 12329-12331 both unreachable: the read starts on a UTF-8 continuation byte, which no JSON token begins with, so the parser always names a fault in it; kept so a read it could not name is still an error and not silently dropped (#5975)
src/bin/succinctly/jq_runner.rs tolerate 12346-12348 both unreachable with the arm above: the parser always names the fault, so the dropped bytes are never counted by hand (#5975)
src/bin/succinctly/jq_runner.rs tolerate 12508-12514 both untestable: a read that fails after the file opened
src/bin/succinctly/jq_runner.rs tolerate 12551-12555 both unreachable: the bytes were validated above
src/bin/succinctly/jq_runner.rs tolerate 12560-12565 both untestable: a read that fails after the file opened
src/bin/succinctly/jq_runner.rs tolerate 12579-12583 both untestable: a read that fails after the file opened
src/bin/succinctly/jq_runner.rs tolerate 12853 both unreachable: called with the source just read at the front
src/bin/succinctly/jq_runner.rs tolerate 12962-12968 both unreachable: the splitter is the more lenient of the two parsers and the prefix parse materializes from its spans, so it cannot find fewer values (the guard read_json_tail keeps per segment, #4311)
src/bin/succinctly/jq_runner.rs tolerate 13053-13060 both unreachable: the splitter is the more lenient of the two parsers and the prefix parse materializes from its spans, so it cannot find fewer values (the guard read_json_tail keeps per segment, #4311)
src/bin/succinctly/jq_runner.rs tolerate 13220 both unreachable: a stitch cuts at a token's first byte, which is ASCII
src/bin/succinctly/jq_runner.rs tolerate 13877 both unreachable: the one source pushed above
src/bin/succinctly/jq_runner.rs tolerate 13921 both defensive: an RS inside a value makes the walk warn (a truncated record), which returns above; kept so a walk that stopped warning cannot leave an RS in a value the lazy route then reads
src/bin/succinctly/jq_runner.rs tolerate 14003 both unreachable: a file holds a document, so there is a last one
src/bin/succinctly/jq_runner.rs tolerate 16282 both unreachable: no M2-streamable program halts, so stats.halt is None here, as in the uncolored arm below (#5415)
src/bin/succinctly/jq_runner.rs tolerate 16283 both unreachable: see the line above (#5415)
src/bin/succinctly/jq_runner.rs tolerate 16284 both unreachable: see the line above (#5415)
src/bin/succinctly/jq_runner.rs tolerate 21263 both unreachable: this test builds only ResolveError::Call values (#3313)
src/bin/succinctly/jq_runner.rs tolerate 25819 both unreachable in a passing suite by design -- the fixed b\
src/bin/succinctly/main.rs tolerate 1568 both unreachable given clap 4.6's own unknown_argument() error constructor: every ErrorKind::UnknownArgument it builds sets ContextKind::InvalidArg to ContextValue::String(arg) in the same call, so this arm only guards a future clap release changing that invariant
src/bin/succinctly/main.rs tolerate 1737 both llvm-cov line-attribution artifact, not unreachable: test_negative_filter_accepted_via_sjq_multicall_alias_3389's '-x' row demonstrably reaches the e.exit() two lines below (exit code 2, confirmed by that test passing), which this if-let's own closing brace sits directly above -- the brace itself is never credited a hit, the same class of artifact eval.rs's own tolerate list documents for other closing braces (#3389)
src/bin/succinctly/main.rs tolerate 1993 both llvm-cov line-attribution artifact, not unreachable: test_negative_filter_boundary_characters_3389's '-x'/'-n1'/'--bogus' rows demonstrably reach the e.exit() two lines below (exit code 2, confirmed by that test passing), which this if-let's own closing brace sits directly above -- the brace itself is never credited a hit, the same class of artifact eval.rs's own tolerate list documents for other closing braces (#3389)
src/bin/succinctly/main.rs tolerate 2017 both the CLI suites drive yq through the syq multi-call arm above; this arm is the same call reached only when spelled succinctly yq (#2999)
src/bin/succinctly/output.rs tolerate 1384 both unreachable: a writer emits a placeholder whole, so the text never ends inside one (#5497)

642 more not shown: the full report is in the coverage-comment-x86_64 artifact.

Touched files absent from every coverage report (4)
  • changelog.d/6922.added.md
  • docs/compliance/yq/limitations.md
  • docs/guides/cli.md
  • tests/yq_cli/i6922.rs

These files may contain no executable code or may have been omitted from the coverage run.

Patch coverage

Patch: 100% (51/51 new lines covered)

File Patch Uncovered new lines
src/bin/succinctly/yq_runner.rs 100% (9/9) —
src/jq/eval.rs 100% (42/42) —

📦 Full per-file coverage summary · run summary

@github-actions

Copy link
Copy Markdown

Coverage

Total: 93.03% ⚪ 0 pp vs main

Comparing 3521337..42c4c9d (merge-base → PR head)

No per-file coverage changes vs main.

🔇 0 ignored region(s), 681 tolerated region(s)

ignore removes the lines from both reports; tolerate keeps them in the reported percentage but scores them against the baseline, so a cross-run flip cannot move a delta. Regions are read from each revision's own source.

File Kind Lines Rev Reason
src/bin/succinctly/jq_path_mode.rs tolerate 81 both unreachable: the same filter compiled above
src/bin/succinctly/jq_path_mode.rs tolerate 89-92 both defensive: reached only if the interpreter panics, which no test provokes
src/bin/succinctly/jq_runner.rs tolerate 1930 both unreachable: widening the shadow-candidate set never rejects a program the first parse accepted, for the identical reason run_jq's own analogous fallback (#2395) is tolerated -- see that line's own comment (#2950)
src/bin/succinctly/jq_runner.rs tolerate 2319 both unreachable while the walk and the loader agree on what fails: both resolve with resolve_module_in/resolve_data_file_in, parse with parse_program and name a cycle by canonical file, which the fuzz in #3573 held over thousands of programs; kept so drift prints the loader's failure and not 0 compile errors
src/bin/succinctly/jq_runner.rs tolerate 3046-3058 both unreachable: try_parse_meta_op only fires under ParserMode::Yq (src/jq/parser.rs), and rewrite_namespaced_calls is only reached via ModuleProcessor::process_program, which jq_runner's own jq-mode run is the sole caller of -- so a MetaAssign node can never reach this function (#798)
src/bin/succinctly/jq_runner.rs tolerate 3662 both unreachable by construction: every error this wrapper receives today is a decode or nesting-depth failure the evaluator raised itself, never error(v); kept so a future one is rendered rather than dropped (#2999)
src/bin/succinctly/jq_runner.rs tolerate 3663 both see the arm above (#2999)
src/bin/succinctly/jq_runner.rs tolerate 4092 both unreachable in practice today: def is only None when occurrences.module_def() is None, which (given origin is Some) would require an open run whose innermost frame has no def set at a check point that isn't itself inside a module-level def body -- but every module run is a strict chain of def nodes (the loader's own defs, each wrapping its dependency stubs INSIDE its own body via wrap_defs/dep_stubs_for) terminated by the run's own end marker, so a diagnosable call/var/break site is always reached either inside a module-level def's body (module_def Some) or outside every run (origin None) -- and even when def is Some, self.defs is always ModuleSource::read's own re-parse of the exact same file run_id_for interned this origin's id from, so collect_def_sites always finds the matching (name, arity, ordinal) span. Kept as a defensive fallback rather than a panic/unwrap in case that invariant is ever violated (#3085)
src/bin/succinctly/jq_runner.rs tolerate 4341-4344 both unreachable in a single-process run by construction: run_id_for (the sole source of an origin id) always inserts a run_origins entry for the id it hands back -- from a real load's canonical path, or its own literal-path fallback on a resolve failure -- and a def body only ever gets stamped with an origin after its module loaded successfully, so at always names a file that existed and was readable moments earlier. Reaching this arm needs that same file to vanish (or become unreadable) in the narrow window between that load and this re-read, entirely outside this process's control (#2964)
src/bin/succinctly/jq_runner.rs tolerate 4769 both unreachable: widening the shadow-candidate set never rejects a program the first parse accepted -- a newly covered name only wraps an already-successful dedicated parse, and a failing one would have propagated its error in the first parse too, so the retry budget is charged at the identical sites in both (#2395)
src/bin/succinctly/jq_runner.rs tolerate 5339-5348 both defensive: the splitter stops only at a fault jq's parser also meets, so slurp_fault names one; kept so a disagreement between them prints the old line-0 message rather than none (#5555)
src/bin/succinctly/jq_runner.rs tolerate 5850-5858 both defensive: the splitter stops only at a fault jq's parser also meets, so jq_stream_fault names one; kept so a disagreement between them prints the old line placement rather than none (#5555)
src/bin/succinctly/jq_runner.rs tolerate 5950-5954 both defensive: the only malformed document get_inputs returns here is a slurped stream, which carries the read that met its fault unless slurp_fault_at finds none (unreachable, see there); kept as the line-0 placement every other malformed document gets (#5555)
src/bin/succinctly/jq_runner.rs tolerate 6976 both untestable: stdin failing part-way
src/bin/succinctly/jq_runner.rs tolerate 8564 both unreachable: the prefix parse stopped at value parsed, which the splitter either delimits or stopped at, so a read is found; kept so a disagreement falls back to line 0 (#5555)
src/bin/succinctly/jq_runner.rs tolerate 10711 both untestable: stdin failing part-way
src/bin/succinctly/jq_runner.rs tolerate 12225-12232 both unreachable: the splitter is the more lenient of the two and the prefix parse materializes from its spans, so it cannot find fewer values -- the same internal-error guard as the non-resuming branch below (#1064), kept per segment (#4311)
src/bin/succinctly/jq_runner.rs tolerate 12329-12331 both unreachable: the read starts on a UTF-8 continuation byte, which no JSON token begins with, so the parser always names a fault in it; kept so a read it could not name is still an error and not silently dropped (#5975)
src/bin/succinctly/jq_runner.rs tolerate 12346-12348 both unreachable with the arm above: the parser always names the fault, so the dropped bytes are never counted by hand (#5975)
src/bin/succinctly/jq_runner.rs tolerate 12508-12514 both untestable: a read that fails after the file opened
src/bin/succinctly/jq_runner.rs tolerate 12551-12555 both unreachable: the bytes were validated above
src/bin/succinctly/jq_runner.rs tolerate 12560-12565 both untestable: a read that fails after the file opened
src/bin/succinctly/jq_runner.rs tolerate 12579-12583 both untestable: a read that fails after the file opened
src/bin/succinctly/jq_runner.rs tolerate 12853 both unreachable: called with the source just read at the front
src/bin/succinctly/jq_runner.rs tolerate 12962-12968 both unreachable: the splitter is the more lenient of the two parsers and the prefix parse materializes from its spans, so it cannot find fewer values (the guard read_json_tail keeps per segment, #4311)
src/bin/succinctly/jq_runner.rs tolerate 13053-13060 both unreachable: the splitter is the more lenient of the two parsers and the prefix parse materializes from its spans, so it cannot find fewer values (the guard read_json_tail keeps per segment, #4311)
src/bin/succinctly/jq_runner.rs tolerate 13220 both unreachable: a stitch cuts at a token's first byte, which is ASCII
src/bin/succinctly/jq_runner.rs tolerate 13877 both unreachable: the one source pushed above
src/bin/succinctly/jq_runner.rs tolerate 13921 both defensive: an RS inside a value makes the walk warn (a truncated record), which returns above; kept so a walk that stopped warning cannot leave an RS in a value the lazy route then reads
src/bin/succinctly/jq_runner.rs tolerate 14003 both unreachable: a file holds a document, so there is a last one
src/bin/succinctly/jq_runner.rs tolerate 16282 both unreachable: no M2-streamable program halts, so stats.halt is None here, as in the uncolored arm below (#5415)
src/bin/succinctly/jq_runner.rs tolerate 16283 both unreachable: see the line above (#5415)
src/bin/succinctly/jq_runner.rs tolerate 16284 both unreachable: see the line above (#5415)
src/bin/succinctly/jq_runner.rs tolerate 21263 both unreachable: this test builds only ResolveError::Call values (#3313)
src/bin/succinctly/jq_runner.rs tolerate 25819 both unreachable in a passing suite by design -- the fixed b\
src/bin/succinctly/main.rs tolerate 1568 both unreachable given clap 4.6's own unknown_argument() error constructor: every ErrorKind::UnknownArgument it builds sets ContextKind::InvalidArg to ContextValue::String(arg) in the same call, so this arm only guards a future clap release changing that invariant
src/bin/succinctly/main.rs tolerate 1737 both llvm-cov line-attribution artifact, not unreachable: test_negative_filter_accepted_via_sjq_multicall_alias_3389's '-x' row demonstrably reaches the e.exit() two lines below (exit code 2, confirmed by that test passing), which this if-let's own closing brace sits directly above -- the brace itself is never credited a hit, the same class of artifact eval.rs's own tolerate list documents for other closing braces (#3389)
src/bin/succinctly/main.rs tolerate 1993 both llvm-cov line-attribution artifact, not unreachable: test_negative_filter_boundary_characters_3389's '-x'/'-n1'/'--bogus' rows demonstrably reach the e.exit() two lines below (exit code 2, confirmed by that test passing), which this if-let's own closing brace sits directly above -- the brace itself is never credited a hit, the same class of artifact eval.rs's own tolerate list documents for other closing braces (#3389)
src/bin/succinctly/main.rs tolerate 2017 both the CLI suites drive yq through the syq multi-call arm above; this arm is the same call reached only when spelled succinctly yq (#2999)
src/bin/succinctly/output.rs tolerate 1384 both unreachable: a writer emits a placeholder whole, so the text never ends inside one (#5497)

641 more not shown: the full report is in the coverage-comment-ARM64 artifact.

Touched files absent from every coverage report (4)
  • changelog.d/6922.added.md
  • docs/compliance/yq/limitations.md
  • docs/guides/cli.md
  • tests/yq_cli/i6922.rs

These files may contain no executable code or may have been omitted from the coverage run.

Patch coverage

Patch: 100% (51/51 new lines covered)

File Patch Uncovered new lines
src/bin/succinctly/yq_runner.rs 100% (9/9) —
src/jq/eval.rs 100% (42/42) —

📦 Full per-file coverage summary · run summary

@newhoggy
newhoggy added this pull request to the merge queue Nov 11, 2026
Merged via the queue into main with commit 957cd28 Nov 11, 2026
54 checks passed
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