Coming to yqr from jq

5 min read

yqr borrows jq’s shape on purpose. If you can write a jq path, you can already read and edit a YAML file with yqr – most of what you know transfers with the same spelling and the same meaning.

This page is about the edges: the one operator that means something different, the parts of jq that are not here, and the parts of yqr that jq has no way to express.

Examples run against this file:

# Edge services. Owned by the platform team.
name: web
replicas: 3
tags:
  - search
  - export
services:
  api:
    port: 8080      # public
  cache:
    port: 6379

What transfers unchanged

Your jq habitIn yqr
.same
.name, .a.b.csame
.["name"], ."a.b"same
.tags[0], .tags[-1]same
.tags[]same
.services[]same
a | bsame
f?same
a missing field yielding nullsame
to_entriessame
del(.a)same
+ - * / %, and + on stringssame
.a = 1same
.a |= (. + 1)same
.n = .n + 1same

So this is a yqr session, and there is nothing new in it:

$ yqr -r '.name' config.yaml
web
$ yqr '.services.api.port' config.yaml
8080
$ yqr -r '.tags[-1]' config.yaml
export
$ yqr -r '.services[] | .port' config.yaml
8080
6379

Ordering is the same too, which is worth saying because it is the kind of thing that bites silently: to_entries and .[] keep the order the document was written in, exactly as jq’s do. (jq’s keys sorts – that is why keys_unsorted exists – and yqr has neither.)

One habit to unlearn: +=

This is the only operator that is spelled the same and means something else, so it is the only thing on this page you have to actively remember.

In jq, += is addition or concatenation, and appending to a list takes a list on the right. In yqr, += means append one element to a sequence, and the right-hand side is the element:

jq 1.8.2yqr 0.8.0
.tags += ["x"]appends xparse error – yqr has no array literal
.tags += "x"appends the characters xappends x as one item
.replicas += 14error – += wants a sequence

To increment a number, use the update form or write the sum out. Both work:

$ yqr '.replicas |= (. + 1)' config.yaml | sed -n 3p
replicas: 4
$ yqr '.replicas = .replicas + 1' config.yaml | sed -n 3p
replicas: 4

And to append:

$ yqr '.tags += "audit"' config.yaml | sed -n '4,7p'
tags:
  - search
  - export
  - audit

What jq has that yqr does not

yqr walks and edits documents that already exist. It does not compute over data, and almost everything missing follows from that one line.

No filtering or aggregation. select, map, length, keys, has, add, join, sort_by, from_entries – none of them. to_entries is the only builtin.

No expressions that build values. No object or array construction ({}, []), no string interpolation, no comma operator, no //, no if/then/else, no comparisons, no and/or/not.

No recursive descent. .. has no equivalent.

No format conversion. yqr is YAML in and YAML out. There is no -o=json, and there should not be: the guarantee it exists to make is about the bytes of a YAML file, and there is nothing to preserve once you leave the format.

One edit per run. jq composes a whole program; yqr applies one mutation. Two changes are two commands.

Some of those fail with a message that teaches the model rather than just refusing:

$ yqr '.name, .replicas' config.yaml
yqr: lex error: unexpected character ',' at position 5: yqr has no ',' operator;
a function separates its arguments with ';', as in swap(.xs; 0; 1)

When you need any of it, reach for jq – convert on the way in and pipe:

$ yq -o=json '.' config.yaml | jq -c '[.tags[] | select(. == "export")]'
["export"]

That routes through yq, not yqr, because yq is the tool that converts formats. Nothing about using yqr commits you to using it for everything.

What jq cannot do at all

The trade has another side, and it is the reason yqr exists.

jq reads JSON into a data model and prints the model back. Comments, quote style, blank lines and alignment are not in that model – they are not data, so no jq filter can address them. yqr reads a lossless tree over your actual bytes, so they are addressable:

$ yqr -r 'line_comment(.services.api.port)' config.yaml
public
$ yqr 'line_comment(.replicas) = "bumped for the load test"' config.yaml | sed -n 3p
replicas: 3  # bumped for the load test

Keys and order are addressable the same way, without rebuilding the collection they live in:

$ yqr 'key(.tags) = "labels"' config.yaml | sed -n 4p
labels:
$ yqr 'swap(.tags; 0; 1)' config.yaml | sed -n '4,6p'
tags:
  - export
  - search

And the whole file survives a round trip, which no data-model tool can promise:

$ yqr '.' config.yaml | diff config.yaml - && echo identical
identical

That is the job yqr is for: changing one value in a file that a human is going to review, and leaving every other byte where it was.

Next