cli

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Apr 22, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package cli provides argument-dispatch glue for the ana CLI. It defines the Command interface, an IO struct carrying stdio/env/clock dependencies, and a Group helper that dispatches to named child Commands. The package is pure dispatch logic — it has no dependency on transport or config.

Index

Constants

This section is empty.

Variables

View Source
var ErrHelp = errors.New("help")

ErrHelp marks an explicit help request (`--help`, `-h`, or `help`). Dispatch returns this after printing help text. ExitCode maps it to 0 because the user asked for help and got it — that is success, not a usage error.

View Source
var ErrReported = errors.New("reported")

ErrReported marks errors whose diagnostic text has already been written to stderr by the callee. main()'s fallback stderr print skips these to avoid double-reporting. Wrap with errors.Join(err, ErrReported) after emitting the diagnostic yourself.

View Source
var ErrUsage = errors.New("usage")

ErrUsage marks a usage-related failure — missing arg, unknown command, malformed flag. Commands that want the root to exit with code 1 should return an error that wraps ErrUsage via %w.

Functions

func ApplyAncestorFlags added in v0.3.0

func ApplyAncestorFlags(ctx context.Context, fs *flag.FlagSet)

ApplyAncestorFlags runs every registered ancestor registrar on fs in the order they were appended (outermost first). Leaves call this AFTER declaring their own flags so leaf declarations populate fs first and each ancestor registrar can Lookup-guard its own additions — stdlib flag.FlagSet panics on duplicate declarations, and this ordering makes "leaf wins" fall out naturally.

Callers that build ancestor registrars should wrap StringVar/BoolVar in the DeclareString / DeclareBool helpers (or equivalent Lookup guards) so they're safe when the leaf declared the same name.

func DashIfEmpty

func DashIfEmpty(s string) string

DashIfEmpty renders an em-dash placeholder for empty cells so tabwriter keeps table columns aligned. Every verb's list/show table uses this fallback.

func DeclareBool added in v0.3.0

func DeclareBool(fs *flag.FlagSet, target *bool, name string, def bool, usage string)

DeclareBool is the bool counterpart to DeclareString. Same guard against panicking on duplicate names.

func DeclareInt added in v0.3.0

func DeclareInt(fs *flag.FlagSet, target *int, name string, def int, usage string)

DeclareInt is the int counterpart to DeclareString. Same guard against panicking on duplicate names — used by Group.Flags closures that want to inherit-declare an integer flag (e.g. the Databricks Group's shared `--port`) without clobbering a leaf that already declared the same name.

func DeclareString added in v0.3.0

func DeclareString(fs *flag.FlagSet, target *string, name, def, usage string)

DeclareString is a Lookup-guarded wrapper around fs.StringVar. Ancestor Group.Flags closures should use this (rather than raw StringVar) so a leaf that already declared the same name isn't clobbered by a duplicate declaration — the stdlib flag package panics in that case.

func Dispatch

func Dispatch(ctx context.Context, verbs map[string]Command, args []string, stdio IO) error

Dispatch is the root entry point. It parses global flags, stashes them in ctx, then routes to the matching verb. An explicit help token returns ErrHelp (exit 0); an empty verb or parse error returns ErrUsage (exit 1).

func EnumFlag

func EnumFlag(target *string, allowed []string) flag.Value

EnumFlag returns a flag.Value that validates against a fixed allow-list at parse time: unknown values yield a UsageErr before the verb body runs, so downstream code can trust *target. The allowed values show up in the Set error, which fs.Parse wraps as `invalid value "X" for flag -Y: allowed: ...`. Pairs naturally with RequireFlags when the flag is also mandatory.

func ExitCode

func ExitCode(err error) int

ExitCode maps a returned error to the process exit code.

nil         -> 0
ErrHelp     -> 0  (user asked for help; help was shown)
ErrUsage    -> 1  (including wrapped)
authError   -> 3  (including wrapped; IsAuthError must return true)
otherwise   -> 2

func FirstLine

func FirstLine(s string) string

FirstLine returns the first line of s (without the newline). Exported so verb packages that render streaming/multi-line payloads one-row-per-frame can reuse the same definition the help renderer uses.

func FlagWasSet

func FlagWasSet(fs *flag.FlagSet, name string) bool

FlagWasSet reports whether fs saw name as an explicit argument. Partial- update verbs use it to distinguish "user left this alone" (keep server's current value) from "user passed the zero value on purpose". Uses the stdlib-documented `fs.Visit` idiom, which only traverses flags that were actually set; an unknown name is reported as not-set rather than panicking.

func IntListFlag

func IntListFlag(target *[]int, sep string) flag.Value

IntListFlag returns a flag.Value that parses a separator-delimited list of ints into *target (whitespace around each entry is tolerated). Empty input and non-integer tokens produce a UsageErr; on success *target is guaranteed non-empty.

func IsHelpArg added in v0.3.0

func IsHelpArg(s string) bool

IsHelpArg reports whether s is one of the recognized help tokens.

func NewFlagSet

func NewFlagSet(name string) *flag.FlagSet

NewFlagSet returns a *flag.FlagSet configured the way every leaf verb wants it: ContinueOnError so we can wrap parse failures as usage errors, and output silenced (io.Discard) so each command's own Help() is the sole source of usage text. Currently duplicated in every verb package — Phases 1–10 of the shared-cli-kit refactor switch them over to this helper.

func NewTableWriter

func NewTableWriter(w io.Writer) *tabwriter.Writer

NewTableWriter returns a *tabwriter.Writer configured the way every verb's list/show table wants it: no min-width, no tab-stop, two-space padding. Callers must call Flush() (or defer it) once they finish writing rows.

func ParseFlags

func ParseFlags(fs *flag.FlagSet, args []string) error

ParseFlags parses args into fs, tolerating positional arguments interleaved with flags. Go's stdlib FlagSet.Parse stops at the first non-flag token, which silently drops any flags that follow — so `cmd <id> --flag v` would parse the positional but ignore --flag. This helper iterates: parse, collect a non-flag token, parse the remainder, repeat. A final Parse with a "--" separator then re-seeds fs.Args() with the collected positionals so callers can read them through the normal flag API.

On any underlying Parse failure the error is wrapped with ErrUsage so the root dispatcher maps it to exit code 1.

func ReadPassword

func ReadPassword(r io.Reader) (string, error)

ReadPassword reads a single password line from r and returns the bytes exactly as supplied, stripping only the trailing line terminator (\n or \r\n). Unlike ReadToken, no whitespace trimming is applied: a password may legitimately contain leading or trailing spaces or tabs, and silently mutating it would cause hard-to-diagnose auth failures. The same JWT-sized scanner buffer is used so very long credentials round-trip intact.

func ReadToken

func ReadToken(r io.Reader, tokenStdin bool) (string, error)

ReadToken consumes stdin and returns a trimmed token. With tokenStdin=true the whole stream is consumed; otherwise a single newline-terminated line is read. Whitespace is trimmed in both modes so common pipe quirks (trailing newline from `echo`) don't poison the saved value. Centralised here because auth login and profile add share this exact behaviour.

func RedactToken

func RedactToken(token string) string

RedactToken returns a user-facing display for a bearer token or API key. Empty tokens render as "(unset)" so operators can see the slot needs a login; any other value shows a fixed mask plus the last four characters so two tokens can be disambiguated at a glance without leaking them. Tokens shorter than four bytes (never expected from a real API key but exercised defensively) still get fully masked — echoing the whole value in the "last 4:" slot would defeat the redaction for short/malformed/test tokens.

func Remarshal

func Remarshal(src, dst any) error

Remarshal round-trips src through JSON into dst, letting commands have one Unary decode into map[string]any and still derive a typed view for table rendering without a second RPC.

func RenderOutput

func RenderOutput[T any](
	w io.Writer,
	raw map[string]any,
	jsonFlag bool,
	typed *T,
	render func(w io.Writer, typed *T) error,
) error

RenderOutput picks the JSON-vs-table branch every `--json` verb repeats. On jsonFlag the raw map is dumped via WriteJSON; otherwise raw is Remarshal'd into typed and handed to render. The remarshal error is wrapped with "decode response" so verbs preserve their historical error string under a `verb: %w` wrap at the call site.

func RenderTwoCol

func RenderTwoCol(w io.Writer, m map[string]any) error

RenderTwoCol prints top-level scalar fields then any nested map fields (e.g. postgresMetadata) as an indented sub-block. Keys are sorted so the output is stable across runs for snapshot-style tests. Output is byte-identical to the pre-refactor connector/get.go::renderTwoCol.

func RequireFlags

func RequireFlags(fs *flag.FlagSet, verb string, names ...string) error

RequireFlags returns a UsageErr listing any flag from names that was not explicitly set on fs. Built on FlagWasSet, so "explicit zero value" (e.g. --port 0) still counts as supplied — callers that care about the value's content validate it themselves. The verb prefix is prepended so the error reads `verb: missing required flags: --a, --b`.

func RequireIntID

func RequireIntID(verb string, args []string) (int, error)

RequireIntID extracts an integer positional <id> from args[0]. Missing or empty input returns the same usage error as RequireStringID; a non-numeric input returns a usage error that quotes the underlying strconv error. Behaviour matches the pre-refactor connector.atoiID with the addition of the args-slice indirection.

func RequireStringID

func RequireStringID(verb string, args []string) (string, error)

RequireStringID extracts a non-empty positional <id> from args[0]. An empty or whitespace-only first arg (or a missing arg entirely) is rejected with a usage error. The strictest pre-refactor variant (dashboard's strings.TrimSpace check) is used here so all verb packages converge on identical behaviour after migration.

func RootHelp

func RootHelp(w io.Writer, verbs map[string]Command)

RootHelp writes a sorted listing of the top-level verbs to w, each followed by the first line of its own Help(), then the canonical Global Flags block.

func SinceFlag

func SinceFlag(target *time.Time, now func() time.Time) flag.Value

SinceFlag returns a flag.Value that accepts either a non-negative duration (e.g. "1h", "24h") — interpreted as `now() - dur` — or an absolute RFC3339 timestamp, and stores the UTC-normalised result in *target. The injected now lets tests fix the clock so --since assertions are deterministic. Negative durations are rejected rather than silently producing a future timestamp, which would mask operator typos.

func UsageErrf

func UsageErrf(format string, a ...any) error

UsageErrf builds a user-facing error that wraps ErrUsage so the root dispatcher maps it to exit code 1 via errors.Is. Use this anywhere a verb detects a missing/invalid arg or other shape problem the caller could fix by re-running with different inputs.

func WithAncestorFlags added in v0.3.0

func WithAncestorFlags(ctx context.Context, reg func(*flag.FlagSet)) context.Context

WithAncestorFlags appends reg to the ctx-carried slice of ancestor flag registrars and returns the new context. Groups call this during Run so child commands inherit the Group's declared flags; the slice preserves registration order (outermost ancestor first) so leaf tests can reason about precedence by inserting guards.

Per stdlib context.WithValue contract, ctx must not be nil.

func WithGlobal

func WithGlobal(ctx context.Context, g Global) context.Context

WithGlobal returns a child context carrying g. Per stdlib convention ctx must be non-nil; a nil parent panics (mirroring context.WithValue).

func WriteJSON

func WriteJSON(w io.Writer, v any) error

WriteJSON pretty-prints v to w with a 2-space indent and trailing newline, matching the convention used across every --json branch in the CLI. A single helper keeps output byte-identical between verbs.

Types

type Command

type Command interface {
	Run(ctx context.Context, args []string, io IO) error
	Help() string
}

Command is the consumer interface implemented by every verb or subcommand. Run receives the args remaining after its own name has been consumed.

type Flagger added in v0.3.0

type Flagger interface {
	Flags(fs *flag.FlagSet)
}

Flagger is an optional opt-in for leaf commands whose help should include a flag enumeration that stacks ancestor-declared flags with the leaf's own. Leaves that implement Flags(fs) get an automatic Flags: block appended to their --help output by dispatchChild; leaves that don't implement it keep the current hand-written Help() as their sole source of usage text.

type Global

type Global struct {
	JSON      bool
	Endpoint  string
	TokenFile string
	Profile   string
}

Global holds the root-level flags that apply to every verb. Command implementations read it from context via GlobalFrom; ParseGlobal produces it from raw argv.

func GlobalFrom

func GlobalFrom(ctx context.Context) Global

GlobalFrom extracts the Global from ctx, or a zero value if absent. Per stdlib convention ctx must be non-nil; a nil ctx panics (mirroring context.Value semantics).

func ParseGlobal

func ParseGlobal(args []string) (Global, []string, error)

ParseGlobal parses the global flags at the front of args and returns the resulting Global along with the remaining args (the verb and its args). Parsing stops at the first positional argument or `--`; subcommand flags are left to the subcommand itself.

func StripGlobals added in v0.3.0

func StripGlobals(args []string) (Global, []string, error)

StripGlobals walks args once and splits it into (Global, rest, err). Unlike ParseGlobal, which relies on stdlib flag.FlagSet.Parse and stops at the first positional, StripGlobals consumes known global flags wherever they appear — before, after, or interleaved with the verb path and leaf flags. Everything it does not recognise is passed through to rest in original order so the leaf's FlagSet still handles unknown-flag errors.

Supported forms per known flag in globalFlagRegistry (single- and double- dash spellings are equivalent, matching stdlib `flag.FlagSet.Parse`):

  • `-name` / `--name` (bool) or `-name=value` / `--name=value` / `-name value` / `--name value` (takesValue)

A bare `--` terminator stops global consumption: every remaining token is copied verbatim to rest (including the `--` itself), so leaves can still use `--` to force positional interpretation of an arg that looks like a flag.

Duplicate globals follow stdlib semantics (last wins). Unknown flags are left in rest unchanged so the leaf's own FlagSet reports a precise `flag provided but not defined: --xyz` at its verb name.

type Group

type Group struct {
	Summary  string
	Flags    func(*flag.FlagSet)
	Children map[string]Command
}

Group is a Command that dispatches its first argument to a named child Command. A Group can itself be registered as a child, enabling nested verbs (e.g. `ana chat send ...`).

Flags, if set, declares group-level flags that every descendant leaf inherits via the ctx-carried registrar stack (see WithAncestorFlags in root.go). The closure runs on a leaf's *flag.FlagSet AFTER the leaf has declared its own flags, so use DeclareString / DeclareBool (or an equivalent Lookup guard) to avoid the stdlib flag-redeclaration panic — the guard lets the leaf override a name when it wants to.

func (*Group) Help

func (g *Group) Help() string

Help renders the group's summary (if set) followed by a sorted, two-column listing of child commands and the first line of each child's own Help(). When Flags is set, a trailing "Flags:" block enumerates the group-level flags so `ana <group> --help` surfaces inheritable flags even when the user hasn't drilled into a leaf.

func (*Group) Run

func (g *Group) Run(ctx context.Context, args []string, stdio IO) error

Run dispatches to a child command. With no args or an explicit help flag it prints Help() to stdout and returns ErrHelp (exit 0). An unknown child name writes to stderr and returns ErrUsage (exit 1).

If Flags is non-nil, Run appends it to the ctx-carried ancestor-flag stack before delegating so every descendant leaf can ApplyAncestorFlags and pick up the group's declared flags.

type IO

type IO struct {
	Stdin  io.Reader
	Stdout io.Writer
	Stderr io.Writer
	Env    func(string) string
	Now    func() time.Time
}

IO carries the ambient dependencies a Command needs: standard streams, an environment accessor, and a clock. Pass this through to subcommands rather than reaching for package globals so tests can inject fakes.

func DefaultIO

func DefaultIO() IO

DefaultIO returns an IO backed by os.Stdin/Stdout/Stderr, os.Getenv, and time.Now.

type Token

type Token string

Token wraps a bearer token so any accidental `%s`/`%v`/`%q` on a logger or error renders the redacted form (same mask RedactToken produces). Code that needs the raw value calls Value() — the single explicit escape hatch.

The underlying kind is string, so the type is JSON-transparent (persists and loads as a plain string), comparisons against string literals still work, and tests can write `cli.Token("abcdefgh")` with no extra plumbing.

func (Token) Format

func (t Token) Format(f fmt.State, _ rune)

Format also intercepts `%q`, `%+v`, and `%#v`, which would otherwise bypass String() and dump the raw bytes (fmt special-cases string kinds for %q/%#v specifically). Returning the same redacted form for every verb means no format directive can accidentally leak a token.

func (Token) String

func (t Token) String() string

String returns the redacted representation. Triggered by any verb that doesn't override it (`%s`, `%v`, default Sprintln, error chains, etc.).

func (Token) Value

func (t Token) Value() string

Value returns the raw token string. This is the intended escape hatch for the two legitimate consumers: the transport layer building an Authorization header, and the auth-keys verb that prints a freshly minted key once. Adding new callers is a design smell — prefer passing the Token through.

Jump to

Keyboard shortcuts

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