Output¶
pkg/output is GTB's single source of truth for command output formatting. It provides three complementary capabilities:
Writerβ writes structured data as indented JSON or human-readable text from a single call site.- Response envelope β a standard
{status, command, data, error}JSON schema shared by all built-in commands, withEmit/IsJSONOutput/EmitErrorhelpers to produce it. - Markdown rendering β
RenderMarkdownandWriter.Renderapply glamour ANSI styling in text mode with automatic terminal width detection.
All three integrate with the --output flag defined on the root command.
Quick Start¶
Structured output with Writer¶
import (
"fmt"
"io"
"os"
"gitlab.com/phpboyscout/go-tool-base/pkg/output"
)
type Result struct {
Name string `json:"name"`
Version string `json:"version"`
}
func runMyCommand(cmd *cobra.Command, args []string) error {
format, _ := cmd.Flags().GetString("output")
w := output.NewWriter(os.Stdout, output.Format(format))
result := &Result{Name: "myapp", Version: "v1.2.3"}
return w.Write(output.Response{
Status: output.StatusSuccess,
Command: "mycommand",
Data: result,
}, func(out io.Writer) {
fmt.Fprintf(out, "Name: %s\n", result.Name)
fmt.Fprintf(out, "Version: %s\n", result.Version)
})
}
mytool mycommand β human-readable text. mytool mycommand --output json β Response envelope:
Markdown rendering¶
// Render AI output or release notes in the terminal
fmt.Print(output.RenderMarkdown(markdownContent))
// Or via Writer (no-op in JSON mode)
w.Render("## Changes\n\n- Added new flag\n- Fixed crash")
API Reference¶
Format¶
type Format string
const (
FormatText Format = "text" // Human-readable terminal output (default)
FormatJSON Format = "json" // Machine-readable JSON
FormatYAML Format = "yaml" // Machine-readable YAML
FormatCSV Format = "csv" // Comma-separated values
FormatMarkdown Format = "markdown" // Pipe-delimited markdown table
FormatTSV Format = "tsv" // Tab-separated values
)
Response¶
The standard envelope for all command JSON output.
type Response struct {
Status string `json:"status"`
Command string `json:"command"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
| Field | Values | Purpose |
|---|---|---|
Status |
"success", "error", "warning" |
Quick outcome check |
Command |
e.g. "version", "update" |
Which command produced the output |
Data |
any JSON-serialisable value | Command-specific payload |
Error |
error message string | Populated when Status is "error" |
Status constants:
Emit¶
Writes a Response to cmd.OutOrStdout() when --output json is set. No-op for text mode.
return output.Emit(cmd, output.Response{
Status: output.StatusSuccess,
Command: "deploy",
Data: map[string]any{"environment": "production", "version": "v2.1.0"},
})
Returns an error only if JSON serialisation or writing fails β not for text mode.
IsJSONOutput¶
Returns true when the --output flag is set to "json".
Use this to skip text-only work (spinner animations, table headers, progress bars) when JSON output is requested:
EmitError¶
Builds an error Response and emits it. No-op in text mode.
if err := doWork(); err != nil {
if emitErr := output.EmitError(cmd, "mycommand", err); emitErr != nil {
return emitErr
}
// Log or handle for text mode
return err
}
Writer¶
// NewWriter creates an output writer for the given io.Writer and format.
func NewWriter(w io.Writer, format Format) *Writer
// Write outputs data in the configured format.
// JSON mode: marshals data to indented JSON and writes it.
// Text mode: calls textFunc with the underlying writer.
func (o *Writer) Write(data any, textFunc func(io.Writer)) error
// Render writes glamour-styled markdown in text mode.
// In JSON mode it is a no-op β use Write for JSON output.
func (o *Writer) Render(markdown string) error
// IsJSON returns true when the writer is in JSON mode.
func (o *Writer) IsJSON() bool
Note: Pass the Response struct as the data argument to Write when you want the JSON envelope. Pass a plain struct if you have a specific reason to bypass the envelope (e.g. low-level data APIs).
RenderMarkdown¶
Renders markdown to styled ANSI terminal output via glamour. Detects terminal width automatically; falls back to 80 columns. If glamour fails for any reason, returns the original string unchanged β no error is surfaced.
releaseNotes := "## v1.2.0\n\n- Added feature X\n- Fixed bug Y"
fmt.Print(output.RenderMarkdown(releaseNotes))
TableWriter¶
Renders structured data as an aligned text table, JSON, YAML, or CSV. Columns are derived from table struct tags or defined explicitly via WithColumns.
func NewTableWriter(w io.Writer, format Format, opts ...TableOption) *TableWriter
func (t *TableWriter) WriteRows(rows any) error
Struct tag convention:
type ServiceStatus struct {
Name string `json:"name" table:"NAME,sortable"`
Status string `json:"status" table:"STATUS"`
Port int `json:"port" table:"PORT,sortable"`
}
Tag format: table:"HEADER" or table:"HEADER,sortable". Use table:"-" to exclude a field.
Options:
| Option | Description |
|---|---|
WithColumns(cols ...Column) |
Explicit column definitions (overrides struct tags) |
WithSortBy(field string) |
Sort rows by column header (must be sortable) |
WithSortDescending() |
Reverse sort order |
WithNoHeader() |
Suppress header row in text output |
WithNoTruncation() |
Disable terminal-width truncation |
WithMaxWidth(width int) |
Override automatic terminal width detection |
Column struct:
type Column struct {
Header string // Display name in header row
Field string // Struct field name or map key
Width int // Fixed width (0 = auto-size)
Sortable bool // Allow sorting by this column
Formatter func(any) string // Custom cell formatter
}
Format constants:
| Format | Output |
|---|---|
FormatText (default) |
Aligned, padded text table |
FormatJSON |
Indented JSON array |
FormatYAML |
YAML list |
FormatCSV |
CSV with header row |
FormatMarkdown |
Pipe-delimited markdown table with separator row |
FormatTSV |
Tab-separated values for shell pipelines (awk, cut, sort) |
Example:
services := []ServiceStatus{
{Name: "api", Status: "running", Port: 8080},
{Name: "worker", Status: "stopped", Port: 0},
}
format := output.Format(cmd.Flag("output").Value.String())
tw := output.NewTableWriter(cmd.OutOrStdout(), format,
output.WithSortBy("NAME"),
)
if err := tw.WriteRows(services); err != nil {
return err
}
// Text output:
// NAME STATUS PORT
// api running 8080
// worker stopped 0
// JSON output: [{"name":"api","status":"running","port":8080}, ...]
// YAML output:
// - name: api
// status: running
// port: 8080
// ...
// CSV output: NAME,STATUS,PORT\napi,running,8080\n...
// Markdown output:
// | NAME | STATUS | PORT |
// | ------ | ------- | ---- |
// | api | running | 8080 |
// | worker | stopped | 0 |
Map slices are supported with explicit columns:
rows := []map[string]any{
{"name": "alpha", "count": 10},
{"name": "beta", "count": 20},
}
tw := output.NewTableWriter(os.Stdout, output.FormatText,
output.WithColumns(
output.Column{Header: "NAME", Field: "name"},
output.Column{Header: "COUNT", Field: "count", Sortable: true},
),
)
tw.WriteRows(rows)
Usage Patterns¶
Pattern 1 β Writer with Response envelope (built-in command style)¶
Use this for commands that already use output.NewWriter. Pass Response as the data, so text mode renders your formatted output and JSON mode gets the envelope.
func runVersion(cmd *cobra.Command, p *props.Props) error {
format, _ := cmd.Flags().GetString("output")
w := output.NewWriter(os.Stdout, output.Format(format))
info := getVersionInfo(p)
return w.Write(output.Response{
Status: output.StatusSuccess,
Command: "version",
Data: info,
}, func(out io.Writer) {
fmt.Fprintf(out, "Version: %s\n", info.Version)
})
}
Pattern 2 β Emit for commands that don't use Writer¶
Use Emit when your command produces text output via the logger or fmt.Print and you just need to add a JSON path. The call is placed after all text work is done; it writes nothing in text mode.
func runDeploy(cmd *cobra.Command, p *props.Props) error {
p.Logger.Info("Deployingβ¦")
result, err := deploy()
if err != nil {
return err
}
p.Logger.Info("Deployed", "version", result.Version)
// JSON output only β text users see the logger output above
return output.Emit(cmd, output.Response{
Status: output.StatusSuccess,
Command: "deploy",
Data: result,
})
}
Pattern 3 β Markdown in text mode, JSON data in JSON mode¶
Use Writer.Render and Writer.Write together when a command produces rich markdown text output but structured data for JSON consumers.
func runChangelog(cmd *cobra.Command, p *props.Props) error {
format, _ := cmd.Flags().GetString("output")
w := output.NewWriter(os.Stdout, output.Format(format))
notes, meta := fetchChangelog()
// Render markdown when in text mode
if err := w.Render(notes); err != nil {
return err
}
// Emit structured JSON when in JSON mode
return output.Emit(cmd, output.Response{
Status: output.StatusSuccess,
Command: "changelog",
Data: meta,
})
}
Writer.Render is a no-op in JSON mode, so both calls are safe to make unconditionally.
Testing¶
Use bytes.Buffer as the writer and set it on the command to capture output:
func TestMyCommand_JSONOutput(t *testing.T) {
var buf bytes.Buffer
cmd := &cobra.Command{Use: "mycommand"}
cmd.Flags().String("output", "text", "output format")
_ = cmd.Flags().Set("output", "json")
cmd.SetOut(&buf)
err := runMyCommand(cmd, testProps)
require.NoError(t, err)
var resp output.Response
require.NoError(t, json.Unmarshal(buf.Bytes(), &resp))
assert.Equal(t, output.StatusSuccess, resp.Status)
assert.Equal(t, "mycommand", resp.Command)
}
func TestMyCommand_TextOutput(t *testing.T) {
var buf bytes.Buffer
cmd := &cobra.Command{Use: "mycommand"}
cmd.Flags().String("output", "text", "output format")
cmd.SetOut(&buf)
err := runMyCommand(cmd, testProps)
require.NoError(t, err)
// Verify text output β no JSON envelope
assert.Contains(t, buf.String(), "myapp")
assert.NotContains(t, buf.String(), `"status"`)
}
For RenderMarkdown, simply assert the output is non-empty β ANSI escape codes vary by terminal:
Related Documentation¶
- Add Scriptable JSON Output to a Command β step-by-step guide to adding
--output jsonsupport - Switch to Structured JSON Logging for Containers β complement to JSON output for daemon deployments
- Props β dependency injection container