Selecting
Chapter 2's filters took everything — every
model, every name. Real questions are narrower:
which models, which machines, which events.
select is the filter that narrows. It takes a
test, passes through the values that pass, and
drops the rest.
select and comparisons
$ jq '.models[] | select(.details.context_length >= 16384) | .name' models.json
"ember-2:7b"
"ember-2:32b"
"harbor-4:70b"
Five models flow in. The test
.details.context_length >= 16384 is true for
three of them, so three values reach the final
.name. The comparisons are the usual set —
==, !=, <, <=, >, >= — and tests
combine with and, or, and not:
$ jq '.models[] | select(.details.family == "kestrel" and .details.context_length > 4096) | .name' models.json
"kestrel-1:8b"
$ jq -r '.models[] | select(.details.family == "ember" or .details.family == "harbor") | .name' models.json
ember-2:7b
ember-2:32b
harbor-4:70b
The test can be any expression that ends in true or false, including another filter's result. String builtins make common tests short — this one asks which models use a Q4 quant:
$ jq '.models[] | select(.details.quantization_level | startswith("Q4")) | .name' models.json
"ember-2:7b"
"ember-2:32b"
"kestrel-1:1.5b"
There are also endswith, contains, and test
for regular expressions; the cheat sheet in
examples/filters.txt can't list them all, and the
jq manual can.
//: the answer when there isn't one
Chapter 2 ended on null — the absent
capabilities of kestrel-1:1.5b. The //
operator, read "or else", replaces a missing,
null, or false value with an alternative:
$ jq '.models[] | .capabilities // "none recorded"' models.json
[
"completion",
"tools"
]
[
"completion"
]
[
"completion",
"vision"
]
"none recorded"
[
"completion",
"tools"
]
Four arrays and one plain string come out — //
doesn't care that the replacement has a different
type than the usual value; it cares that the stream
now has an answer for every model. Used inside a
reshaping (chapter 4), the same trick supplies a
default of matching shape:
$ jq '.models[] | select(.details.family == "kestrel") | {name, caps: (.capabilities // [])}' models.json
{
"name": "kestrel-1:8b",
"caps": [
"completion",
"vision"
]
}
{
"name": "kestrel-1:1.5b",
"caps": []
}
Note that // also swallows false. A key that is
present and deliberately false gets replaced
too — which is why garnet-07's status in the
inventory needs the longer spelling below.
Testing presence and type
select on a path tests the value; sometimes the
question is about the key. has answers it:
$ jq '[.models[] | select(has("capabilities"))] | length' models.json
4
Four of the five models carry the key. (The
[…] around the filter collects the stream back
into one array so length can count it — the
constructing brackets are chapter 4's subject.)
has's mirror image is in, which asks the same
question from the key's side:
$ jq -n '"name" | in({"name": 1, "size": 2})'
true
And type, met in chapter 2, earns its keep when
data is ragged. In the inventory, fjord-06 has no
services key at all; asking which machines have a
real services list is a type test:
$ jq '[.[] | select(.services | type == "array")] | length' inventory.json
7
Seven of eight — fjord-06's missing key is
null, null's type is "null", and the test
honestly excludes it. Combining has, type, and
// covers most of what ragged real-world JSON
throws at a filter. One last combination, the
careful way to say "not decommissioned" when the
key is often absent and sometimes false:
$ jq '[.[] | select(.decommissioned // false | not)] | map(.name)' inventory.json
[
"atlas-01",
"birch-02",
"copper-03",
"dune-04",
"elm-05",
"fjord-06",
"garnet-07"
]
.decommissioned // false turns "no key" into
false; not inverts; harbor-08, the one machine
marked "decommissioned": true, is the only one
missing from the list. Filters like this are where
jq starts repaying the learning curve: that
question, over a file of eight thousand machines
instead of eight, is the same one line.