yqr yqr ("YAML query in Rust") is a fidelity-first, jq-style command-line tool for YAML. It queries and edits YAML while preserving every byte it was not asked to change — comments, quoting, indentation, key order, and line endings all survive. Byte-exact reads, by default. yqr '.' file.yaml reproduces the input exactly — no flag, no reflow. Surgical edits. yqr -i '.spec.replicas = 5' deploy.yaml rewrites only the bytes the filter targets, or refuses — clean diffs, guaranteed. jq is JSON-only and cannot preserve YAML formatting at all; yq edits in place but its docs admit comment and whitespace issues. yqr changes nothing but the edit site, or errors. Native YAML, no JSON round-trip. Parsing and emission run through the noyalib engine — the lossless CST behind both the default read path and the --normalize pipeline; the CLI uses clap. Install / build Install the published crate from crates.io: cargo install yqr # binary at ~/.cargo/bin/yqr Or build from a source checkout (requires the Rust 1.97 toolchain, pinned via rust-toolchain.toml): cargo build --release # binary at target/release/yqr Usage yqr [OPTIONS] <FILTER> [FILE] Arguments: <FILTER> The jq-style filter to apply (e.g. '.foo.bar', '.items[]') [FILE] Input YAML file; reads stdin when omitted or '-' Options: -r, --raw-output Emit string results without YAML quoting -N, --normalize Re-serialize output (drop comments, canonicalize scalars) -i, --in-place Edit the input file in place (mutating filters only) --engine <ENGINE> Backend parser for byte-preserving reads (default: noyalib) -h, --help Print help -V, --version Print version Examples # Field access echo 'name: yqr version: 1' | yqr .name # => yqr # Nested access + array indexing echo 'authors: - name: ada - name: linus' | yqr -r '.authors[0].name' # => ada # Negative indexing (from the end) echo 'tags: [cli, yaml]' | yqr -r '.tags[-1]' # => yaml # Iterate a collection (one result per line) echo 'tags: [a, b, c]' | yqr -r '.tags[]' # => a # b # c # Pipe composition echo 'a: {b: {c: 42}}' | yqr '.a | .b | .c' # => 42 # Optional `?` suppresses errors echo 'name: yqr' | yqr '.name[]?' # prints nothing, exits 0 Byte-preserving reads (default) and --normalize yqr preserves formatting by default. Untouched nodes are emitted as their original source bytes, so the identity filter reproduces the input exactly — comments, quoting, indentation, and line endings all survive. Pass --normalize (-N) to re-serialize the output instead, which canonicalizes scalars and drops comments. --engine <name> selects which backend parser performs the byte-preserving read (default noyalib, the always-available lossless CST). Under --normalize the re-serializing pipeline runs and the engine choice has no observable effect (an unknown name is still rejected up front). # Identity reproduces the file byte-for-byte -- comments, blank lines, # quoting, block scalars, CRLF, BOM, and multi-document streams survive yqr '.' config.yaml | diff config.yaml - # no diff (no flag needed) # Projections keep the original spelling echo "zip: 007" | yqr '.zip' # => 007 (not 7) echo "s: 'hi'" | yqr '.s' # => 'hi' (quotes kept) # --normalize re-serializes (lossy: drops comments, canonicalizes scalars) echo "zip: 007" | yqr --normalize '.zip' # => 7 (re-typed) yqr --normalize '.' config.yaml # comments dropped, scalars canonicalized Results that are computed rather than selected (and nodes an engine cannot address faithfully — entries merged in via <<, alias references) fall back to the regular typed rendering. Multi-document inputs run the filter against every document. -r keeps its usual meaning and prints string values. Fidelity notes: Projected nested block collections are emitted at their original indentation (the slice is extended to the line start), so the output is uniformly indented and re-parses to the selected value. Empty input produces no output in the default (byte-preserving) mode (byte-identity with the empty file), where --normalize prints null. The noyalib backend's value model has string-only mapping keys: non-string keys (true:, 8080:) are matched by spelling; distinct keys that collide after string conversion (1 and "1") are refused with an error. Duplicate keys resolve last-wins and emit the last occurrence's real bytes. Keep-chomped (|+) block scalars retain their kept trailing blank lines, alias references project the anchor's real bytes, block-collection spans start at their first line's indent, and classic-Mac CR-only line endings are accepted. Surgical edits (=, +=, del, -i) yqr can also edit YAML, not just read it — and it changes only the bytes the filter targets, leaving every other byte (comments, indentation, quoting, key order) untouched, or refuses. Edits always run through the fidelity engine, so a mutating filter is byte-exact except at the edit site. The mutation surface: Filter Meaning <path> = <value> Replace the scalar at path (style-matched quoting) <path>.<newkey> = <value> Add a new mapping entry under an existing mapping <path> += <value> Append an item to the block sequence at path del(<path>) Remove the block entry at path (single- or multi-line) <value> is a scalar literal (5, 1.5, "web", true, false, null) or a .-rooted path that copies the value found at another location. # Replace a value; the comment and every other line are preserved verbatim echo 'spec: replicas: 3 # keep me image: web' | yqr '.spec.replicas = 5' # => spec: # replicas: 5 # keep me # image: web # Append to a block sequence at the right indent yqr '.spec.ports += 9090' deploy.yaml # Add a new key, delete an entry (a nested/multi-line block closes up cleanly) yqr '.metadata.env = "prod"' deploy.yaml yqr 'del(.metadata.labels)' deploy.yaml yqr 'del(.spec.template)' deploy.yaml # Edit the file in place (rewritten atomically: temp file + rename) yqr -i '.spec.replicas = 5' deploy.yaml git diff deploy.yaml # touches only that one line Guarantees and limits: Structural integrity. An edit whose result would re-parse to a different structure is refused (exit 5) rather than emitted; under -i the file is left unchanged. No-match is a no-op. A filter that matches no node succeeds and leaves the document unchanged (jq/yq semantics), so del(.x) across a batch of files does not fail the ones that lack .x. -i needs a file. Using --in-place with stdin, or with a read-only filter, is an error (diagnosed before any input is read). Writes are atomic (temp file + fsync + rename) and edit through a symlink to the real file; the original mode is preserved. Owner/group, SELinux context, ACLs, extended attributes, and hardlinks are not carried across the replace — the same temp-file+rename tradeoff sed -i makes. Multi-document. The edit applies to each document whose path resolves; the others are emitted byte-identically. Scalar RHS only. =, +=, and new-key values are scalars (number, string, bool, null) or a path copying a scalar; a collection RHS is refused. Structural delete. del removes multi-line and nested block entries too, not just single-line ones; it closes up the entry's lines and leaves every surviving byte identical. Deleting the only entry of a block (which would empty it) or an item of a flow collection ([a, b]) is refused with a clear message. Unsupported operations. Computed updates (|=), key rename, and sequence reorder / comment edits each fail with a clear message. Query filters Filter Meaning . Identity .foo Field access (.["foo"] for non-bareword keys) .a.b Nested field access .[n] Array index (.[-1] counts from the end) .[] Iterate sequence elements / mapping values a | b Pipe f? Suppress errors from f Planned: object/array construction, builtins (length, keys, select, map, …), arithmetic, multi-document/slurp mode, and more. See the spec. Using yqr in Kubernetes (and beyond) Install paths and recipes for running yqr against kubectl output, baking it into a container image, and reading CI configs, Compose files, Ansible playbooks, OpenAPI specs, alerting rules, and app config: zoosky.github.io/yqr/docs/content/home.html (source: docs/content/home.html). Architecture filter ──▶ lexer ──▶ parser ──▶ Ast ──▶ evaluator ──▶ Value(s) ──▶ YAML YAML ──▶ noyalib::from_str ──▶ Value ──┘ Module Responsibility src/lexer.rs Filter string → tokens src/parser.rs Tokens → Ast src/ast.rs Filter AST node definitions src/eval.rs Ast × Value → stream of Value src/value.rs yqr's Value model (converts to/from noyalib) src/fidelity/ Byte-preserving read engine (default reads) + write tier (src/fidelity/write.rs) src/error.rs YqrError + jq-style exit-code mapping src/cli.rs clap argument parsing src/lib.rs Public API (eval_str, render) src/main.rs Binary entry + exit-code mapping Testing cargo test # unit + integration + CLI tests cargo fmt --all -- --check cargo clippy --all-targets --all-features -- -D warnings Unit tests live alongside each module. tests/integration.rs exercises the public library API end-to-end. tests/cli.rs runs the compiled binary against piped input. Benchmarks Criterion benchmarks live in benches/ (cargo bench --bench eval). Every push to main runs them in CI and publishes the results to a tracked history: Live benchmark dashboard — performance over time, with alerts on >30% regressions.
