result

package
v0.1.0-dev.20260918183856 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 18, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

Documentation

Overview

Package result owns the primary output channel — the structured-data stream the framework emits to the user (or to downstream tooling, when piped). Distinct from the side channel (pkg/status) that carries categorized narration.

The pipeline shape is `value → result.Filter → result.Formatter → sink.Sink`. NewPipeline composes the three stages; callers Emit values through it. Ten renderings ship, in the five groups Group names: csv, json and yaml serialize; list and table lay out records; markdown and terminal produce a document; template and value carry a shape the caller composed; none emits nothing. Three filters ship: NoOpFilter pass-through, FieldFilter selection, JQFilter for jq expressions.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type DelimitedFormatter

type DelimitedFormatter struct {

	// Separator is the field delimiter. The zero value is a comma.
	Separator rune

	// SuppressHeadings omits the header row. A spreadsheet wants it; a shell pipeline does not, since awk
	// and cut would each have to skip it.
	SuppressHeadings bool

	// Raw disables RFC 4180 quoting. Set for a renderer whose job is to print what it was given.
	Raw bool
}

DelimitedFormatter renders values as delimiter-separated lines.

One formatter, three attributes, two presets. gcloud reached the same shape: its `csv` and `value` are one renderer differing in separator, heading, and quoting, with `value` documented as "CSV with no heading and <TAB> separator instead of <COMMA>".

Quoting is what separates them, and it is what the two names promise. A machine-parseable format must quote a field containing its own delimiter or the row loses a boundary; a raw renderer must not, or the text a caller composed comes back wearing quotes it did not write.

Shape drives column inference:

  • If value implements HasHeaders, its Headers() result is the column order.
  • Otherwise, if value is a slice/array of structs, the struct fields (in declaration order, with `csv:"name"` overrides and `csv:"-"` skips) are the columns.
  • Otherwise, if value is a slice/array of maps, the union of map keys (sorted alphabetically) is the columns.
  • Otherwise the elements are scalars: one column, no header, since a scalar has no field to name.

A value that is not a slice or array at all is one row. That is the shape `--jq '.count'` produces, and erroring on it would make the filter stage incomplete.

Cell values render via fmt.Sprint, which honors fmt.Stringer and produces reasonable defaults for numbers, bools, time.Time, and nil. Empty input renders no bytes.

func NewCSVFormatter

func NewCSVFormatter() DelimitedFormatter

NewCSVFormatter returns the spreadsheet preset: comma, heading, quoted.

Returns:

  • `DelimitedFormatter`: the RFC 4180 formatter.

func NewValueFormatter

func NewValueFormatter() DelimitedFormatter

NewValueFormatter returns the raw preset: tab, no heading, no quoting.

This is what completes the filter stage. `--jq` can build any line; every other format then imposes a syntax on it -- json quotes and escapes, yaml applies scalar rules, csv adds a header. This one prints it. `aws` ships the same rendering as `text` and `gcloud` as `value`.

A quoted tab preset was tried and dropped. `cut` and `awk -F'\t'` have no quote awareness, so quoting a field that contains a tab does not save them -- the row still splits, and they get `"a` and `b"` instead of `a` and `b`. Quoting only helps a caller running a real parser, and such a caller is better served by `csv`, which every language's standard library reads. That leaves nothing a quoted tab format does better than both, which is why gcloud and aws each ship a raw one and no `tsv`.

Returns:

  • `DelimitedFormatter`: the raw formatter.

func (DelimitedFormatter) Format

func (f DelimitedFormatter) Format(value any, w io.Writer) error

Format renders value as delimiter-separated values to w.

Returns an error if value is not a slice/array of structs or maps and does not implement HasHeaders.

func (DelimitedFormatter) Group

func (f DelimitedFormatter) Group() Group

Group reports that CSV and value are serialized or composed, by which preset this formatter is.

One type backs two names: `csv` quotes so a parser can round-trip it, and `value` is raw so a shell reads exactly what the filter stage composed. DelimitedFormatter.Raw is the field that separates them, so it is the field that answers here.

Returns:

type FieldFilter

type FieldFilter struct {
	// contains filtered or unexported fields
}

FieldFilter narrows a slice of structs/maps to those whose named field matches a target value. The expression form is `field=value`, mirroring `gh`'s `--filter` flag and `kubectl`'s field selectors. Multiple expressions AND together — every expression must match for a row to pass.

Comparison is done after rendering both sides through fmt.Sprint, so primitives, fmt.Stringer implementations, and time.Time values all compare correctly. Numeric strings ("42") and numeric values (42) compare equal — the expression dialect is string-typed.

Construct via NewFieldFilter; surface parse errors directly to the user.

func NewFieldFilter

func NewFieldFilter(exprs ...string) (*FieldFilter, error)

NewFieldFilter parses zero or more `field=value` expressions into a FieldFilter. Empty expressions are skipped silently, supporting the common pattern where the caller does `strings.Split(flag, ",")` against an empty flag value. An expression missing `=` is a parse error.

Parameters:

  • exprs: zero or more `field=value` expressions.

Returns:

  • *FieldFilter: the constructed filter. With zero predicates, Filter.Apply is a pass-through.
  • error: when any expression fails to parse.

func (*FieldFilter) Apply

func (f *FieldFilter) Apply(value any) (any, error)

Apply returns the subset of value's elements for which every predicate matches. Non-slice values pass through unchanged when the filter has zero predicates; otherwise non-slice values error loudly.

type Filter

type Filter interface {

	// Apply returns a (possibly modified) value, or an error if the filter expression is invalid
	// for the input shape.
	Apply(value any) (any, error)
}

Filter is the selection stage of the result pipeline. Implementations narrow, transform, or pass-through the value before it reaches the formatter.

func FilterByExprs

func FilterByExprs(fieldExprs []string, jqExpr string) (Filter, error)

FilterByExprs returns a composed Filter from optional `field=value` expressions and an optional jq expression. When both are present, the field filter runs first (cheap predicate elimination), then the jq filter (full transform). With both empty, returns a NoOpFilter.

Parameters:

  • fieldExprs: zero or more `field=value` expressions for FieldFilter.
  • jqExpr: an optional jq expression for JQFilter; empty disables the jq stage.

Returns:

  • Filter: the composed filter.
  • error: when any expression fails to parse.

type Formatter

type Formatter interface {

	// Format renders value to w in the implementation's format (JSON, YAML, CSV, template, etc.).
	Format(value any, w io.Writer) error

	// Group reports the kind of reader this rendering serves. See [Group].
	Group() Group
}

Formatter is the rendering stage of the result pipeline. Implementations encode the (filtered) value as bytes on the writer.

The writer parameter is typed as io.Writer rather than sink.Sink because formatters operate on a byte stream — they don't need TTY-awareness, Close lifecycle, or any other Sink-specific concerns. Pipeline.Emit passes its sink.Sink (which is an io.Writer) to Format directly; stdlib encoders (encoding/json, gopkg.in/yaml.v3, etc.) plug in without adapter shims.

func FormatterByName

func FormatterByName(spec string) (Formatter, error)

FormatterByName returns the Formatter named by spec.

spec is a format name, or `NAME=ARGUMENT` for a format that takes one. The split is on the FIRST `=`, so an argument containing `=` survives intact. A format needing an argument carries it here rather than in a second flag: a sidecar would give the format stage two inputs and a mutual-exclusion rule to enforce, where this makes the conflict impossible by construction. `kubectl` ships the same form -- `-o go-template=`, `-o jsonpath=`, `-o custom-columns=`.

The names are "csv", "json", "list", "markdown", "none", "table", "template=BODY", "terminal", "value", and "yaml". Reshaping a value is the filter stage's job -- see FilterByExprs -- so only "template" takes an argument.

"csv" and "value" are the delimited pair, and the split is by consumer rather than by separator: "csv" quotes, so a parser can round-trip it; "value" does not, so a shell reads exactly what was composed. A quoted tab format sat between them and served neither -- see NewValueFormatter.

Parameters:

  • `spec`: the format name, or `NAME=ARGUMENT`; the name is case-insensitive.

Returns:

  • `Formatter`: the constructed formatter.
  • `error`: when the name is unknown, or an argument is required and missing, or given and unwanted.

type Group

type Group string

Group is the kind of reader a rendering serves.

Nine names in one alphabetical list answer "what exists" and never "which one do I want". Five groups answer the second question, and the grouping is load-bearing rather than decorative: the pager pages what a person reads and nothing else, and it asks the formatter its group rather than keeping a list of names that would drift the first time a rendering was added.

The values are the display names, because they are shown as headings in `--output`'s help and in the generated man pages.

const (

	// GroupComposed is for renderings whose shape the caller chose: `template=BODY` and `value`. The filter
	// stage built the shape and this stage prints it, so paging or decorating it would be this suite second-
	// guessing a shape it was handed.
	GroupComposed Group = "Composed"

	// GroupDocument is for renderings that produce a document: `markdown` and `terminal`. They differ in
	// whether the markup is rendered.
	GroupDocument Group = "Document"

	// GroupNothing is for `none`, which emits nothing at all: the exit code and the side effects are the
	// result.
	GroupNothing Group = "Nothing"

	// GroupRecords is for records laid out for a person: `list` and `table`. They differ in whether the
	// records share one schema -- `table` derives one column set and leaves holes, `list` gives each record
	// its own keys (§8).
	GroupRecords Group = "Records"

	// GroupSerialized is for the lossless renderings a library reads back: `csv`, `json` and `yaml`. That
	// yaml is pleasanter to read than json is a property of yaml, not a different job -- nobody hand-writes a
	// `writ status` result.
	GroupSerialized Group = "Serialized"
)

The groups. Alphabetical, as every set in this suite is presented.

type HasHeaders

type HasHeaders interface {

	// Headers returns the column order to use when rendering the receiver as CSV.
	Headers() []string
}

HasHeaders is the opt-in interface that overrides automatic header inference. Implementations are typically named-slice types whose row shape doesn't lend itself to reflection (e.g., heterogeneous row content keyed by symbolic names).

type JQFilter

type JQFilter struct {
	// contains filtered or unexported fields
}

JQFilter applies a jq expression to the value via github.com/itchyny/gojq.

The input is normalized to gojq's native shape (map[string]any / []any / primitives) by round-tripping through encoding/json. This costs a serialization pass per Apply but lets callers hand any JSON-serializable Go value to the filter without first converting it.

The expression is parsed and compiled at construction; parse errors surface there. Execution errors from gojq propagate through Apply. Single-result expressions return the unwrapped value; multi-result expressions (jq's `,` operator, `..` recursive descent, etc.) collect into a slice.

func NewJQFilter

func NewJQFilter(expression string) (*JQFilter, error)

NewJQFilter parses and compiles a jq expression into a JQFilter.

Parameters:

  • expression: the jq query, e.g. ".[] | select(.kind == \"file\")".

Returns:

  • *JQFilter: the constructed filter.
  • error: when expression fails to parse or compile.

func (*JQFilter) Apply

func (f *JQFilter) Apply(value any) (any, error)

Apply normalizes value to gojq's native shape, runs the compiled query, and returns the result.

A single-result expression returns the unwrapped value. A multi-result expression collects every emitted value into an []any. An expression that emits no values returns nil. Execution errors are returned as-is.

type JSONFormatter

type JSONFormatter struct{}

JSONFormatter renders the value as indented JSON. Two-space indentation; no HTML escaping concerns (the stream is for tooling consumption, not browser embedding).

func (JSONFormatter) Format

func (JSONFormatter) Format(value any, w io.Writer) error

Format encodes value as indented JSON to w.

func (JSONFormatter) Group

func (JSONFormatter) Group() Group

Group reports that JSON is serialized.

Returns:

type ListFormatter

type ListFormatter struct{}

ListFormatter renders each record as one field per line, keys aligned within the record.

This is the rendering for a result that is wide, heterogeneous, or both -- where `table` is unreadable and `json` is punctuation a reader has to see past:

name  : x
state : active

name     : y
runs     : 3
findings : ["a","b"]

It shares key derivation with DelimitedFormatter and TableFormatter -- `csv:"name"` tags, struct declaration order, sorted map keys -- and diverges on one point: the delimited formats derive ONE column set as the union across every record, where this gives each record its own keys. That is what makes it right for a heterogeneous stream, where a union renders mostly holes.

Keys are padded within a record rather than across the stream, so a heterogeneous stream does not pay for its widest key everywhere. The separator is " : " with the colon aligned, deliberately not "key: value", which reads as YAML when `-o yaml` is one flag away and means something else.

func NewListFormatter

func NewListFormatter() ListFormatter

NewListFormatter returns the one-field-per-line formatter.

Returns:

  • `ListFormatter`: the formatter.

func (ListFormatter) Format

func (f ListFormatter) Format(value any, w io.Writer) error

Format writes value as records of aligned `key : value` lines separated by blank lines.

Parameters:

  • `value`: the result to render.
  • `w`: the destination.

Returns:

  • `error`: a write failure, or nil.

func (ListFormatter) Group

func (ListFormatter) Group() Group

Group reports that a list is records.

Returns:

type MarkdownFormatter

type MarkdownFormatter struct{}

MarkdownFormatter renders a value as a markdown document.

It shares its column inference with TableFormatter and the delimited formats -- HasHeaders, `csv:"name"` tag overrides, and the map-key union -- so one value names the same columns whether it is asked for as `markdown`, `table`, `csv`, or `value`. Only the presentation differs.

Two rules decide the shape, and between them they answer §8's eight:

  • **A scalar string passes through verbatim.** A command whose result is prose returns the document itself, and a document is not a datum to be laid out. `star docs starlark` is one.
  • **Otherwise: a GFM table where the inference yields headers, a bullet list where it does not.** GitHub-flavored markdown has no headerless table -- the delimiter row is required -- and synthesizing column names would name fields the data does not have.

Headers keep the case the JSON gives them, unlike TableFormatter, which upper-cases. These are the names the `json:` tags declare and the names the Starlark surface shows a customer; a markdown document is read by people and by GitHub, and neither wants them shouted.

A non-scalar cell renders as compact JSON, at any depth, never truncated -- the rule §8 states for every presentation that lays data out.

func NewMarkdownFormatter

func NewMarkdownFormatter() MarkdownFormatter

NewMarkdownFormatter returns the markdown document renderer.

Returns:

  • `MarkdownFormatter`: the formatter.

func (MarkdownFormatter) Format

func (f MarkdownFormatter) Format(value any, w io.Writer) error

Format renders value as a markdown document to w.

Parameters:

  • `value`: any normalized value; a string is the document itself.
  • `w`: the destination.

Returns:

  • `error`: any write error.

func (MarkdownFormatter) Group

func (MarkdownFormatter) Group() Group

Group reports that markdown is a document.

Returns:

type NoOpFilter

type NoOpFilter struct{}

NoOpFilter is the pass-through Filter; Apply returns its argument unchanged.

func (NoOpFilter) Apply

func (NoOpFilter) Apply(value any) (any, error)

Apply returns value unchanged.

Parameters:

  • value: the value to pass through.

Returns:

  • any: value unchanged.
  • error: always nil.

type NoneFormatter

type NoneFormatter struct{}

NoneFormatter renders nothing at all.

This is not the same as redirecting to the null device. Redirection renders the value and then discards the bytes; this never renders it. The distinction matters where no shell exists to redirect -- a config file or an environment variable can select a rendering, and "no result" has to be one of them.

`aws` ships the same value as `off`; `az` and `gcloud` spell it `none`.

func (NoneFormatter) Format

func (NoneFormatter) Format(_ any, _ io.Writer) error

Format writes nothing and reports no error.

Parameters:

  • `value`: ignored.
  • `w`: never written to.

Returns:

  • `error`: always nil.

func (NoneFormatter) Group

func (NoneFormatter) Group() Group

Group reports that none emits nothing.

Returns:

type Pipeline

type Pipeline struct {
	// contains filtered or unexported fields
}

Pipeline composes a Filter and a Formatter against a sink.Sink. Callers Emit values; the Pipeline applies the filter, hands the result to the formatter, and the formatter writes through the sink.

All fields are unexported and set at construction by NewPipeline; the value is immutable from the caller's perspective. To suppress all output, construct with sink.Discard as the sink.

func NewPipeline

func NewPipeline(filter Filter, formatter Formatter, s sink.Sink) *Pipeline

NewPipeline constructs an immutable Pipeline writing through the supplied sink.

Parameters:

  • filter: the selection stage. Pass nil to use NoOpFilter pass-through.
  • formatter: the rendering stage. Must not be nil.
  • s: the sink.Sink to write through. Must not be nil. Pass sink.Discard to suppress all emissions; pass sink.Stdout for the standard cli case.

Returns:

  • *Pipeline: the constructed pipeline.

func (*Pipeline) Emit

func (p *Pipeline) Emit(value any) error

Emit runs value through the filter, then hands the filtered value to the formatter, which writes through the sink.

Parameters:

  • value: the value to emit.

Returns:

  • error: non-nil if the filter rejects value, or if the formatter fails to encode it.

type TableFormatter

type TableFormatter struct {

	// MinPadding is the space between the longest cell of a column and the next column. Zero means two.
	MinPadding int
}

TableFormatter renders a slice of rows as aligned columns for a human to read.

It shares its column inference with the delimited formats -- HasHeaders, `csv:"name"` tag overrides, and the map-key union -- so one value renders the same columns whether it is asked for as `table`, `csv`, or `value`. Only the presentation differs: commas, tabs, or padding.

Alignment is text/tabwriter's, which measures cell widths in runes rather than bytes. A hand-rolled `%-30s` counts bytes and so misaligns every row containing a multi-byte character -- the defect #741 records against lore's search table.

Headers are upper-cased, matching `aws`, `kubectl`, and star's own table.

This is the human rendering. A spreadsheet wants `csv`; a shell pipeline wants `value`.

func NewTableFormatter

func NewTableFormatter() TableFormatter

NewTableFormatter returns the aligned-column renderer.

Returns:

  • `TableFormatter`: the formatter.

func (TableFormatter) Format

func (f TableFormatter) Format(value any, w io.Writer) error

Format renders value as aligned columns to w.

Parameters:

  • `value`: a slice or array of structs or maps, or a value implementing HasHeaders.
  • `w`: the destination.

Returns:

  • `error`: when value is not a slice or array, or a row cannot be rendered.

func (TableFormatter) Group

func (TableFormatter) Group() Group

Group reports that a table is records.

Returns:

type TemplateFormatter

type TemplateFormatter struct {
	// contains filtered or unexported fields
}

TemplateFormatter renders a value through a text/template.Template.

Reached as `--output template=<body>`, never as a separate flag: the format value carries its own argument, so there is no pairing to get wrong and no state where a body is supplied and ignored.

The value passed to [Format] is the template's `.` binding. The template is parsed once at construction, so a malformed body fails when the pipeline is built rather than per emission.

Most reshaping belongs in the filter stage instead. `--jq` selects, maps, and interpolates, and composes with every format; a template earns its place only for text layout a query cannot express.

func NewTemplateFormatter

func NewTemplateFormatter(body string) (*TemplateFormatter, error)

NewTemplateFormatter parses body and returns the formatter that renders through it.

Parameters:

  • `body`: the template text; the value being rendered is its `.` binding.

Returns:

  • `*TemplateFormatter`: the formatter.
  • `error`: when the body does not parse.

func (*TemplateFormatter) Format

func (t *TemplateFormatter) Format(value any, w io.Writer) error

Format executes the template against value, writing to w.

Parameters:

  • `value`: the template's `.` binding.
  • `w`: the destination.

Returns:

  • `error`: any execution or write error.

func (*TemplateFormatter) Group

func (*TemplateFormatter) Group() Group

Group reports that a template is composed by its caller.

Returns:

type TerminalFormatter

type TerminalFormatter struct {

	// WordWrap is the column text wraps at. Zero means [terminalWordWrap]. Fixed rather than read from the
	// terminal, since reading it would be a probe.
	WordWrap int

	// NoColor drops every color and keeps every attribute. [NewTerminalFormatter] sets it from `NO_COLOR`.
	NoColor bool
	// contains filtered or unexported fields
}

TerminalFormatter renders a value as a document laid out for a terminal: bold, italic, color, word wrap, and box-drawn tables.

It is the second link of a chain. MarkdownFormatter turns the normalized JSON into a markdown document, and this formatter hands that document to glamour: goldmark parses it, glamour's renderer walks the tree, and [terminalStyle] decides what each element becomes.

Every marker is consumed and re-emitted as an attribute -- a heading loses its `#`, `**` becomes bold, `*` becomes italic, a code span loses its backticks. That is what separates this from glamour's own `notty` style, which keeps every marker, and from `markdown`, which is the source.

Nothing is probed. glamour's color profile defaults to TrueColor rather than asking the terminal, and the style is fixed rather than glamour's `auto`, which reads the terminal's background. So the bytes are the same piped, redirected, or on a TTY -- §7's rule that a rendering does not change when it is observed. A caller who wants no escape codes asks for `markdown`.

`NO_COLOR` is honored, because it is an input rather than a probe. Set and not empty, it drops every color and keeps every attribute: no-color.org's rule suppresses color, not bold, italic, or underline. §10 of the specification records why the suite honors a convention no standards body stands behind.

func NewTerminalFormatter

func NewTerminalFormatter() TerminalFormatter

NewTerminalFormatter returns the terminal document renderer.

Returns:

  • `TerminalFormatter`: the formatter.

func (TerminalFormatter) Format

func (f TerminalFormatter) Format(value any, w io.Writer) error

Format renders value as markdown, then renders that markdown for a terminal, to w.

Parameters:

  • `value`: any normalized value; a string is the document itself.
  • `w`: the destination.

Returns:

  • `error`: when the renderer cannot be built or the document cannot be rendered, or any write error.

func (TerminalFormatter) Group

func (TerminalFormatter) Group() Group

Group reports that the terminal rendering is a document.

Returns:

type YAMLFormatter

type YAMLFormatter struct{}

YAMLFormatter renders the value as indented YAML. Two-space indentation; latest stable yaml.v3 from go-yaml.

func (YAMLFormatter) Format

func (YAMLFormatter) Format(value any, w io.Writer) (err error)

Format encodes value as indented YAML to w.

The yaml.Encoder is closed before return to flush trailing bytes. Close errors are wrapped in the returned error chain via the deferred sequence.

func (YAMLFormatter) Group

func (YAMLFormatter) Group() Group

Group reports that YAML is serialized.

Returns:

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL