Skip to the content.

boson(1)

Contents


NAME

boson — query JSON with jq-style path expressions and pipelines

SYNOPSIS

boson [-r | -e | --into=VAR | -E] EXPR [FILE]
boson (-h | --help | --examples | --caveats | --about | --boop)

EXPR is a path expression, or a pipeline of stages joined by ` | `. With no FILE, boson reads JSON from standard input.

DESCRIPTION

boson parses a JSON document into a flat key-value store and evaluates an expression against it, like a small jq. It reads standard input or a single file given as the final argument, and writes the selected value(s) in one of several output formats.

The name stands for “Bash Oriented Scripting Object Notation.”

NUL bytes. Bash variables cannot hold or detect NUL bytes. Any JSON string value containing a NUL will be silently truncated at the first one. This applies to all output modes. See CAVEATS.

Path expressions

A path expression selects a value or set of values from the document.

Expression Meaning
.key Top-level key
.key.sub Nested key
.arr[0] Array element by index
.arr[] Iterate all array elements (one per line)
.obj[].name Iterate an array, extract .name from each element

Internally a path becomes a compound key — .users[0].emailusers.0.email. Array iteration ([]) enumerates the numeric children under a prefix, in index order. A key that does not exist yields null. A non-leaf node (an object or array) is re-emitted as JSON.

Pipe expressions

Stages are joined by ` | ` (spaces required). Each stage receives the set of contexts produced by the previous one and transforms or filters them:

boson -r '.users[] | select(.active) | .name' < data.json

The two stage types are:

Predicates

Predicates are used inside select(...). The field reference may be .field (relative to the current context) or . (the whole element, for scalar iteration). String values on the right-hand side may be quoted ("value") or bare. Numeric comparisons use integer arithmetic.

Form Passes when
.field non-empty, not null, not false (truthy)
. current element is truthy
-n .field non-empty string (bash -n semantics; "false" passes)
-z .field empty or missing value
has(.field) key is present in document (even if null, false, or "")
.field == VALUE string equality (byte-for-byte)
.field != VALUE string inequality
.field -eq N numeric equality — missing/empty field coerces to 0
.field -ne N numeric inequality
.field < N or .field -lt N numeric less-than
.field > N or .field -gt N numeric greater-than
.field <= N or .field -le N numeric less-than-or-equal
.field >= N or .field -ge N numeric greater-than-or-equal
.field =~ PAT ERE regex match — covers startswith (^pfx), endswith (sfx$), contains

== vs -eq: == compares bytes; -eq compares integers. For well-formed JSON numbers they usually agree, but they differ in two important cases:

Use == when the field is a string or when exact byte identity matters. Use -eq / -ne when you’re comparing JSON numbers and want integer semantics, particularly when the field might be absent in some records.

The -n/-z distinction from truthy matters when a field holds the string "false" or "0": truthy rejects both, -n passes them (the string is non-empty).

The has / truthy distinction matters when a field exists but holds null or false: has(.field) passes, .field (truthy) does not.

OPTIONS

The output modes are mutually exclusive; choose at most one. With none, boson uses type-aware default output.

Short Long Meaning
  (default) Type-aware: strings quoted, numbers/booleans/null bare
-r --raw Raw values, unquoted (one per line when iterating)
-e --emit Every leaf under EXPR as sourceable var=value lines
  --into=VAR The value as VAR=value, or an array as VAR=(...)
-E --eponymous Like --emit; errors on variable name collisions — see CAVEATS

Help:

Short Long Meaning
-h --help Synopsis
  --examples Cookbook
  --caveats Known gotchas and limitations
  --about About boson
  --boop About the boop framework

--emit and --eponymous preserve the original document key order: they walk the parser’s companion ordered-key index, not the raw hash, so assignments emerge in source order — the same mechanism Data.JSON.stringify uses.

EXAMPLES

Reading values

boson '.name' < data.json             # "boop"      (default: strings quoted)
boson -r '.name' < data.json          # boop        (raw, unquoted)
boson '.database.host' < config.json  # nested key
boson '.scores[0]' < data.json        # first array element
boson '.version' package.json         # FILE as the final argument

Iterating arrays

boson '.tags[]' < data.json           # each element, one per line
boson -r '.users[].email' < data.json # one unquoted email per line
boson '.users[].age' < data.json      # a field extracted from each element

Non-leaf output

When an expression resolves to an object or array rather than a scalar, boson re-emits it as JSON:

boson '.database' < config.json       # {"host":"localhost","port":5432,...}
boson '.users[0]' < data.json         # {"name":"Alice","age":30}
boson '.users[]' < data.json          # one JSON object per line

Type-aware default output

boson '.count'   < d.json   # 42      (number, bare)
boson '.active'  < d.json   # true    (boolean, bare)
boson '.nothing' < d.json   # null
boson '.name'    < d.json   # "boop"  (string, quoted)

Pipe expressions and filtering

Pipe stages filter or transform an iterating result set. The output mode (-r, --into, etc.) applies to the final stage.

# Keep users where active is true, extract name
boson -r '.users[] | select(.active) | .name' < data.json

# Numeric comparison
boson '.orders[] | select(.total > 100) | .id' < data.json

# String equality
boson -r '.items[] | select(.type == "fruit") | .name' < data.json

# Regex — startswith, endswith, or contains
boson -r '.files[] | select(.name =~ "\.sh$") | .name' < data.json
boson -r '.logs[]  | select(.msg  =~ "error|warn") | .msg' < data.json

# Key existence — field present even if null or false
boson -r '.users[] | select(has(.email)) | .email' < data.json

# Non-empty string — field is set and not empty (unlike truthy, "false" passes)
boson -r '.items[] | select(-n .description) | .name' < data.json

# Missing or empty — inverse of the above
boson -r '.items[] | select(-z .description) | .name' < data.json

# Collect filtered results into a shell array
boson --into=active '.users[] | select(.active) | .name' < data.json
# → active=(Alice Carol)

# Non-leaf output through a pipe
boson '.users[] | select(.active)' < data.json   # one JSON object per line

Sourceable output for shell scripts

Pull configuration straight into shell variables instead of shelling out per value:

# Whole subtree as var=value lines, in document order
boson --emit '.database' < config.json
#   host=localhost
#   port=5432
#   ssl=false

# Load an entire document into the current shell
. <(boson --emit '.' < config.json)

# A single named value
boson --into=host '.database.host' < config.json    # host=localhost
. <(boson --into=port '.server.port' < config.json)

# An iterated field as an array
boson --into=names '.users[].name' < data.json       # names=(Alice Bob)

# Eponymous: variable named by the leaf key
boson -E '.database.host' < config.json              # host=localhost
boson -E '.database' < config.json                   # host=…  port=…  ssl=…

CAVEATS

NUL bytes

Bash variables cannot hold or detect NUL bytes. Any JSON string value containing a NUL will be silently truncated at the first one. This applies to all output modes — there is no warning and no error.

-E / --eponymous: variable name collisions

Variable names are derived from the key path relative to your expression root, with dots replaced by underscores. Two source paths that differ only in a key containing underscores versus nested dots will sanitize to the same variable name:

{ "a_b": 1, "a": { "b": 2 } }

boson -E '.' data.json
  → boson: -E collision: "a_b" and "a.b" both map to variable "a_b"

boson detects every collision before emitting anything and exits nonzero, listing all conflicting pairs. It never produces partial output. Options:

$() strips trailing newlines

All variable captures use $(...) substitution, which bash unconditionally strips trailing newlines from. A JSON string value ending in one or more newlines will lose them when assigned to a shell variable.

Predicate numeric comparisons

</-lt, >/-gt, <=/-le, >=/-ge, -eq, -ne all use integer arithmetic ((( ))). Floating-point values are truncated; non-numeric strings are coerced to 0; missing or empty fields are also coerced to 0, which means select(.count -eq 0) will match records where count is absent. Use == "0" (string equality) if you want to match the literal value 0 without catching missing fields.

Pipe spaces are required

The pipe separator is ` | ` — a space, pipe, space. A bare | without surrounding spaces is not recognized and will be treated as part of the expression, producing unexpected results.

EXIT STATUS

Diagnostics print to standard error beginning with boson:.

NOTES

boson is pure bash, built on the boop framework’s Data.JSON, Map.Fast, and Args classes. It began as a thought experiment — how much of jq can a bash OOP standard library express? — and it will never match a C-based JSON processor on speed. Its value is reach: it runs anywhere bash 4.3+ is present, with no jq, no Python, and no external dependencies, which makes it a dependable fallback on stripped containers and minimal hosts where those are absent.

The query engine operates on the flat key-value store, not on JSON syntax, so the same engine is intended to serve YAML and Config sources once those parsers feed the same backend. For repeated use, bundle boson with collider (collider bosonbundle-boson) to avoid the per-invocation framework load.

SEE ALSO

jq(1) — the tool boson imitates. docs/JSON.md for the parser, docs/Map.md for the backing store, docs/tools.md for the tool family, and TODO.md for the staged roadmap.