cli

package module
v0.0.0-...-6c66404 Latest Latest
Warning

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

Go to latest
Published: Apr 7, 2026 License: Apache-2.0 Imports: 18 Imported by: 0

README

cli

A struct-driven CLI framework for Go with a Configure-Validate-Run lifecycle.

Designed as a simpler alternative to Cobra+Viper, with native integration with loader (config file loading with HuJSON + env var replacement) and await (goroutine lifecycle and graceful shutdown).

Quick Start

package main

import (
    "context"
    "fmt"
    "os"

    "github.com/runreveal/lib/cli"
)

type ServeCmd struct {
    Addr string `cli:"addr,a" usage:"listen address" default:":8080"`
}

func (s *ServeCmd) Run(ctx context.Context, args []string) error {
    fmt.Printf("serving on %s\n", s.Addr)
    return nil
}

func main() {
    app := cli.New("myapp", "My application")
    app.AddCommand(cli.Command("serve", "Start the server", &ServeCmd{}))
    os.Exit(app.Run(context.Background(), os.Args[1:]))
}

Features

Struct Tags

Command structs use tags to define flags and config bindings:

type ServeCmd struct {
    Addr    string        `cli:"addr,a"    usage:"listen address"  default:":8080"`
    Timeout time.Duration `cli:"timeout,t" usage:"request timeout" default:"30s"`
    DB      DBConfig      `config:"database"` // loaded from config file
}
Tag Purpose
cli:"name,alias" Flag name and optional single-char alias
cli:"-" Skip this field
usage:"text" Help text
default:"value" Default value (parsed to field type)
config:"key" JSON path in config file to unmarshal into this field

Supported types: string, bool, int, int64, uint, uint64, float64, time.Duration, []string, pointer variants, and encoding.TextUnmarshaler.

Global Flags

Define shared flags once at the app level instead of embedding in every command:

type Globals struct {
    Verbose bool   `cli:"verbose,v" usage:"enable verbose output"`
    Config  string `cli:"config,c"  usage:"config file" default:"config.json"`
}

globals := &Globals{}
app := cli.New("myapp", "desc",
    cli.WithGlobals(globals),
    cli.WithConfigFlag("config"),
)

// Access in any handler:
func (s *ServeCmd) Run(ctx context.Context, args []string) error {
    g := cli.GlobalsFromContext[Globals](ctx)
    if g.Verbose { ... }
}
Configure-Validate-Run Lifecycle

Globals and handlers can implement optional lifecycle interfaces:

// Configurer is called after config loading to initialize resources.
type Configurer interface {
    Configure() error
}

// Validator is called after Configure to check readiness.
type Validator interface {
    Validate() error
}

The framework also calls io.Closer on globals after the command exits.

Lifecycle order:

  1. Parse flags (globals + handler)
  2. Load config file into struct fields
  3. Globals: Configure() -> Validate() -> defer Close()
  4. Handler: Configure() -> Validate()
  5. Middleware -> handler.Run(ctx, args)
  6. Globals Close()
Config File Loading

One config file for the whole app. Commands declare which sections they need using config:"key" struct tags:

type ServeCmd struct {
    DB DBConfig `config:"database"`
}

Config files are processed through loader.LoadConfig, which supports HuJSON (comments, trailing commas) and $ENV_VAR replacement in string values.

Precedence: explicit CLI flag > config file > default tag > zero value

Commands and Groups
app.AddCommand(
    // Command with handler
    cli.Command("serve", "Start the server", &ServeCmd{}),

    // Command with long-form help text shown by --help
    cli.Command("up", "Launch a forge pod", &UpCmd{},
        cli.WithLong(`Launch a new forge pod for the given branch.

If the branch doesn't exist, create it first. The pod runs Claude Code
in headless mode with the given prompt.`),
    ),

    // Command with handler AND subcommands
    cli.Command("admin", "Admin tools", &AdminCmd{},
        cli.Command("migrate", "Run migrations", &MigrateCmd{}),
    ),

    // Group (no handler, prints help when invoked directly)
    cli.Group("db", "Database commands",
        cli.WithLong("Manage database migrations and connections."),
        cli.Command("migrate", "Run migrations", &MigrateCmd{}),
        cli.Command("seed", "Seed data", &SeedCmd{}),
    ),
)

The short description is always used in parent command listings. The long text appears indented below the title line when the user runs myapp <command> --help:

myapp up - Launch a forge pod

  Launch a new forge pod for the given branch.

  If the branch doesn't exist, create it first. The pod runs Claude Code
  in headless mode with the given prompt.

Usage:
  myapp up [flags]

Flags:
  ...
Command Registry

Allow commands to register themselves from outside the main package, enabling build-tag-based inclusion of optional commands:

// cmd/myapp/debug/debug.go
//go:build debug

package debug

import (
    "context"
    "github.com/runreveal/lib/cli"
)

type DebugCmd struct{}

func (d *DebugCmd) Run(ctx context.Context, args []string) error { ... }

func init() {
    cli.Register(cli.Command("debug", "Debug tools", &DebugCmd{}))
}
// cmd/myapp/main.go
package main

import (
    "github.com/runreveal/lib/cli"
    _ "myapp/cmd/myapp/debug" // only included with -tags debug
)

func main() {
    app := cli.New("myapp", "My app")
    app.AddCommand(cli.Registered()...) // commands from init() calls
    app.AddCommand(                      // explicit commands
        cli.Command("serve", "...", &ServeCmd{}),
    )
    os.Exit(app.Run(context.Background(), os.Args[1:]))
}

Register is safe to call from init() and accumulates across multiple calls. Registered returns a copy of the registered nodes.

Middleware
app := cli.New("myapp", "desc",
    cli.WithMiddleware(func(ctx context.Context, info cli.CommandInfo, next func(context.Context) error) error {
        slog.Info("running", "command", info.Name)
        return next(ctx)
    }),
)
Args Validation
cli.Command("get", "Get a resource", &GetCmd{}, cli.WithArgs(cli.ExactArgs(1)))
cli.Command("run", "Run a task", &RunCmd{}, cli.WithArgs(cli.NoArgs))
cli.Command("ping", "Ping hosts", &PingCmd{}, cli.WithArgs(cli.MinArgs(1)))
Await Integration

For long-running services, use await in your handler's Run method:

func (s *ServeCmd) Run(ctx context.Context, args []string) error {
    server := &http.Server{Addr: s.Addr, Handler: mux}

    w := await.New(await.WithSignals)
    w.AddNamed(await.ListenAndServe(server), "http")
    return w.Run(ctx)
}

For multiple services under one process:

func (d *DaemonCmd) Run(ctx context.Context, args []string) error {
    w := await.New(await.WithSignals, await.WithStopTimeout(15*time.Second))
    w.AddNamed(await.ListenAndServe(apiServer), "api")
    w.AddNamed(await.ListenAndServe(metricServer), "metrics")
    return w.Run(ctx)
}
Shell Completion
# Generate completion script
eval "$(myapp completion bash)"   # or zsh, fish

Handlers can provide custom completions for positional args:

func (g *GetCmd) Complete(ctx context.Context, args []string) []cli.Completion {
    return []cli.Completion{
        {Value: "pods", Description: "list pods"},
        {Value: "services", Description: "list services"},
    }
}
Default Config

Ship a reference config with your binary using go:embed:

//go:embed config.json
var defaultConfig []byte

app := cli.New("myapp", "desc",
    cli.WithDefaultConfig(defaultConfig),
)
myapp defcon > config.json   # dump the default config

The command name is defcon by default, overridable with WithDefaultConfigCommand.

Built-in Flags
Flag Behavior
-h, --help Print help for the app or command
--version Print version (requires WithVersion)

See Also

  • await — goroutine lifecycle management
  • loader — polymorphic config loading
  • cli/example — complete working example

Documentation

Overview

Package cli provides a simple, struct-driven CLI framework. It follows a Configure-Validate-Run lifecycle and integrates with github.com/runreveal/lib/loader for config file loading.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DumpConfig

func DumpConfig(handler Runnable) map[string]any

DumpConfig returns the resolved configuration of handler as a map of flag name → current field value. It reflects directly over the handler struct, so it captures values set by both CLI flags and config files.

func GlobalsFromContext

func GlobalsFromContext[T any](ctx context.Context) *T

GlobalsFromContext retrieves the globals pointer from context, cast to *T. Returns nil if no globals were registered or the type doesn't match.

func IsSet

func IsSet(ctx context.Context, flagName string) bool

IsSet reports whether a flag was explicitly set on the command line. Must be called from within Run to return meaningful results.

func NoArgs

func NoArgs(args []string) error

NoArgs returns an error if any positional args are present.

func Register

func Register(nodes ...Node)

Register appends nodes to the package-level command registry. Intended for use in init() functions to enable build-tag-based inclusion of optional commands.

Types

type App

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

App is the top-level CLI application.

func New

func New(name, desc string, opts ...AppOption) *App

New creates a new App.

func (*App) AddCommand

func (a *App) AddCommand(nodes ...Node)

AddCommand adds top-level command nodes to the app.

func (*App) Run

func (a *App) Run(ctx context.Context, args []string) (exitCode int)

Run executes the CLI with the given args (typically os.Args[1:]). Returns an exit code.

type AppOption

type AppOption func(*App)

AppOption configures an App.

func WithConfigFlag

func WithConfigFlag(flagName string) AppOption

WithConfigFlag sets which flag name holds the config file path.

func WithDefaultConfig

func WithDefaultConfig(data []byte) AppOption

WithDefaultConfig registers a default configuration that can be printed with "myapp defcon". Typically used with go:embed to ship a reference config alongside the binary. The command name defaults to "defcon" but can be overridden with WithDefaultConfigCommand.

func WithDefaultConfigCommand

func WithDefaultConfigCommand(name string) AppOption

WithDefaultConfigCommand overrides the command name used to print the default config (default: "defcon").

func WithGlobals

func WithGlobals(ptr any) AppOption

WithGlobals registers a struct pointer whose cli-tagged fields become flags available on every command. The pointer is stored in context and can be retrieved with GlobalsFromContext.

func WithMiddleware

func WithMiddleware(m Middleware) AppOption

WithMiddleware adds a middleware to the app.

func WithOutput

func WithOutput(w io.Writer) AppOption

WithOutput sets the writer for all output: help/errors (normally stderr) and completion/defcon (normally stdout). Useful for testing.

func WithVersion

func WithVersion(v string) AppOption

WithVersion sets the application version (enables --version flag).

type ArgsFunc

type ArgsFunc func(args []string) error

ArgsFunc validates positional arguments.

func ExactArgs

func ExactArgs(n int) ArgsFunc

ExactArgs returns an ArgsFunc that requires exactly n positional args.

func MinArgs

func MinArgs(n int) ArgsFunc

MinArgs returns an ArgsFunc that requires at least n positional args.

type CmdOption

type CmdOption func(*cmdOptions)

CmdOption configures a Command or Group node.

func WithArgs

func WithArgs(f ArgsFunc) CmdOption

WithArgs sets an args validation function on a command.

func WithLong

func WithLong(text string) CmdOption

WithLong sets long-form help text shown when the command is invoked with --help. The short description is still used in parent command listings.

type CommandInfo

type CommandInfo struct {
	Name string   // full command path, e.g. "admin migrate"
	Args []string // positional args after flag parsing
}

CommandInfo is passed to middleware.

type Completer

type Completer interface {
	Complete(ctx context.Context, args []string) []Completion
}

Completer is optionally implemented by command handlers to provide custom completions for positional arguments.

type Completion

type Completion struct {
	Value       string
	Description string
}

Completion represents a single shell completion suggestion.

type Configurer

type Configurer interface {
	Configure() error
}

Configurer is optionally implemented by globals or handler structs. Called after config file loading to initialize resources (e.g. open database connections, create clients).

type ExitError

type ExitError struct {
	Code int
	Err  error
}

ExitError carries a custom exit code.

func (*ExitError) Error

func (e *ExitError) Error() string

func (*ExitError) Unwrap

func (e *ExitError) Unwrap() error

type FlagSet

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

FlagSet is a parsed set of flag definitions.

func FlagSetFromContext

func FlagSetFromContext(ctx context.Context) *FlagSet

FlagSetFromContext returns the *FlagSet stored in ctx during command execution, or nil if called outside of a command handler.

func (*FlagSet) IsSet

func (fs *FlagSet) IsSet(name string) bool

IsSet reports whether the named flag was explicitly set.

func (*FlagSet) Parse

func (fs *FlagSet) Parse(args []string) ([]string, error)

Parse parses args, sets flag values, and returns remaining positional args.

type HelpExtra

type HelpExtra interface {
	ExtraHelp() string
}

HelpExtra is optionally implemented by command handlers to append additional information to help output. Useful for showing available loader types, config file schemas, or other context that the cli framework can't derive from struct tags alone.

type Middleware

type Middleware func(ctx context.Context, info CommandInfo, next func(context.Context) error) error

Middleware wraps command execution.

type Node

type Node interface {
	// contains filtered or unexported methods
}

Node is a node in the command tree.

func Command

func Command(name, desc string, handler Runnable, opts ...any) Node

Command creates a command node. Each element of opts may be a Node (child subcommand) or a CmdOption (behavioural option); they are distinguished by type at runtime.

func Group

func Group(name, desc string, opts ...any) Node

Group creates a group node that only prints help when invoked directly. Each element of opts may be a Node (child subcommand) or a CmdOption (e.g. WithLong); they are distinguished by type at runtime.

func Registered

func Registered() []Node

Registered returns a copy of all nodes added via Register. Call this when building the App to include registered commands.

type Runnable

type Runnable interface {
	Run(ctx context.Context, args []string) error
}

Runnable is the core interface every command handler must implement.

type Validator

type Validator interface {
	Validate() error
}

Validator is optionally implemented by globals or handler structs. Called after Configure to check that the fully-loaded config is valid.

Jump to

Keyboard shortcuts

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