Documentation
¶
Overview ¶
Package output writes one result in whichever shape was asked for: aligned text, CSV, JSON or YAML.
The premise is that a program builds two representations of the same result and lets the requested Format choose between them:
- a rich object, for JSON and YAML, where nesting, types and optional fields all survive; and
- a deliberately simplified table, for text and CSV, where a person reading a terminal wants a handful of columns and not the object.
The two are built independently, on purpose. The alternative -- deriving the table from the object, or the object from the table -- forces the richer shape through the poorer one and makes both worse: JSON grows stringly-typed cells and pre-formatted numbers, while the table grows columns nobody wanted to read. Writing both is a few lines in the program that has the data, and each comes out shaped for its reader.
doc := output.Document{Data: result, Table: summary}
if err := doc.Write(os.Stdout, format); err != nil {
return err
}
A Document only needs the representation the chosen format uses: JSON of a Document with no Table is fine, and CSV of one with no Data is fine. Asking for a format whose representation is missing is an error naming the format, and nothing is written.
WriteJSON and WriteYAML are the same encoders without the Document, for a program that has only the object.
Example ¶
Example builds both representations of the same result -- the object for JSON and YAML, a simplified table for text and CSV -- and lets the format choose. The table deliberately leaves out the detector: it is detail the machine wants and the person reading a terminal does not.
package main
import (
"fmt"
"os"
"github.com/wisborg/output"
"github.com/wisborg/output/table"
)
// probe is the result of one comparison: the object a machine gets.
type probe struct {
Clip string `json:"clip" yaml:"clip"`
Offset float64 `json:"offset" yaml:"offset"`
Score float64 `json:"score" yaml:"score"`
Detector string `json:"detector" yaml:"detector"`
}
func main() {
results := []probe{
{Clip: "corner_1", Offset: 2.70, Score: 0.821, Detector: "phase-correlation"},
{Clip: "corner_3", Offset: 2.75, Score: 0.366, Detector: "phase-correlation"},
}
summary := table.New(
table.Column{Header: "clip"},
table.Column{Header: "offset", Align: table.Right, Format: "%+.2fs"},
table.Column{Header: "score", Align: table.Right, Format: "%.3f"},
)
for _, r := range results {
summary.MustAppend(r.Clip, r.Offset, r.Score)
}
doc := output.Document{Data: results, Table: summary}
if err := doc.Write(os.Stdout, output.Text); err != nil {
fmt.Println("write:", err)
}
// The same document as JSON is the object, detector and all.
if err := doc.Write(os.Stdout, output.JSON); err != nil {
fmt.Println("write:", err)
}
}
Output: clip offset score ------------------------- corner_1 +2.70s 0.821 corner_3 +2.75s 0.366 [ { "clip": "corner_1", "offset": 2.7, "score": 0.821, "detector": "phase-correlation" }, { "clip": "corner_3", "offset": 2.75, "score": 0.366, "detector": "phase-correlation" } ]
Index ¶
Examples ¶
Constants ¶
const DefaultYAMLIndent = 2
DefaultYAMLIndent is the number of spaces per nesting level when YAMLStyle.Indent is 0. Two is the prevailing convention, and the one Kubernetes, Docker Compose and most hand-written YAML use; the library's own default is four.
Variables ¶
var ( // ErrNoData is returned when a format that writes Document.Data is // asked for and Data is nil. ErrNoData = errors.New("output: no Data") // ErrNoTable is returned when a format that writes Document.Table is // asked for and Table is nil. ErrNoTable = errors.New("output: no Table") // ErrUnknownFormat is the sentinel behind every "this is not a format // I know" error, whether the format arrived as a string from a flag or // as an out-of-range Format value. ErrUnknownFormat = errors.New("output: unknown format") )
The sentinels every error from this package can be tested for with errors.Is. Each returned error wraps one of these and adds what was actually wrong -- which format was asked for, or what was typed.
var ErrYAMLIndent = errors.New("output: unsupported YAML indent")
ErrYAMLIndent is returned for a YAMLStyle.Indent the emitter would not honour. See YAMLStyle.Indent for the accepted range.
Exported, where errUnsupportedYAMLType is not, because the two differ in how firm a promise they can make. This one is this package's own check on its own field: it fires exactly when we say it does. The other depends on recognising an upstream panic by its text, so a caller matching on it would be relying on a dependency's private wording.
Functions ¶
func WriteJSON ¶
WriteJSON writes v as JSON, followed by exactly one newline.
A nil v writes "null" -- JSON's own spelling of "no value", and the only honest rendering of the argument it was given. Note the deliberate asymmetry with Document.Write, where a nil Data is instead an error wrapping ErrNoData: there the nil is a field nobody filled in, which is far more often a bug than a value, and the Document knows which format needed it. Here the value is the argument, stated by the caller, so writing it is what was asked for.
Object keys are sorted by encoding/json (map keys alphabetically, struct fields in declaration order); there is no knob for that here.
Marshalling errors are wrapped with %w rather than flattened into text, because encoding/json's error types carry things a caller can act on: *json.UnsupportedTypeError names the offending type, *json.MarshalerError names the type whose MarshalJSON failed and unwraps to its error, and *json.UnsupportedValueError covers NaN, +Inf and a cyclic value.
Example ¶
ExampleWriteJSON writes an object without a Document, for a program that has no table to offer. Note that HTML characters are left alone, unlike encoding/json's default: this output is for a terminal or for jq.
package main
import (
"fmt"
"os"
"github.com/wisborg/output"
)
func main() {
v := map[string]string{"url": "https://example.test/?a=1&b=2"}
if err := output.WriteJSON(os.Stdout, v, output.JSONStyle{}); err != nil {
fmt.Println("write:", err)
}
if err := output.WriteJSON(os.Stdout, v, output.JSONStyle{Compact: true}); err != nil {
fmt.Println("write:", err)
}
}
Output: { "url": "https://example.test/?a=1&b=2" } {"url":"https://example.test/?a=1&b=2"}
func WriteYAML ¶
WriteYAML writes v as YAML, indented per style, followed by exactly one newline. There is no leading "---": a single document does not need one, and it is noise in the common case of a program printing one result.
A nil v writes "null", exactly as WriteJSON does, and for the same reason: the value is the argument the caller passed, and null is YAML's word for it. See WriteJSON for the asymmetry with Document.Write, where a nil Data is instead an error wrapping ErrNoData.
Mapping keys are ordered by the YAML library: map keys in a sorted order of its own, struct fields in declaration order. There is no knob for that here.
The document is encoded in memory first ¶
The YAML encoder streams, so a failure part-way through an encode has already put a large fragment of a document on the writer -- valid-looking YAML, truncated mid-value, with no trailing newline. WriteYAML therefore encodes into a buffer and copies to w only once the whole document is known to be good, which makes it atomic in the way encoding/json already is: on any error, nothing is written at all. That is what lets Document.Write promise that output is either empty or ends in exactly one newline, rather than promising it for three formats and excusing the fourth. The cost is holding the rendered document in memory, which for program output is a document a person or a pipeline was about to read anyway.
A type YAML cannot encode is an error here, not a panic ¶
The underlying encoder PANICS on a value it cannot marshal -- a channel or a function, where encoding/json returns *json.UnsupportedTypeError. A library where the value of a --format flag decides whether the program crashes is not one you can build a CLI on, so WriteYAML recovers that one panic and returns it as an error wrapping an unexported sentinel. The recovery is deliberately narrow: only a panic whose value is a string beginning "cannot marshal type: " is converted, and anything else is re-panicked, because anything else is a bug in the encoder and swallowing it would hide it.
A cyclic value still takes the process down ¶
This cannot be fixed here. The YAML encoder follows pointers with no visited set, so a value that points back at itself is encoded again at each level, for ever. What that looks like in practice is not a prompt crash: the encoder spins, emitting an ever-deeper nesting of the same data and consuming CPU and memory without bound, until the buffer can no longer grow or the recursion overflows the stack. Neither end is recoverable -- a stack overflow is fatal in Go, not a panic, and recover cannot see it -- and buffering does not change that, it only moves the unbounded growth from the writer into memory. encoding/json detects cycles and reports them as errors, so the same Document that writes as JSON can take the program down as YAML.
The alternative, a reflective pre-walk of every value looking for cycles, would cost that walk on every write to defend against a shape that almost never occurs in the data a program actually prints, and would have to track the same visited set for types this package knows nothing about. If your data can contain cycles -- a doubly linked list, a parent pointer, a graph -- break them before handing it to any encoder.
Example ¶
package main
import (
"fmt"
"os"
"github.com/wisborg/output"
)
// probe is the result of one comparison: the object a machine gets.
type probe struct {
Clip string `json:"clip" yaml:"clip"`
Offset float64 `json:"offset" yaml:"offset"`
Score float64 `json:"score" yaml:"score"`
Detector string `json:"detector" yaml:"detector"`
}
func main() {
v := probe{Clip: "corner_1", Offset: 2.70, Score: 0.821, Detector: "phase-correlation"}
if err := output.WriteYAML(os.Stdout, v, output.YAMLStyle{}); err != nil {
fmt.Println("write:", err)
}
}
Output: clip: corner_1 offset: 2.7 score: 0.821 detector: phase-correlation
Types ¶
type Document ¶
type Document struct {
// Data is the object written by JSON and YAML. nil means "not
// supplied", and asking for one of those formats is then an error
// wrapping ErrNoData rather than a document reading "null".
//
// Only a nil interface counts as absent. Data holding a TYPED nil
// pointer -- a (*Result)(nil) -- is a value, and marshals as null in
// both formats, because at that point the program has said what it has
// and null is the accurate answer.
Data any
// Table is the table written by Text and CSV. nil means "not
// supplied", and asking for one of those formats is then an error
// wrapping ErrNoTable.
//
// A table with no rows is NOT the same thing: it is a supplied answer
// that happens to be empty, and it follows the table package's own
// rules -- text writes nothing at all, CSV still writes its header.
Table *table.Table
// TextStyle is the style Text is rendered in. The zero value is the
// plain unframed table.
TextStyle table.Style
// CSVStyle is the style CSV is written in. The zero value is ordinary
// comma-separated data with a header row.
CSVStyle table.CSVStyle
// JSONStyle is the style JSON is written in. The zero value is
// indented, without HTML escaping.
JSONStyle JSONStyle
// YAMLStyle is the style YAML is written in. The zero value is
// two-space indentation.
YAMLStyle YAMLStyle
}
Document is one result in both of its representations, plus the styles each format is written in. The zero Document holds neither representation and can be written in no format; fill in the ones your program produces.
func (Document) Write ¶
Write writes the document to w in format f.
Only the representation f needs is required: JSON and YAML use Data, text and CSV use Table. A missing one is an error wrapping ErrNoData or ErrNoTable that names the format asked for, and nothing is written -- the check happens before the first byte, so a caller that reports the error and exits has not already put half a document on the terminal. The same holds for a value the encoder rejects part-way through: JSON and YAML both write all of a document or none of it.
A Format outside the defined set is an error wrapping ErrUnknownFormat. There is deliberately no fallback to text: the format usually comes from a flag, and a program that prints a table when its caller asked for JSON has produced output that cannot be parsed and no message saying why.
The receiver is a value because Write reads the Document and changes nothing in it. Copying the struct copies the Table pointer, not the table.
Whatever the format, the output is either empty or ends in exactly one newline.
Example (Json) ¶
package main
import (
"fmt"
"os"
"github.com/wisborg/output"
)
// probe is the result of one comparison: the object a machine gets.
type probe struct {
Clip string `json:"clip" yaml:"clip"`
Offset float64 `json:"offset" yaml:"offset"`
Score float64 `json:"score" yaml:"score"`
Detector string `json:"detector" yaml:"detector"`
}
func main() {
doc := output.Document{
Data: probe{Clip: "corner_1", Offset: 2.70, Score: 0.821, Detector: "phase-correlation"},
}
if err := doc.Write(os.Stdout, output.JSON); err != nil {
fmt.Println("write:", err)
}
}
Output: { "clip": "corner_1", "offset": 2.7, "score": 0.821, "detector": "phase-correlation" }
Example (Missing) ¶
ExampleDocument_Write_missing shows what happens when the format asks for a representation the Document does not have. Nothing is written, and the error names the format and can be tested for with errors.Is.
package main
import (
"errors"
"fmt"
"os"
"github.com/wisborg/output"
)
// probe is the result of one comparison: the object a machine gets.
type probe struct {
Clip string `json:"clip" yaml:"clip"`
Offset float64 `json:"offset" yaml:"offset"`
Score float64 `json:"score" yaml:"score"`
Detector string `json:"detector" yaml:"detector"`
}
func main() {
doc := output.Document{Data: probe{Clip: "corner_1"}} // no Table
err := doc.Write(os.Stdout, output.CSV)
fmt.Println(err)
fmt.Println(errors.Is(err, output.ErrNoTable))
}
Output: output: no Table to write as csv true
Example (Yaml) ¶
package main
import (
"fmt"
"os"
"github.com/wisborg/output"
)
// probe is the result of one comparison: the object a machine gets.
type probe struct {
Clip string `json:"clip" yaml:"clip"`
Offset float64 `json:"offset" yaml:"offset"`
Score float64 `json:"score" yaml:"score"`
Detector string `json:"detector" yaml:"detector"`
}
func main() {
doc := output.Document{
Data: probe{Clip: "corner_1", Offset: 2.70, Score: 0.821, Detector: "phase-correlation"},
}
if err := doc.Write(os.Stdout, output.YAML); err != nil {
fmt.Println("write:", err)
}
}
Output: clip: corner_1 offset: 2.7 score: 0.821 detector: phase-correlation
type Format ¶
type Format int
Format selects which shape a Document is written in.
The zero value is Text, so a Format field that nobody set is the plain human-readable rendering -- the right default for a command-line program, and the one that makes a struct literal with no Format in it do something sensible.
func Formats ¶
func Formats() []Format
Formats returns every defined format, in declaration order. It is for building a flag's help text or a menu without hard-coding the list a second time and letting the two drift apart.
Each call returns a fresh slice, so a caller may sort or filter it without affecting anyone else.
func ParseFormat ¶
ParseFormat turns a format name into a Format. Surrounding space is trimmed and case is ignored, so "JSON", " json" and "json" are one thing: the name usually comes from a human typing a flag.
The canonical names are those of Format.String; "table" is also accepted for Text and "yml" for YAML.
An unrecognised name is an error wrapping ErrUnknownFormat. There is deliberately no fallback to Text: a program that silently prints a table when asked for JSON produces output its caller cannot parse and no message saying why.
func (Format) MarshalText ¶
MarshalText makes Format encode as its canonical name in JSON, YAML and anything else built on encoding.TextMarshaler -- so a Format inside a config struct is "json" rather than the integer 2, which would be both unreadable and silently wrong the day a format is inserted into the middle of the constant block.
func (*Format) Set ¶
Set parses s into f, making *Format a flag.Value. A format flag is then one line, validated where the flag is parsed rather than where it is used:
format := output.JSON // the default flag.Var(&format, "format", "output format")
Because Format.String is the canonical name, the flag package prints the default in the same spelling Set accepts.
Example ¶
ExampleFormat_Set uses a Format as a flag value. Set validates at parse time, so an unknown name is rejected where the user typed it rather than silently falling back to text.
package main
import (
"flag"
"fmt"
"io"
"github.com/wisborg/output"
)
func main() {
format := output.Text // the default
fs := flag.NewFlagSet("probe", flag.ContinueOnError)
fs.SetOutput(io.Discard) // the usage message is not the point here
fs.Var(&format, "format", "output format")
if err := fs.Parse([]string{"-format", "YAML"}); err != nil {
fmt.Println("parse:", err)
}
fmt.Println(format)
if err := fs.Parse([]string{"-format", "xml"}); err != nil {
fmt.Println(err)
}
}
Output: yaml invalid value "xml" for flag -format: output: unknown format "xml" (accepted: text, csv, json, yaml, table, yml)
func (Format) String ¶
String returns the format's canonical name: "text", "csv", "json" or "yaml". These are the names ParseFormat accepts, so a value that survives String and ParseFormat is unchanged, and a flag's default can be printed with the same spelling the user has to type.
A Format outside the defined set renders as "Format(n)", following the convention of the standard library and of table.Align: an invalid value should be conspicuous in a message rather than pretending to be text.
func (*Format) UnmarshalText ¶
UnmarshalText parses a format name, with the same spellings ParseFormat takes.
type JSONStyle ¶
type JSONStyle struct {
// Compact writes the whole value on one line with no indentation, for
// a log line or for JSON Lines. The default is indented, because the
// usual reader of a CLI's JSON is a person looking at a terminal, and
// the machine reader does not care either way.
Compact bool
// EscapeHTML escapes <, > and & as \u003c, \u003e and \u0026.
//
// The zero value does NOT escape, which inverts encoding/json's
// default. That default exists because the standard library's JSON is
// often embedded in HTML, where those characters can end a script
// element early. Program output is not: it goes to a terminal or into
// jq, and there a URL comes out as "https://x/?a\u0026b", which is
// wrong on sight and stays wrong when the reader copies it. Set this
// when the JSON really is destined for a web page.
EscapeHTML bool
}
JSONStyle controls JSON output. The zero value is what a command-line program wants: indented, and with HTML escaping off.
type YAMLStyle ¶
type YAMLStyle struct {
// Indent is the number of spaces per nesting level. 0 means
// DefaultYAMLIndent, so the zero YAMLStyle is the conventional
// rendering and this field only has to be set to depart from it.
//
// Any other value must be between 2 and 9. That ceiling is the YAML
// emitter's, not this package's: it accepts 2 through 9 and RESETS
// anything else to 2 -- not to the nearer bound, to 2 -- so
// YAMLStyle{Indent: 10} would silently produce output identical to
// never having set Indent at all. WriteYAML therefore rejects an
// out-of-range Indent with an error naming the value and the range,
// and writes nothing. Clamping 10 to 9 was considered and rejected: it
// is a quieter version of the same surprise, and a caller who asked
// for ten spaces is working from an assumption worth correcting rather
// than approximating.
Indent int
}
YAMLStyle controls YAML output. The zero value is two-space indentation.