output

package
v0.0.0-...-9997ea8 Latest Latest
Warning

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

Go to latest
Published: Nov 5, 2026 License: BSD-2-Clause Imports: 16 Imported by: 0

Documentation

Overview

Package output writes a command's result to stdout.

stdout carries the result and nothing else; diagnostics, progress and prompts go to stderr through github.com/tarantool/tt/sdk/log. The result format (--format) is independent of the log format (--log-format).

A command renders its result through a Printer bound to the process streams and the format the user chose. The human format is the command's own rendering, Result.Human; a machine format encodes the result value. A machine-format result is written whole or not at all, so a command that fails leaves stdout empty - there is no error envelope; the error goes to stderr and into the exit code.

The format is chosen by the --format flag alone: a command declares the flag with BindFormat, which takes the formats it accepts and its fixed default, and ResolveFormat maps a flag value to a format. Whether stdout is a terminal never changes the format; it may change only styling - colour, width, truncation - and a command reads it from Printer.Terminal.

The SDK encodes JSON itself. Any other machine format is supplied by the tt core with WithEncoder, which keeps its encoder library out of the SDK.

A result too large to hold - the tuples of a big space, records read from xlogs, a log being followed - is written item by item through a Stream from Printer.Stream. In JSON a stream is JSON Lines: one compact value per line, no enclosing array. In the human format each item renders itself with Result.Human, so columns cannot be aligned across items: an item that is a table row pads its cells to fixed widths, and a heading or a summary is printed with Print before the stream opens or after it closes. Another machine format streams only if the core gives it a stream encoder with WithStreamEncoder; YAML's stream, say, is a sequence of documents.

A stream is not all-or-nothing. Each item is written whole or not at all, and as soon as it is emitted; when a command fails midway the items already written stay on stdout, the error goes to stderr and into the exit code, and the exit code is how a consumer tells a complete stream from a cut one. While a stream is open, Print, Printf, Emit and Stream refuse with ErrStreamOpen, so nothing interleaves with the items.

The JSON encoder passes every value through Normalize first, so a result may carry what a MessagePack decoder produced - maps keyed by interfaces, integers or bools - without the command converting it. A core encoder calls Normalize itself when its format needs it.

A float that is NaN, +Inf or -Inf has no JSON number, so it is written as the string "NaN", "Infinity" or "-Infinity", while a finite float stays a number: the JSON type of such a value depends on the value. This is how encoding/json/v2 writes a float under `json:",format:nonfinite"` and how the proto3 JSON mapping writes a double.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrKeyCollision reports a map in which two distinct keys have the same
	// string form, such as the integer 1 and the string "1".
	ErrKeyCollision = errors.New("map keys collide")
	// ErrNotNormalizable reports a value Normalize cannot make encodable:
	// a map key with no string form, a struct field whose declared type
	// cannot hold the normalised value, or a reference cycle.
	ErrNotNormalizable = errors.New("value cannot be normalised")
)
View Source
var (
	// ErrUnknownFormat reports a format that is neither human nor one the
	// Printer has an encoder for.
	ErrUnknownFormat = errors.New("unknown output format")
	// ErrNoMachineForm reports human-only text printed while a machine format
	// was chosen. Such output has no machine form: emit a Result instead.
	ErrNoMachineForm = errors.New("human-only output in a machine format")
	// ErrNoStream reports streams without a stdout.
	ErrNoStream = errors.New("no output stream")
	// ErrNoStreamForm reports a stream requested in a machine format that
	// has no stream encoder.
	ErrNoStreamForm = errors.New("output format cannot be streamed")
	// ErrStreamOpen reports output attempted on a Printer while one of its
	// streams is open.
	ErrStreamOpen = errors.New("an output stream is open")
	// ErrStreamClosed reports an item emitted to a closed stream.
	ErrStreamClosed = errors.New("output stream is closed")
)

Functions

func Normalize

func Normalize(value any) (any, error)

Normalize returns value in a form every machine encoder can take: the shape of a document decoded from MessagePack - a Tarantool tuple, a Lua table - with no map keyed by an interface, a bool or a float. The JSON encoder applies it to every result and stream item; a core encoder for another format calls it when that format needs it.

The rules:

  • A map whose key type is an interface, a bool or a float becomes a map[string]any. A string key stays as it is, a TextMarshaler key becomes its text, an integer or a bool its decimal or true/false spelling, a finite float its shortest 'g' formatting and a non-finite one "NaN", "Infinity" or "-Infinity". A key of any other kind, nil included, is ErrNotNormalizable. Maps keyed by strings, integers or TextMarshalers keep their key type, as encoding/json handles them.
  • Two keys of one map with the same string form - 1 and "1", NaN and "NaN", two NaNs - are ErrKeyCollision naming both; neither is dropped.
  • A float32 or float64 value that is NaN, +Inf or -Inf becomes the string "NaN", "Infinity" or "-Infinity"; a finite float stays a number. JSON has no number for them, and encoding/json refuses them. The JSON type of such a value then depends on the value - a number normally, a string when it is non-finite - which is how encoding/json/v2 encodes a float under `json:",format:nonfinite"` and how the proto3 JSON mapping encodes a double. A struct field declared as a float cannot hold the string, so a non-finite value in one is ErrNotNormalizable.
  • []byte is left as it is, so JSON carries it as base64. MessagePack tells a binary value from a string, and a JSON string keeps that distinction only if it does not depend on the content: a binary value that happens to be valid UTF-8 is still binary.
  • A value whose type implements json.Marshaler or encoding.TextMarshaler is left to its own marshalling.
  • A struct with no exported and no embedded fields that implements fmt.Stringer becomes its String form. encoding/json would render it as {}, losing the value; go-tarantool's datetime.Datetime is such a type.
  • Slices, arrays, maps, pointers, interfaces and the exported fields of structs, exported embedded ones included, are walked. A container whose element type cannot hold a normalised element becomes a []any or a map with any values; a struct field that cannot hold its normalised value is ErrNotNormalizable - declare such a field as any. Fields tagged json:"-" and the fields of an unexported embedded struct are not walked. A reference cycle is ErrNotNormalizable.

Normalize never mutates value: whatever it changes is a copy, and what it does not change is shared. A value that needs no change is returned as it is.

Types

type Encoder

type Encoder func(w io.Writer, value any) error

Encoder writes value to w in a machine format.

type Format

type Format string

Format names how a result is rendered.

const (
	// FormatHuman is the command's own rendering for people: a table, a tree,
	// a sentence. It is spelled "table", as every tt command with a --format
	// flag spells its human format.
	FormatHuman Format = "table"
	// FormatJSON is indented JSON with a trailing newline.
	FormatJSON Format = "json"
	// FormatYAML is YAML at a two-space indent; a stream of results is a
	// sequence of YAML documents. The SDK does not encode it: the core adds
	// its encoders with WithEncoder and WithStreamEncoder, so a Printer made
	// without them refuses it with ErrUnknownFormat.
	FormatYAML Format = "yaml"
)

func ResolveFormat

func ResolveFormat(flag string, fallback Format) Format

ResolveFormat returns the format a --format flag value selects: the value itself, or fallback - the command's default - when the flag is empty. It is a pure function of its arguments; in particular it does not depend on whether stdout is a terminal. Whether the result names a format the command can render is checked by NewPrinter.

type FormatFlag

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

FormatFlag is a -o/--format flag declared by BindFormat: the format the user named, checked against the formats the command accepts, or the command's default.

func BindFormat

func BindFormat(flags *pflag.FlagSet, fallback Format, formats ...Format) *FormatFlag

BindFormat declares -o/--format on flags, accepting formats and defaulting to fallback, and returns the flag to read the chosen format from once the flags are parsed. Every command that prints a result declares its format flag with BindFormat, so the flag has one name, one shorthand and one help text everywhere: the accepted values in the order given, and the default.

A value outside formats fails flag parsing with ErrUnknownFormat. An empty value selects fallback, as ResolveFormat does. The terminal plays no part: without the flag the format is fallback whatever stdout is.

BindFormat panics when formats is empty, names a format twice or does not include fallback - a mistake in the command's code, not in its input - and, as pflag does, when flags already has a format or o flag.

func (*FormatFlag) Format

func (f *FormatFlag) Format() Format

Format returns the format the flag selects: the value given, or the default when the flag was not given or given empty.

func (*FormatFlag) Set

func (f *FormatFlag) Set(value string) error

Set takes a value from the command line. A format the command does not accept is ErrUnknownFormat.

func (*FormatFlag) String

func (f *FormatFlag) String() string

String returns the selected format; pflag shows it as the default in the help text.

func (*FormatFlag) Type

func (*FormatFlag) Type() string

Type names the flag's value type in the help text.

type NewStreamEncoder

type NewStreamEncoder func(w io.Writer) StreamEncoder

NewStreamEncoder starts a stream encoder writing to w. Anything it writes before returning - a header - reaches stdout when the stream opens.

type Option

type Option func(*Printer)

Option configures a Printer.

func WithEncoder

func WithEncoder(format Format, encoder Encoder) Option

WithEncoder makes format a machine format rendered by encoder, replacing any encoder the format already has. The core uses it to add formats the SDK does not encode itself, such as YAML.

func WithStreamEncoder

func WithStreamEncoder(format Format, newEncoder NewStreamEncoder) Option

WithStreamEncoder lets format be streamed by the encoders newEncoder starts, replacing any stream encoder the format already has. The format must also have an encoder, built in or given by WithEncoder. JSON streams out of the box; the core adds streaming to the formats it registers, such as YAML.

func WithTerminalProbe

func WithTerminalProbe(probe TerminalProbe) Option

WithTerminalProbe sets how the Printer finds out whether stdout is a terminal. The core passes its isatty check; tests pass a constant. Without a probe stdout counts as not a terminal, which selects plain styling.

type Printer

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

Printer writes results to stdout in one format.

func NewPrinter

func NewPrinter(streams Streams, format Format, opts ...Option) (*Printer, error)

NewPrinter returns a Printer that writes to streams.Out in format. The format must be FormatHuman, FormatJSON, or one given an encoder by opts.

func (*Printer) Emit

func (p *Printer) Emit(result Result) error

Emit renders result to stdout: its Human rendering in the human format, the encoded value otherwise. A machine format is encoded in full before anything is written, so an encoding error leaves stdout untouched.

func (*Printer) Format

func (p *Printer) Format() Format

Format returns the format the Printer renders in.

func (*Printer) Print

func (p *Printer) Print(args ...any) error

Print writes human text to stdout, formatted as fmt.Fprint does. It is for a result that exists only as text; in a machine format it writes nothing and returns ErrNoMachineForm.

func (*Printer) Printf

func (p *Printer) Printf(format string, args ...any) error

Printf writes human text to stdout, formatted as fmt.Fprintf does. Like Print, it returns ErrNoMachineForm in a machine format.

func (*Printer) Stream

func (p *Printer) Stream() (*Stream, error)

Stream opens a stream on stdout. In the human format every item is rendered by its Human method; in a machine format it is encoded by the format's stream encoder, and a format without one is ErrNoStreamForm. A Printer has at most one open stream: while it is open, Stream, Print, Printf and Emit return ErrStreamOpen.

func (*Printer) Terminal

func (p *Printer) Terminal() bool

Terminal reports whether stdout is a terminal, as the probe given by WithTerminalProbe answered when the Printer was made. It is for styling a human rendering - colour, width, truncation - and never for choosing the format.

func (*Printer) Writer

func (p *Printer) Writer() io.Writer

Writer returns stdout itself, for output the Printer does not shape - bytes passed through as they are. The command is then responsible for writing the chosen format; records written as they are read go through Stream instead. Writes through Writer are not checked against an open stream: keeping them apart is the caller's job.

type Result

type Result interface {
	Human(w io.Writer) error
}

Result is a command's result.

Human writes the human rendering to w; it writes nothing that is not the result, so a note about the result (a stale lock, say) is logged to stderr instead. A machine format encodes the Result value itself, so its exported fields and struct tags are the machine schema; tt's payloads use snake_case keys.

type Stream

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

Stream writes a result to stdout one item at a time, each as soon as it is emitted. It is opened by Printer.Stream and must be closed; until then the Printer refuses any other output. A Stream is safe for concurrent use: items emitted from several goroutines are written one after another, never interleaved.

func (*Stream) Close

func (s *Stream) Close() error

Close ends the stream: the stream encoder writes what follows the last item, and the Printer accepts other output again, even when Close fails. Closing a closed stream does nothing.

func (*Stream) Emit

func (s *Stream) Emit(item Result) error

Emit writes item to stdout: its Human rendering in the human format, its encoding otherwise. The item is rendered in full before anything is written, so an item that fails to render writes nothing, and the stream stays open for the next one. Items emitted before stay on stdout whatever happens after them. When stdout can be flushed it is flushed after every item.

type StreamEncoder

type StreamEncoder interface {
	Encode(item any) error
	Close() error
}

StreamEncoder writes the items of one stream in a machine format.

Encode writes one item; Close ends the stream and writes whatever the format puts after the last item. The writer a StreamEncoder is given collects what one call writes: what Encode wrote reaches stdout when it returns nil, and is discarded when it fails. Encode is not called concurrently.

type Streams

type Streams struct {
	// In is where input and prompt answers are read from.
	In io.Reader
	// Out receives the command's result and nothing else.
	Out io.Writer
	// Err receives diagnostics, progress and prompts.
	Err io.Writer
}

Streams are the standard streams a command works with.

func StdStreams

func StdStreams() Streams

StdStreams returns the process's stdin, stdout and stderr.

type TerminalProbe

type TerminalProbe func(w io.Writer) bool

TerminalProbe reports whether w is a terminal.

Jump to

Keyboard shortcuts

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