Hubuum Pipe DSL¶
Hubuum CLI commands can be followed by pipe stages that filter, reshape, group, aggregate, and extract semantic output before final rendering.
The DSL is useful when a command already returns the right kind of data and you want a smaller local view without adding another API flag.
The native-language increments are specified in RFC 0001 and land in independently reviewable slices. Typed boolean predicates, multi-key sorting, strict IP sorting, projection aliases, stable distinct, global aggregation with selector counts, and ordered first/last aggregates are implemented; later RFC sections remain proposals until their linked delivery issues land.
Grouping with G and aggregation with A are local pipe operations over the
values returned by the preceding command. A GLOBAL aggregates the complete
current local collection. For permission-scoped aggregation over the complete
matching object set before pagination, use object aggregate; see the main
README for command examples.
Examples below use REPL/script syntax. In a POSIX shell, escape or quote |,
>, and >> so those standalone operator arguments reach Hubuum CLI, for
example hubuum-cli config show \| F output \> output.txt.
Quick Recipes¶
Keep Host objects whose OS version contains 26:
Show a few Host fields:
object list --class Hosts | P Name os_version data.network.interfaces[*].ipv4
object list --class Hosts --computed S:average_load --computed P:note | P Name S:average_load P:note
Sort by a numeric data field:
object list --class Hosts | S data.cpu.cores AS num
object list --class Hosts --computed S:average_load | S S:average_load desc AS num
Group Hosts by OS version and count them:
Sort aggregate output by the aggregate number:
Extract only IPv4 values:
Write one JSON file per Host:
Search And Filter¶
Bare text and the one-argument F form are broad quick searches over key paths
and all semantic values, including values not selected as visible table
columns. Matches found only in hidden values are reported as value in the
Match column when visible-column metadata causes that column to be generated.
The two-argument F <field> <regex> form searches one field. Compact embedded
operators provide equality, regex, and numeric/string comparisons without
being confused with a standalone redirect operator:
object list --class Hosts | F 129.240
object list --class Hosts | F os_version 26
object list --class Hosts | F data.cpu.cores>=8
object list --class Hosts | F data.network.interfaces[*].ipv4 '^129\.240\.'
object list --class Hosts --computed S:average_load | F S:average_load>=1
object list --class Hosts --computed P:note | F P:note '^mine$'
V searches values only:
K searches keys only and returns the matched key projection:
reject removes matching rows:
F WHERE starts the typed predicate grammar. It preserves quoted strings and
JSON literal types, supports NOT, AND, OR, and parentheses, and leaves all
legacy filter forms unchanged. Precedence is parentheses, NOT, a field test,
AND, then OR:
object list --class Hosts | F WHERE data.cpu.cores AS num >= 8 AND state IN ["ready", "running"]
object list --class Hosts | F WHERE NOT (owner IS MISSING OR owner IS NULL)
object list --class Hosts | reject WHERE status == "retired" OR disabled == true
Typed tests support =, ==, !=, <, <=, >, >=, ~, !~,
MATCHES, [NOT] IN [...], IS [NOT] NULL, and IS [NOT] MISSING.
Keywords are ASCII case-insensitive. Double-quoted strings use JSON escapes;
single-quoted strings accept \\ and \' and otherwise preserve their text.
Numbers, booleans, and null are unquoted JSON literals; strings must be
quoted, so 3 differs from "3".
An optional AS str|num|bool|ip|datetime|version|natural cast makes conversion
explicit. Invalid literals fail while parsing. An invalid selected value stops
evaluation and reports the stage, selector, cast, 1-based row, and offending
JSON value. Boolean evaluation short-circuits left to right.
Positive comparisons, matches, and IN are existential for fanout selectors.
Their negative forms require at least one selected value and require every
selected value to pass, so a missing selector does not satisfy !=, !~, or
NOT IN. IS NULL detects selected JSON null; IS MISSING detects no selector
matches. Their IS NOT forms and general NOT are exact logical negations.
? removes empty values, or keeps rows where a selector is truthy:
Projection And Values¶
P selects columns. Selectors can be separated by spaces or commas.
object list --class Hosts | P Name os_version
object list --class Hosts | P Name,data.cpu.cores
object list --class Hosts | P Name AS Host, data.network.interfaces[].ipv4 AS Addresses
object list --class Hosts | P Name data !data.secrets
object list --class Hosts | P data.network.interfaces !data.network.interfaces[].mac
object show --class Hosts host-1 --computed S:average_load --computed P:note | P Name S:average_load P:note
Prefix a selector with ! to remove its matches from the projected value. Drop
terms use the same dotted fields, indexes, negative indexes, fanout, and slices
as keep terms. A terminal index removes that array element, a terminal slice
removes the selected range, and a terminal [] or [*] empties the selected
array. Missing paths are harmless. Array traversal must be explicit, so use
!items[].secret, not !items.secret, to remove a field from every item.
An optional AS alias creates one top-level output field. No selector match
becomes null, one match stays scalar, and multiple matches become an array. If
any term uses AS, commas are required between every term so alias boundaries
are unambiguous. Drop terms cannot use AS.
Aliases use the crate's validated output-name type. Empty names, duplicate final names, and names that would overwrite an existing group or aggregate field fail before rendering. Grouped projection changes only visible summaries and keeps member rows attached for later aggregation. Unaliased and drop terms retain their existing behavior and whitespace-separated legacy syntax.
Shared and personal computed fields use the ordinary top-level selectors
S:<key> and P:<key> after they are selected with the repeatable
--computed option (or --computed all). Their underlying JSON types are
retained for semantic pipe operations. Computation errors are represented as
ERROR: ... strings.
Selections configured in output.object_class_computed_fields.<class> are
available to the pipeline automatically. Explicit --computed values replace
the class default; --computed none disables it for one command.
VALUE and VAL extract selected leaves as a value list:
object list --class Hosts | VALUE data.network.interfaces[*].ipv4
object list --class Hosts | VAL Name
Stable Distinct¶
D and distinct retain the first occurrence and preserve input order, shape,
and column metadata. With no keys, equality uses the complete visible JSON
value and ignores object field order:
Comma-separated keys form an ordered selector tuple. A fanout selector's whole
ordered sequence participates in equality; it is not reduced to its first
match. Missing is a distinct sentinel and does not equal a selected JSON null.
Keys accept the shared strict casts str, num, bool, ip, datetime,
version, and natural.
object list --class Hosts | D owner, os_version
object list --class Hosts | D data.network.interfaces[].ipv4 AS ip
Distinct supports empty, line, row, value, and grouped collections while
rejecting detail and message output explicitly. Whole-value line distinct uses
D without keys. For groups, equality sees only visible group and aggregate
summaries, and duplicate whole groups are removed without merging hidden member
rows.
Sorting, Limits, And Counts¶
Sort ascending by default:
Use ! or desc for descending order:
Keys can be comma-separated. They compare lexicographically in declaration order, and rows equal on every key retain their original order:
Each key accepts these modifiers in order:
The strict casts are str, num, bool, ip, datetime, version, and
natural. Invalid strict casts stop the stage and identify the key, selector,
row, and offending JSON value.
AS ip accepts only values parsed by std::net::IpAddr. IPv4 compares by its
numeric 32-bit value and IPv6 by its numeric 128-bit value. IPv4 precedes IPv6
ascending, and descending reverses that valid order. IPv4-mapped IPv6 remains
in the IPv6 family; invalid addresses never become zero or null.
Fanout selectors use the first selected value by default. USING min and
USING max cast every selected non-null value before reducing it. A missing
selector or a selection containing no non-null values supplies a null key.
Nulls default to last for both ascending and descending sorts; override this
per key with NULLS FIRST or NULLS LAST.
Use casts when text ordering is not right:
object list --class Hosts | S data.cpu.cores AS num
object list --class Hosts | S data.network.interfaces[0].ipv4 AS ip
object list --class Hosts | S data.network.interfaces[].ipv4 AS str USING min, Name AS natural
object list --class Hosts | G os_version AS "OS Version" | A count AS Hosts | S Hosts desc AS num
Limit rows:
object list --class Hosts | L 10
object list --class Hosts | L 10 20
object list --class Hosts | tail 5
Count rows:
Grouping And Aggregates¶
These stages operate locally on the current semantic rows. They do not call the server's object-aggregate endpoint.
G establishes a grouped-state boundary. Stages before G operate on member
rows. Stages after G operate on each visible summary, which is the flattened
set of group aliases and aggregate aliases:
F,V,reject, and? fieldkeep or remove whole groups by testing the visible summary. They never edit hidden members. A member-only selector afterGtherefore matches no group.Kprojects matching summary key paths, whilePprojects selected summary fields.Uunrolls a summary array into multiple groups with the same member rows.S,L, andtailorder or limit whole groups.Areads the unchanged members of each retained group, so aggregates added before or after a summary filter remain accurate.Zemits one visible row per retained group.- Grouped
Cis a terminal collapse that emits one summary row per group with acountfield for its member count.
Grouping never creates an empty group, and summary filters remove whole groups, so they cannot make a retained group member-empty. A pre-existing empty group is retained or removed solely by the same visible-summary predicate as any other group.
Group by one or more fields:
object list --class Hosts | G os_version AS "OS Version"
object list --class Hosts | G os_version AS "OS Version" data.cpu.arch AS Architecture
Group aliases must be unique. Aggregate aliases cannot reuse a group alias or an earlier aggregate alias; collisions fail instead of overwriting a visible value while leaving duplicate column metadata.
Array selectors fan out group membership:
Aggregate grouped rows:
object list --class Hosts | G os_version AS "OS Version" | A count AS Hosts
object list --class Hosts | G os_version AS "OS Version" | A sum(data.cpu.cores) AS Cores
object list --class Hosts | G os_version AS "OS Version" | A avg(data.cpu.cores) AS "Average Cores"
object list --class Hosts | G os_version AS "OS Version" | A min(Name) AS First
object list --class Hosts | G os_version AS "OS Version" | A max(Name) AS Last
count(selector) counts every non-null selector match, including every fanout
occurrence. count_distinct(selector) excludes missing and null values and uses
the same canonical JSON equality as D, including object-field-order
independence. Both work in grouped and global aggregation:
object list --class Hosts | G rack | A count(data.network.interfaces[].ipv4) AS Addresses
object list --class Hosts | G rack | A count_distinct(data.owner) AS Owners
Use A GLOBAL for one aggregate row over the current Empty, Rows, or
Values collection. Commas separate aggregate terms, and all final output names
must be unique:
object list --class Hosts --all | A GLOBAL count AS Hosts, count(data.owner) AS Owned, count_distinct(os_version) AS Versions
Global aggregation consumes its input and always returns exactly one Rows
record. Counts are zero on empty input; numeric and ordered aggregates without
contributing values return null. Lines, Detail, Message, and Groups are
rejected. Use Z first when the intended input is the visible summaries of
existing groups.
On paginated commands, use --all for cardinality across every matching result;
otherwise the standard partial-pipeline warning applies and only the current
page is aggregated.
first(selector) and last(selector) return the first or last non-null match
in current row and selector order. Missing and null matches are skipped; no
contributing value produces JSON null. Sort before G or A GLOBAL to make
event boundaries explicit and reproducible.
Find the complete time range for one task's events:
task events <task-id> --all | S created_at asc AS datetime | A GLOBAL first(created_at) AS First, last(created_at) AS Last
Find the first and last visible audit event for each action:
audit list --all | S occurred_at asc AS datetime | G action | A first(occurred_at) AS First | A last(occurred_at) AS Last | Z
Aggregates are ordinary visible output columns, so later stages can filter, sort, project, or redirect them:
object list --class Hosts | G os_version AS "OS Version" | A count AS Hosts | S Hosts desc AS num | L 10
object list --class Hosts | G os_version AS "OS Version" | A count AS Hosts | F Hosts>=2
C after grouping returns one count row per group:
Z collapses grouped output to group and aggregate columns:
Before G, U unrolls member arrays into rows:
After G, U instead unrolls an array in the visible group summary and keeps
the original member rows attached to every resulting group.
Shape Contracts¶
Every pipeline value has one of seven shapes: Empty, Lines, Rows,
Detail, Message, Values, or Groups. A stage validates its input shape
before doing any work. Unsupported combinations fail with the stage name, the
current shape, and every accepted shape; a transforming stage never silently
passes through non-empty input.
The table is the complete input and result contract. same retains the input
shape, /E means the predicate may produce Empty, and dynamic means JQ
derives Empty, Rows, Detail, Message, or Values from its JSON result.
| Stage | Empty | Lines | Rows | Detail | Message | Values | Groups |
|---|---|---|---|---|---|---|---|
bare, legacy F, legacy reject |
same | same | same | same/E | same/E | same | same |
F WHERE, reject WHERE |
same | error | same | same/E | same/E | same | same |
V |
same | same | same | same/E | same/E | same | same |
K |
same | error | Rows | Detail/E | Detail/E | Rows | Groups |
? |
same | error | same | same/E | same/E | same | same |
L, head, tail |
same | same | same | error | error | same | same |
C, count |
Values | Values | Values | Values | Values | Values | Rows |
whole-line S, sort |
same | same | same | error | error | same | same |
field S, sort |
same | error | same | error | error | same | same |
whole-value D, distinct |
same | same | same | error | error | same | same |
keyed D, distinct |
same | error | same | error | error | same | same |
P, columns |
same | error | Rows | Detail | Detail | error | Groups |
G |
Groups | error | Groups | Groups | Groups | Groups | error |
grouped A |
error | error | error | error | error | error | Groups |
A GLOBAL |
Rows | error | Rows | error | error | Rows | error |
Z |
error | error | error | error | error | error | Rows |
U |
same | error | same | error | error | same | same |
JQ |
dynamic | error | dynamic | dynamic | dynamic | dynamic | dynamic |
VALUE, VAL |
Values | error | Values | Values | Values | Values | Values |
Empty is an intentional identity only for stages that retain, order, limit,
project, or unroll an existing collection. C, G, JQ, and VALUE make an
explicit shape transition from Empty; A GLOBAL also returns one aggregate
row from Empty, while grouped A and Z require Groups.
P and K turn a structured Message into Detail because projection removes
message presentation semantics. Grouped C is the special summary-row result
described above.
Line-shaped Output¶
Most command results enter the pipeline as semantic rows, details, messages, or
values regardless of the selected renderer. Commands whose result is inherently
prose or a text stream enter as an explicit Lines shape. Lines support broad
or value regex filtering (legacy F, V, bare filters, and legacy reject),
head/L, tail, C, and whole-line S/sort. Typed predicates and other
field-aware stages fail because lines do not contain structured fields.
C turns Lines into a one-element Values result containing the numeric line
count. Other supported line stages retain the Lines shape.
After a line pipeline, text emits the retained lines, JSON emits an array of
strings, JSONL emits one JSON string per line, and CSV/TSV emit a value column.
Selecting one of those renderers does not change which lines reach the stages.
JQ¶
JQ evaluates a jq-compatible expression against the current semantic payload
using the in-process jaq interpreter:
object list --json --class Hosts | JQ 'map({Name, os_version})'
object list --json --class Hosts | JQ '.[] | .Name'
Zero jq outputs become empty semantic output, one output keeps its natural shape, and multiple outputs are collected into semantic rows or values. JQ clears the existing visible-column metadata and infers the result shape.
Prefer built-in stages for common filtering, grouping, and projection because they preserve Hubuum table metadata and completions.
Selectors¶
Selectors are shared by filter, projection, sorting, grouping, and value extraction.
Name top-level field
os_version top-level field
data.owner dotted path
data.network.interfaces[0] array index
data.network.interfaces[-1] negative index
data.network.interfaces[*] array fanout
data.network.interfaces[] array fanout
data.network.interfaces[:2] slice
Dotted and indexed selectors are strict path lookups. Bare quick search remains permissive and can match keys or values. Malformed selectors fail while the pipeline is parsed, before any stage changes the data. This includes empty path components, unmatched brackets, invalid indexes or slice bounds, and characters after a closing bracket.
Dots and square brackets are selector syntax. Field names containing those
characters cannot currently be addressed because the DSL does not define an
escape syntax for selector metacharacters. Colons remain ordinary field-name
characters, including in computed-field selectors such as S:average_load.
Redirects¶
Redirects run after pipe stages:
object list --class Hosts | P Name os_version > hosts.txt
object list --class Hosts | P Name os_version >> hosts.txt
object list --json --class Hosts | P Name os_version > each:hosts/{Name}.json
object list --class Hosts | VALUE Name > each:names/{value}.txt
each:<template> writes one file per semantic row or value. Placeholders use
the same field names as the current output, plus {value} for VALUE output
and {n} for a 1-based item number. It requires structured semantic output;
parent directories must already exist. Duplicate generated paths are rejected
before any files are written, and placeholder values are sanitized for paths.
Redirect paths support quoting, ~/... expansion, and REPL file path
completion. The > and >> operators must be standalone,
whitespace-delimited tokens. Compact comparisons such as F age>3 remain
filter expressions. A spaced comparison in F WHERE age > 3 remains part of
the predicate because the prefix before > is not a complete pipeline. A
later standalone operator redirects once its preceding pipeline is complete.
Command filters such as --where age > 3 are likewise retained when the
command before > would otherwise be invalid.
Redirect files honor the configured color mode. auto and never strip ANSI
styling from non-terminal files; always preserves it. In one-shot POSIX shell
commands, escape application-level operators, for example:
hubuum-cli object list --class Hosts \| P Name os_version \> hosts.txt
hubuum-cli object list --class Hosts \| VALUE Name \> each:/tmp/host-{value}.txt
Help¶
Use focused help topics in the REPL: