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:

  1. 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.
  2. Pipeline-aware completion (see below).

Out-Default format resolution

First match wins:

  1. Explicit --format flag (auto table list json jsonl tsv)
  2. BU_OUTPUT_FORMAT environment variable
  3. 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 (enum auto table list json jsonl tsv) and --columns (comma list, supports key:Label display labels) for free via the standard bu_parse_positional autocomplete DSL — see commands/bu-get-command.sh.
  • Hints for humans (e.g. “No modules registered”) go to stderr via bu_log_info so 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 null group.
  • select x distinctgroup-by x once sorted; distinct preserves original order, group-by sorts.
  • Composition is eval-free: each clause is a function stage, absent clauses are cat in the default pipeline executor. The combined executor builds a jq program from the same parsed clauses, without shell eval.

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 cat stages and head.
  • How first interacts 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); default unicode, overridable via BU_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:

  1. Static registry BU_OUT_PRODUCER_FIELDS (longest producer-prefix match, so flags and later stages don’t break it). Seeded for the builtins; extend with bu_register_output_fields.
  2. Opt-in probing: BU_OUT_PROBE_PIPELINE=true plus the producer head in BU_OUT_PROBE_COMMANDS executes 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 input format token matches the upstream output (e.g. after bu convert-to-tsv, jsonl consumers like bu select are hidden; bu convert-from-tsv and bu convert-from-lines remain). 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:lessless -R, preset:batbat --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 --synopsis on registration functions). An alias without a registered synopsis has an empty synopsis; its expansion is exposed as the definition field of bu 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/output format tokens in bu get-command are derived from it (and, for codec, from the noun: convert-to-jsonjsonl → json). Function/alias commands that have no file register via bu_register_stage_effect instead.
    • producernone → jsonl: emits records from its own data sources.
    • transformjsonl → jsonl: consumes records and emits its own result records (output schema = its own # Fields:, falling back to input).
    • consumejsonl → none: acts on each record, no stream out.
    • standalonenone → none: participates in no pipeline at all.
  • # Requires-All: — field names a cmdlet must ALL receive on piped JSONL (AND; surfaced in bu get-command and the --help PIPELINE 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 for execute/source commands, the function name for function commands, or the full expansion spec for alias commands (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 unregistered
  • input / output — stream format tokens (jsonl, json, tsv, csv, text, base64, display, none): what the cmdlet accepts as pipeline input and what it emits. Derived from stage (and, for codec, the noun)
  • requires_all — fields the cmdlet must ALL receive from upstream (# Requires-All: header, AND), space-joined
  • requires_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_autocompletions with binding locals (command_line_front_before_pipe, pipe_before) simulated per test.

This site uses Just the Docs, a documentation theme for Jekyll.