Structured Output (PowerShell-inspired)
BashTab commands can emit records instead of text. Instead of parsing columns with awk/grep, you filter and shape fields with SQL-style cmdlets or raw jq, and presentation (table vs JSON) is decided automatically at the end of the pipeline — just like PowerShell’s Out-Default.
Everything is built on one idea: JSONL (one JSON object per line) is the object stream, and jq is the engine. All of it lives in lib/core/bu_core_out.sh.
Quick tour
# On a terminal, bu commands render tables
$ bu get-command
name type definition synopsis
-------------------- ------ ------------------------------------------ --------------------------------------------
convert-from-lines source commands/pipeline/bu-convert-from-lines.sh Convert line-oriented text to JSONL records
get-command source commands/core/bu-get-command.sh List registered commands and their properties
...
# Piped, the same command emits JSONL — jq is your Where-Object
$ bu get-command | jq -r 'select(.type == "source") | .name'
# SQL-style cmdlets compose the same operations
$ bu get-command | bu where '.type == "source"' \
| bu select name,verb | bu sort verb
# ...or as a single query
$ bu get-command | bu query-object where '.type == "source"' \
select name,verb order-by verb
# Grouping and aggregation
$ bu get-command | bu query-object group-by verb agg count order-by count desc
verb count
----------- -----
convert-from 2
convert-to 3
format 2
...
The pipeline model
producer → recordify → transform → sink
(raw) (→JSONL) (JSONL→JSONL) (→display)
| Layer | Core functions | Cmdlets |
|---|---|---|
| Recordifiers | bu_out_record, bu_out_from_tsv, bu_out_from_lines | bu new-record, bu convert-from-tsv, bu convert-from-lines |
| Transforms | bu_out_where, bu_out_select, bu_out_sort_by, bu_out_group_by, bu_out_distinct | bu where, bu select, bu sort, bu query-object, bu distinct-object |
| Sinks | bu_format_table, bu_format_list, bu_format_json, bu_format_jsonl, bu_format_tsv | bu format-table, bu format-list, bu convert-to-json, bu convert-to-jsonl, bu convert-to-tsv |
| Dispatcher | bu_out | bu out-default |
The functions are the scripting API — pure JSONL in/out. The cmdlets wrap them for interactive use and add two behaviors:
- Implicit Out-Default: every cmdlet pipes through
bu_out. At the end of a terminal pipeline you get a table; anywhere mid-pipeline you get JSONL. No explicit formatter needed. - Pipeline-aware completion (see below).
Out-Default format resolution
First match wins:
- Explicit
--formatflag (auto table list json jsonl tsv) BU_OUTPUT_FORMATenvironment variable- stdout is a terminal →
table; otherwise →jsonl
Stream vs buffer
| Behavior | Formatters / stages |
|---|---|
| Streams (O(1) latency) | jsonl, tsv, list, where, select, distinct¹, table --stream |
| Buffers all input | table (auto-width), json (array envelope), sort, group-by |
¹ distinct streams first occurrences but remembers keys seen so far — inherent to dedupe.
Authoring structured commands
The pattern — zero forks in the record loop, exactly two jq processes:
{
for entry in "${entries[@]}"; do
printf '%s\t%s\t%s\n' "$name" "$version" "$path" # builtin printf only
done
} | bu_out_from_tsv --columns name,version,path | bu_out --format "$format"
- Values must not contain tabs/newlines in TSV mode; for arbitrary strings use
bu_out_record key="$value"per record (one jq fork each). - Expose
--format(enumauto table list json jsonl tsv) and--columns(comma list, supportskey:Labeldisplay labels) for free via the standardbu_parse_positionalautocomplete DSL — seecommands/bu-get-command.sh. - Hints for humans (e.g. “No modules registered”) go to stderr via
bu_log_infoso they never pollute the structured stream.
Register your command’s fields so completion can offer them downstream:
# In your module's preinit script
bu_register_output_fields "bu get-pokemon" name id type hp attack
PowerShell mapping
| PowerShell | BashTab |
|---|---|
[PSCustomObject]@{...} | bu new-record k=v / bu_out_record |
| ConvertFrom-Csv | bu convert-from-tsv, bu convert-from-lines |
| Where-Object | bu where '<jq expr>' (or raw jq) |
| Select-Object | bu select a,b=version |
| Sort-Object | bu sort key [--desc] |
| Group-Object + Measure-Object | group-by + agg (flat records, not nested) |
| Select-Object -Unique | bu distinct-object |
| Format-Table / Format-List | bu format-table / bu format-list |
| ConvertTo-Json | bu convert-to-json (+ jsonl, tsv) |
| Out-Default | bu out-default (implicit in every cmdlet) |
Get-Cmdlet | Select -First 5 | first 5 in query-object |
bu query-object — SQL in one command
Clause keywords work bare or dashed (select / --select) and in any order; execution always follows SQL logical order:
where → group-by → having → select → distinct → order-by → first
| Clause | Semantics |
|---|---|
where '<jq expr>' | Pre-group filter, source field names. Repeatable, ANDed. |
grep 'pattern' | Search a pattern across any field value of each record (grep of a row). Default regex; -like/-ilike glob (*/?, bare = substring); -i/-ilike case-insensitive. Repeatable, ANDed. |
group-by a[,b] | Collapse to one record per (composite) key. No agg = SELECT DISTINCT keys. |
agg [name=]func[:field] | Aggregates, repeatable and/or comma-separated. count, sum:f, avg:f (numeric only), min:f, max:f, first:f, last:f, collect:f (array of values). Default name: func_field. |
having '<jq expr>' | Post-group filter on group/aggregate fields. Repeatable, ANDed. |
select a,b=version | Project/reorder/rename (new=old). |
distinct | Dedupe whole records (first occurrence wins, order preserved, key-order canonicalized). |
order-by field [--desc] | Sort by output field names (SELECT aliases, like SQL). |
first N | LIMIT; streams, short-circuits slow producers. |
--format, --columns | Output control (--columns accepts key:Label). |
bu get-command | bu query-object where '.type == "source"' \
group-by verb agg count,collect:noun having '.count > 1' \
select v=verb,n=count order-by n desc first 3
# grep a pattern across any field (regex), or glob/case-insensitive:
bu get-command | bu query-object grep '^get-' select name,verb
bu get-command | bu query-object grep -like command select name
bu get-command | bu query-object grep -ilike get-* select name
Design notes:
- Records missing a group key form a
nullgroup. select x distinct≡group-by xonce sorted;distinctpreserves original order,group-bysorts.- Composition is eval-free: each clause is a function stage, absent clauses are
catin the default pipeline executor. The combined executor builds a jq program from the same parsed clauses, without shelleval.
Incremental aggregation
group-by reduces incoming records to state for each group. count, sum, avg, min, max, first, and last retain only their aggregate state; avg tracks a numeric sum and count. collect additionally retains its field values, including missing/null values. Unused fields and full input records are not retained.
This applies to both query executors and bu_out_group_by. Output remains sorted by the original grouping keys, with the same aggregate aliases and numeric/null handling. Memory is proportional to the number of groups and the size of their retained values, rather than all input records. If nearly every record creates a new group, or collect retains large values, memory can still be substantial.
Incremental does not mean final groups can be emitted early: later records can change an existing group. Grouping still requires EOF, so a following first N does not generally avoid reading the full input. --explain labels grouping as retains-group-state and describes this EOF requirement.
Query execution modes
BU_QUERY_EXECUTOR selects the implementation for query-object and its query aliases (where, select, sort, grep, etc.):
| Mode | Behavior |
|---|---|
pipeline (default) | Original executor: separate processes for clauses, forwarding cat stages for unused clauses, and head for first. |
combined | One jq evaluator for the query clauses, followed by the existing Out-Default formatter. Projection, aggregates, and distinct use shared definitions with the pipeline executor. |
# Try it for a single query, or select it for the current shell:
BU_QUERY_EXECUTOR=combined bu query-object --from services.jsonl select name first 5
export BU_QUERY_EXECUTOR=combined
# Persist the preference using the normal config command:
bu set-config BU_QUERY_EXECUTOR combined
# Return to the original executor:
bu set-config BU_QUERY_EXECUTOR pipeline
Both modes retain clause order, renaming, numeric comparison behavior, grouping/aggregates, expansion, distinct, output formats, file input/output, and the existing --debug plan used by completion. The setting changes the query executor only; standalone bu_out_* functions retain their pipelines.
Combined queries stream filtering and projection, and remember seen records for order-preserving distinct. Grouping retains aggregate state per group; sorting still buffers its input. Both operations need EOF before yielding final results. first N counts results after all preceding clauses and stops requesting records through jq’s limit, rather than closing an internal pipe. first 0 produces no records without reading input. Input errors beyond the limit are not examined. As with raw jq generally, expressions using input, inputs, or input-position builtins see the combined evaluator’s input rather than a separate clause process’s input.
JSONL, JSON, and TSV files are read directly by the combined evaluator; CSV still requires jc. A JSON array must be parsed before its elements can be queried. Both executors preserve query, input-converter, and formatter failures through cleanup, even without shell pipefail.
An external producer in producer | bu query-object first N can still receive SIGPIPE when the query stops reading, and the outer pipeline can therefore fail under set -o pipefail. Combined mode eliminates the query’s internal head/forwarder cancellation; it does not drain the remaining producer output or hide upstream failures.
Query errors
Both executors return a nonzero status for a failed stage and add its name and status to stderr, alongside the tool’s original diagnostic. For example:
query-object [pipeline]: where stage failed (status 5)
query-object [combined]: query stage failed (status 5)
Pipeline mode can identify individual transform stages. Combined mode reports the shared jq evaluator as query, since its clauses execute together. Invalid field specifications caught while building that evaluator report compile.
When multiple stages fail, the rightmost non-SIGPIPE failure determines the query status. Cleanup failures are reported but do not replace an existing query failure. Results already emitted before an error remain partial output; check the exit status before treating an output file as complete.
In pipeline mode, SIGPIPE (141) from a writer before a successful first stage is treated as expected cancellation. Other failures are retained, including broken-write errors when SIGPIPE is ignored. SIGPIPE caused by downstream failure is secondary to that failure. This handling is internal to the query; it does not change shell options or the status of external upstream producers.
Explain a query without running it
--explain describes the parsed query and its execution stages without consuming stdin, reading the data file, executing predicates, or writing outfile:
BU_QUERY_EXECUTOR=combined bu query-object --explain \
where active -eq true select name,score order-by score desc first 5
The readable plan shows:
- Input source and format, and the eventual output destination and format.
- Logical clause order, expressions, projections, aggregates, sort direction, and the result limit.
- Streaming operations, sorting that buffers input, grouping that retains per-group state, and distinct’s retained set of seen values.
- Execution stages: the combined jq evaluator, or the original pipeline with forwarding
catstages andhead. - How
firstinteracts with buffering and upstream cancellation.
Sorting and grouping need all input before they yield results. A table or JSON formatter instead buffers the query results, which may already be limited by first. JSON file input parses each complete JSON value before unrolling arrays; CSV conversion buffers input. Combined first 0 bypasses input and the other query stages.
Use --format json for a pretty-printed JSON plan, or --format jsonl for one compact JSON object:
bu query-object --explain --format json group-by team agg count,avg:score
The structured plan has version: 1, executor, input, output, stages, execution, and notes, alongside the clause and field summary. An expanded projection has unknown outputFields (null). Other format choices produce readable explanations. --format still describes the query’s eventual output format; with auto, the plan resolves BU_OUTPUT_FORMAT, the output file, and terminal detection as normal. The explanation itself always goes to stdout.
This is a static description, without timings or actual row counts. Raw jq expressions are passed through as text; constructs such as input/inputs can alter the ordinary per-record behavior described by the plan. Argument, file-path, and dependency checks still apply.
The existing --debug summary
--debug parses the query without reading stdin or executing its transforms:
bu query-object --debug select label=name,score order-by score first 5
# {"clauses":["select","order-by"],"outputFields":["label","score"]}
clauses lists the kinds of clauses relevant to field analysis. outputFields contains projected names (after renaming), or group keys and aggregate names when there is no projection. Otherwise it is null; completion can inherit the upstream fields. The summary is a completion aid, not a runtime trace: it omits first, sort direction, clause expressions, and executor details. Argument and file-path checks still apply. Both executors emit the same format. --debug and --explain share a plan builder; --debug preserves its original JSON projection for completion and takes precedence when both flags are present.
Tables
bu_format_table (buffered, the default sink):
- Column widths from data, then widest columns shrink until the table fits
$COLUMNS; overflow truncated with…. - Header is bold on a terminal; rows are right-trimmed (no trailing spaces).
--columns a,b:Label— order/select fields, rename headers.--colors name=green,version=yellow— per-column color (keys, not labels).--style name— table border/separator style (see below); defaultunicode, overridable viaBU_TABLE_STYLE.--stream— emit immediately with proportional widths from$COLUMNS(requires--columns). Use for large/slow streams.- Empty input → no output (PowerShell semantics).
Table styles
bu format-table --style <name> (or BU_TABLE_STYLE=<name>) picks a border/separator look. unicode (single-line box-drawing ┌─┬┐ │ ├┼┤ └┴┘) is the default. The rest:
| Style | Look |
|---|---|
plain | Padded columns only — no header underline, no bold |
ascii | +/-/\| box |
unicode | Single-line box-drawing (┌─┬┐ │ ├┼┤ └┴┘) |
double | Double-line box-drawing (╔═╦╗ ║ ╠╬╣ ╚╩╝) |
clickhouse | ClickHouse PrettyCompact — single-line box, no header rule |
markdown | \| a \| b \| + \|---\|---\| header (GitHub-flavoured) |
mysql | +----+ borders between every row |
psql | Postgres-style -+- header separator only |
Styles are registered in __BU_TABLE_STYLES (a name → JSON-descriptor assoc) and extended with bu_register_table_style <name> <descriptor>, e.g. from a module preinit script. A descriptor sets left/vsep/right (line wrappers) and optional top/hsep/rsep/bottom rule specs ({left,char,join,right,min,pad}) plus header_bold.
bu_format_list renders key : value blocks — good for wide records on narrow terminals.
Pipeline-aware completion
After a pipe, field names of the producer’s records are offered:
bu get-command | bu select <TAB> # name verb noun namespace type definition synopsis fields stage input output requires_all requires_any module shadows shadowed_by
bu get-command | bu select name,<TAB> # comma-aware: the remaining fields
bu get-command | bu where <TAB> # .name .verb .noun .namespace .type .definition .synopsis .fields .stage .input .output .requires_all .requires_any
Sources, in order:
- Static registry
BU_OUT_PRODUCER_FIELDS(longest producer-prefix match, so flags and later stages don’t break it). Seeded for the builtins; extend withbu_register_output_fields. - Opt-in probing:
BU_OUT_PROBE_PIPELINE=trueplus the producer head inBU_OUT_PROBE_COMMANDSexecutes the producer as typed and reads keys off the first JSONL record. Off by default — it runs user-typed text.
Producer text is resolved from the completion bindings via dynamic scope (command_line_front_before_pipe for the legacy parser, pipe_before for tree-sitter), with a COMP_WORDS pipe-walk fallback.
Command completion after a pipe
At the command position after a pipe (bu get-command | <TAB>), candidate commands are filtered by compatibility with the upstream stream:
- Format — a command is offered only if its
inputformat token matches the upstreamoutput(e.g. afterbu convert-to-tsv, jsonl consumers likebu selectare hidden;bu convert-from-tsvandbu convert-from-linesremain). Unknown formats are never filtered out — only positively-known mismatches are hidden. - Fields — a command with a
# Requires-All:contract is offered only when the upstream producer’s fields are statically known to include every required field; a# Requires-Any:contract is satisfied when at least one is present. Static resolution uses multi-stage analysis, the field registry, and# Fields:headers (no producer execution).
Static pipeline validation
bu validate-pipeline '<pipeline>' statically checks a pipeline’s field references and reports any field a stage reads that is not produced upstream — the runtime analogue of the completion filter:
bu validate-pipeline 'bu get-command | bu sort madeup' # {"field":"madeup"}
bu validate-pipeline 'bu get-command | bu select name' # (no output = valid)
Only structurally-parseable reads are checked: sort/select/where/ group-by field arguments and # Requires-All:/# Requires-Any: contracts. Raw jq expressions, order-by aliases, and grep patterns are skipped; unknown producers make validation skip rather than report false positives.
Inferred output schema
bu get-shape <producer> runs a producer once and infers the record shape: one record per field with its observed JSON type(s), whether it is present on every record, and presence/null counts:
bu get-shape get-command # name/type/types/required/count/null_count per field
Declared fields from the producer’s # Fields: header are listed first (in declared order), even when the producer emitted no records; inferred-only fields follow. This is the type-level complement to the name-only # Fields: header — types are inferred, never hand-authored.
File schema inference
Cmdlets and clauses that read a data file infer the record schema directly from the file — the header row for CSV/TSV, the first record’s keys for JSONL/JSON — and feed it into the same field-aware completion machinery as a pipeline producer. No producer execution and no hand-authored registry entry:
bu query-object --from data.csv select <TAB> # type name verb version ...
bu query-object --from data.csv where <TAB> # field names from the header
bu import-tsv data.tsv | bu select <TAB> # same, via a recordify_file stage
Formats are detected by extension: .csv (via jc), .tsv/.tab, .jsonl/.ndjson, and .json. query-object --from dispatches the same way at runtime, so --from data.csv / --from data.tsv query a file directly instead of requiring a convert-from-* stage first.
The import-* cmdlets are thin # Pipeline: recordify_file file producers that emit JSONL and register the file’s schema for downstream completion:
bu import-csv data.csv # jc --csv; header row becomes the keys
bu import-tsv data.tsv # first row is the header
bu import-jsonl data.jsonl # passthrough (identity), registers the schema
bu import-json data.json # unroll an array / pass an object through
Inference is read-only and bounded: it inspects only regular files (never FIFOs/devices), reads at most the header plus one row (CSV/TSV) or the first record (JSONL/JSON), and is memoized per path:mtime:size for the session. Distinct field values are also completed at the where -eq / -in value position from the same bounded file sample.
Runtime record validation
Commands with # Requires-All: / # Requires-Any: contracts validate incoming JSONL records before passing them to their input-processing loop. This includes both consumers and transforms that act on records, such as remove-git-tag. The defaults preserve the original behavior: warn about missing fields in the first record and pass the stream through.
# Check every record and reject the first invalid one:
bu set-config BU_OUT_VALIDATE_RECORDS all
bu set-config BU_OUT_VALIDATION error
# Restore the original behavior:
bu set-config BU_OUT_VALIDATE_RECORDS first
bu set-config BU_OUT_VALIDATION warn
BU_OUT_VALIDATION accepts off, warn, or error. BU_OUT_VALIDATE_RECORDS accepts first or all. The legacy BU_OUT_STRICT=false switch also disables validation, regardless of these settings. A command without a field contract passes input through.
In warning mode, records pass through without reformatting and diagnostics go to stderr. Error mode stops at the first invalid record, does not forward it or subsequent records, and returns status 2. Diagnostics include the command, input line number, and missing fields; malformed JSON and non-object records are also invalid. Blank lines count as invalid JSON records. Field presence is the contract: a present field whose value is null still counts as present. Type checks are not inferred from the first record.
For example, under all/error, this stream fails on record 2:
{"name":"alpha"}
{"unexpected":"beta"}
Validation uses one streaming jq process, rather than spawning jq per field per record. Built-in commands propagate its status even when their input loop uses process substitution. Commands that gather names before acting abort before applying those actions; streaming consumers may already have acted on valid preceding records. This is not transactional rollback or whole-stream preflight validation.
When adding a consumer, use the managed reader and check its status instead of an unchecked done < <(__bu_out_strict_guard ...):
local validation_fd validation_pid validation_status=0 record
__bu_out_strict_open validation_fd validation_pid my-command || return 1
while IFS= read -r record; do
# Process the validated record (or accumulate records before acting).
:
done <&"$validation_fd"
__bu_out_strict_close "$validation_fd" "$validation_pid" || validation_status=$?
# Perform the command's scope cleanup, then return validation_status if nonzero.
An optional fourth argument to __bu_out_strict_open is a jq expression for extracting consumer values, such as .name // empty. Read the stream to EOF and always close/wait; the helper preserves validator and extractor failures.
Alias merging in option completion
Case-pattern alternatives equal modulo leading -/+ and case (--select|select|SELECT) collapse into one row: the first form wins (the row switches to a typed prefix so compgen keeps it), metadata lists aka <other forms>, and using any form excludes the group. Alternatives that differ after normalization (-v|--verb, --json|--yaml) stay separate rows. Put the preferred insert form first in the pattern.
Multi-word verbs
Command name parsing honors BU_MULTI_WORD_VERBS (default convert-to, convert-from), longest match first — bu-convert-to-jsonl.sh registers verb=convert-to, noun=jsonl. Extend the array for custom multi-word verbs.
Configuration reference
| Variable | Default | Purpose |
|---|---|---|
BU_OUTPUT_FORMAT | (empty) | Force output format when --format auto |
BU_QUERY_EXECUTOR | pipeline | Query execution mode: pipeline (original separate stages) or combined (one jq evaluator before formatting). |
BU_TABLE_STYLE | unicode | Default table style. plain, ascii, unicode, double, clickhouse, markdown, mysql, or psql (see Table styles). Overridden per-call by --style. |
BU_TABLE_PAGER | preset:less | Pager for tables. preset:less → less -R, preset:bat → bat --paging=always, preset:never → cat, or a raw command like less -R. Empty disables. |
BU_OUT_PRODUCER_FIELDS | builtins | Assoc: producer prefix → field list |
BU_OUT_PROBE_PIPELINE | false | Master switch for live probing during completion |
BU_OUT_PROBE_COMMANDS | (empty) | Assoc allowlist of probe-safe producer heads |
BU_OUT_STRICT | true | Legacy master switch; false disables record validation. |
BU_OUT_VALIDATION | warn | Contract validation action: off, warn, or error. |
BU_OUT_VALIDATE_RECORDS | first | Validate the first record or all records. |
BU_PIPELINE_CONTRACT_WARN | true | Scan-time warnings for commands missing a # Pipeline: header or field contract (false silences) |
BU_MULTI_WORD_VERBS | convert-to convert-from | Multi-word verb list for name parsing |
Dependency: jq (≥1.6) is required for all of the above; the module checks at source time and errors with install instructions otherwise.
Command discovery and the # Synopsis convention
Every command may declare a static one-line description via a # Synopsis: comment in the first 30 lines of its script file:
#!/usr/bin/env bash
# Synopsis: List registered commands and their properties
Rules:
- One sentence, imperative, <100 characters, no trailing period.
- Plain text only — no variable interpolation, no command substitution, no ANSI color codes. The text is extracted verbatim.
- First match within the first 30 lines wins; scanning stops there.
- Non-file commands (aliases, functions) get synopses from the registry (set via
--synopsison registration functions). An alias without a registered synopsis has an empty synopsis; its expansion is exposed as thedefinitionfield ofbu get-command.
Pipeline contract headers
Command scripts declare their pipeline behavior with # Key: value headers in the same first-30-lines block. All of them are read by a single shared header parser (__bu_command_header_get), so adding a header is cheap and needs no central registry.
#!/usr/bin/env bash
# Pipeline: codec # stage effect: producer | passthrough | project |
# query | transform | consume | standalone | sink |
# codec | recordify_tsv | recordify_lines |
# recordify_new | recordify_jc | recordify_file
# Requires-All: host port # (optional) EVERY field must be present
# Requires-Any: unit name # (optional) at least ONE field must be present
# Fields: name path version # (optional) output fields this producer emits
# Pipeline:— the pipeline stage effect.input/outputformat tokens inbu get-commandare derived from it (and, forcodec, from the noun:convert-to-json→jsonl → json). Function/alias commands that have no file register viabu_register_stage_effectinstead.producer—none → jsonl: emits records from its own data sources.transform—jsonl → jsonl: consumes records and emits its own result records (output schema = its own# Fields:, falling back to input).consume—jsonl → none: acts on each record, no stream out.standalone—none → none: participates in no pipeline at all.
# Requires-All:— field names a cmdlet must ALL receive on piped JSONL (AND; surfaced inbu get-commandand the--helpPIPELINE section).# Requires-Any:— field names a cmdlet accepts ANY one of (OR; the structural-typing fallback, e.g. services reading.unit // .name).# Fields:— output field names a producer emits, used for pipeline-aware completion after a pipe.
Agent and script integration
Agents and scripts should enumerate capabilities via:
bu get-command --format jsonl
Each record includes all sixteen fields:
{"name":"get-command","verb":"get","noun":"command","namespace":"bu",
"type":"source","definition":"/path/to/commands/core/bu-get-command.sh",
"synopsis":"List registered commands and their properties",
"fields":"name verb noun namespace type definition synopsis fields stage input output requires_all requires_any module shadows shadowed_by",
"stage":"producer","input":"none","output":"jsonl","requires_all":"","requires_any":""}
definition— what the name resolves to: the script path forexecute/sourcecommands, the function name forfunctioncommands, or the full expansion spec foraliascommands (e.g.query-object --where {...})synopsis— one-line description (static, safe to parse)fields— output fields this command produces (space-joined, for pipeline composition)stage— pipeline stage effect:producer,passthrough,project,query,transform,consume,standalone,sink,codec,recordify_*, or empty if unregisteredinput/output— stream format tokens (jsonl,json,tsv,csv,text,base64,display,none): what the cmdlet accepts as pipeline input and what it emits. Derived fromstage(and, forcodec, the noun)requires_all— fields the cmdlet must ALL receive from upstream (# Requires-All:header, AND), space-joinedrequires_any— fields the cmdlet accepts ANY one of (# Requires-Any:header, OR), space-joined
This is a single fast call (~7ms awk scan) that gives agents a complete manifest of available commands — no per-command --help forks needed.
New commands created with bu new-command get a placeholder # Synopsis: line in the template so they never ship without one.
Testing
test/out_test.bats (126 tests, run via ./bu_run_tests.sh):
- All assertions are TTY-independent: captured stdout is a pipe, so Out-Default deterministically resolves to JSONL and headers are unbolded.
- Terminal behavior is covered with a real pty via
script(1). - Completion is tested end-to-end through
bu_autocomplete_get_autocompletionswith binding locals (command_line_front_before_pipe,pipe_before) simulated per test.