Emit JSON for scripting¶
A CLI that is scriptable needs a stable, machine-readable envelope alongside its
human output. output provides the Response envelope and Emit.
The envelope¶
type Response struct {
Status string `json:"status"` // "success" | "error" | "warning"
Command string `json:"command"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
Emit — only when the format is JSON¶
Emit writes the envelope only when the renderer's format is json; in any
other format it is a no-op. That lets you write both paths unconditionally:
r := output.New(output.WithWriter(os.Stdout), output.WithFormat(format))
result, err := doWork()
if err != nil {
// In JSON mode this prints an error envelope; in text mode it is a no-op,
// so return the error and let your normal text error-handling run.
_ = r.EmitError("update", err)
return err
}
if r.IsJSON() {
return r.Emit(output.Response{
Status: output.StatusSuccess,
Command: "update",
Data: result,
})
}
// Human-readable path:
fmt.Fprintf(os.Stdout, "Updated to %s\n", result.Version)
return nil
Output for --output json:
From a cobra command¶
If you build on cobra, the output/cobra subpackage reads the --output flag and
the command's writer for you — see Wire it to a cobra CLI:
import ocobra "gitlab.com/phpboyscout/go/output/cobra"
return ocobra.NewRenderer(cmd).Emit(output.Response{
Status: output.StatusSuccess, Command: "update", Data: result,
})
Emit once per invocation¶
Each Emit call writes a complete indented JSON document followed by a
newline. Two calls produce two documents back to back — a stream of JSON values,
not a single valid JSON value. jq reads such a stream by default, but a
consumer that needs one value has to slurp it (jq -s), and any parser that
expects exactly one document will reject the second. Put everything the caller
needs inside one Data payload.
There is no YAML envelope¶
Emit writes only when the format is json. Under --output yaml — and under
csv, tsv, markdown and text — it returns nil having written nothing.
That is what makes the unconditional EmitError above safe, but it does mean a
YAML consumer gets the raw payload from Write with no status field.
If a script needs the envelope's fields in YAML, model them yourself:
type envelope struct {
Status string `yaml:"status"`
Command string `yaml:"command"`
Data any `yaml:"data,omitempty"`
}
_ = r.Write(envelope{Status: output.StatusSuccess, Command: "update", Data: result}, nil)
Also worth knowing¶
Renderis not silenced by JSON mode's siblings. It is a no-op injsononly; inyaml,csv,tsvandmarkdownit writes ANSI escape sequences into the stream. See behaviour by format.- Send progress feedback to stderr. A spinner on the same writer as the envelope corrupts it — interactive vs plain rendering explains the two-renderer idiom.
--output jsnosilently produces text.ParseFormatnever errors; validate it yourself if a typo should be fatal (limitations).