DocsCLIDefining commands

Defining commands

Embed BaseCommand, implement Run, build the App, and dispatch os.Args.

Every command follows the same shape. A config struct declares the inputs, a command struct embeds BaseCommand[ThatConfig] and implements Run, and an App holds the commands and dispatches to the one that matches.

The config struct

The config type is a plain struct whose fields, tagged with arg, short, env, default, help, and rules, declare everything the framework parses and validates. It carries no methods and no framework types.

type ServeConfig struct {
	Addr  string `arg:"addr" short:"a" env:"ADDR" default:":8080" help:"Listen address"`
	Debug bool   `arg:"debug" help:"Enable debug logging"`
}

Each tag is covered in Args and flags. The type parameter on BaseCommand binds this struct to its command.

The command struct

A command embeds BaseCommand[T] and implements Run and Help. The embedding provides name management, subcommand registration, validation, and no-op defaults for the richer help providers, so Run and Help are the methods you write.

type ServeCommand struct {
	cli.BaseCommand[ServeConfig]
}

func (c *ServeCommand) Run(globals cli.GlobalFlags, unknowns cli.Unknowns) error {
	fmt.Printf("listening on %s (debug=%v)\n", c.Inputs.Addr, c.Inputs.Debug)
	return nil
}

func (c *ServeCommand) Help() string { return "Run the HTTP server" }

The parsed and merged config is available as c.Inputs, a *ServeConfig. Run receives the built-in global flags and any arguments that matched no field, as cli.Unknowns. Help returns the one-line summary shown in listings, the one help method every command sets.

Note

Point the command's receiver at a pointer (*ServeCommand) so Run and the help methods attach to the same value the framework holds. BaseCommand fills c.Inputs before Run is called.

Building and running the app

NewApp takes the app identity (name and version) and the default values for the global flags. Register commands with Add, then hand os.Args[1:] to Run.

func main() {
	app := cli.NewApp(
		cli.Config{Name: "srv", Version: "1.0.0"},
		cli.GlobalFlags{},
	)
	app.Add("serve", &ServeCommand{BaseCommand: cli.NewBaseCommand[ServeConfig]()})

	if err := app.Run(os.Args[1:]); cli.IsRealError(err) {
		fmt.Fprintf(os.Stderr, "error: %v\n", err)
		os.Exit(1)
	}
}

NewBaseCommand[ServeConfig]() builds the embedded base with an initialized subcommand slice. Add registers the command under a name and returns it, which is what lets subcommand trees chain.

Handling the result

Run on the app returns the error from the matched command, or one of the clean-exit sentinels once the framework has printed help or the version itself. IsRealError returns false for nil and for those sentinels, so the call site surfaces only genuine failures.

if err := app.Run(os.Args[1:]); cli.IsRealError(err) {
	fmt.Fprintf(os.Stderr, "error: %v\n", err)
	os.Exit(1)
}