cli

package
v0.0.0-...-23883aa Latest Latest
Warning

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

Go to latest
Published: Oct 22, 2025 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package cli provides an opinionated package for how a CLI with sub-commands can be structured.

There are a few reasonable (IMHO) policies for how this operates.

  • User-visible output should go to STDERR by default. This is supported with a configurable Printer.
  • This package uses pflag for posix style flags.
  • Flags should NOT be interspersed by default. This makes flag and argument parsing much more consistent and predictable, but can be overridden.
  • Global flags are often confusing and not necessary. Flags apply to the command at hand, while global state may be configured through other means.
  • Sub-command aliases are often very convenient, so they're supported as additional, optional parameters to CommandSet.AddCommand.

Invocation

Invoking a CLI with sub-commands can always follow this form:

CLI_NAME [SUB-COMMAND...] [FLAGS...] [ARGS...]

This consistency helps to build muscle memory for frequent CLI use, and a predictable user experience. Just calling CLI_NAME will print usage information for the tool.

Usage by default

Usage information can be incredibly helpful for understanding a tool's purpose and expectations. That's why the '-h' and '--help' flags are set up by default, with input from the developer with the Command.Usage method.

Flag usage and sub-command usage is included in a usage template along with developer-provided usage information.

To display usage information from the root CommandSet's perspective, use CommandSet.RespondUsage. This method will return true if the user requested root command usage.

NOTE: Commands will NOT respond with usage by default if an error is returned.

Prioritizing Dev UX

Developers want nice things too, especially with tooling they rely on. This is the motivation for interactive mode.

If your CLI calls CommandSet.RespondInteractive, then you're enabling the use of the InteractiveFlag (which can be changed) to enter this mode. This method will block for interactions and return true if the user requested interactive mode.

If you want to work with a nested sub-command the UseCommand can be used to push that string of sub-commands to an invocation stack. Use the BackCommand to pop the invocation stack and go back to where you were.

To exit interactive mode, use one of the InteractiveQuitCommands at the prompt.

For more robust interactivity, I can recommend tview as a great tool for full TUI support. It's easy to use, and quick to get productive. I haven't tried many alternatives because this works well for me. YMMV.

Index

Examples

Constants

View Source
const (
	UseCommand  = "$use"  // This is used in interactive mode to indicate that a set of sub-commands should be pushed to the invocation stack.
	BackCommand = "$back" // This is used in interactive mode to indicate that the last element on the invocation stack should be popped.
)

Variables

View Source
var (
	ErrUnknownCommand = errors.New("unknown command")
	HelpPatterns      = []string{"--help", "-h"} // HelpPatterns is a slice of flags that should trigger the output of usage information with the top-level [CommandSet].

)
View Source
var (
	InteractiveFlag         = "-i"                  // InteractiveFlag specifies the flag that the user should pass to trigger [CommandSet.RespondInteractive].
	InteractiveQuitCommands = []string{"quit", "x"} // InteractiveQuitCommands is a slice of strings that should escape from interactive mode.
)
View Source
var (
	ErrArgMap = errors.New("failed to map argument(s)")
)

Functions

func AddGlobalPreExec

func AddGlobalPreExec(fn PreExec)

AddGlobalPreExec registers a function that will be executed right before a Command runs. If an error is returned from a PreExec, then the Command will not be executed, and the error will be returned from Exec instead. Note that no PreExec commands will be executed for calling the top level CommandSet, since it just prints usage.

Passing a nil PreExec function to this function will panic.

func MapArgs

func MapArgs(args []string, minArgs int, targets ...*string) error

MapArgs is an easy way to map arguments to variables (targets), and require a certain amount. This will return an error if there are not enough args and/or targets to satisfy the amount required by minArgs. Targets elements should not be nil.

func MustGet

func MustGet[T any](val T, err error) T

MustGet is used with a [pflag.FlagSet] getter to panic if the flag is not defined, or is not the right type. The developer usually knows whether a get call will fail, so this function makes it easier to avoid global flag state.

func NewUsageError

func NewUsageError(format string, args ...any) error

NewUsageError is used to create a UsageError. The format and args parameters are passed to fmt.Errorf to create the underlying error.

Example
tlc := NewCommandSet("parent")
cmd := tlc.AddCommand("command", "test command")
cmd.Does(func(flags *flag.FlagSet, out *Printer) error {
	return NewUsageError("test usage error")
})
// Done for testing purposes
cmd.Printer().Redirect(os.Stdout)
// Error not handled for brevity
_ = tlc.Exec([]string{"command"})
Output:

usage error: test usage error

test command

USAGE:
parent command

FLAGS
  -h, --help   Prints this usage information

Types

type Command

type Command struct {
	CommandSet
	// contains filtered or unexported fields
}

Command is an executable function in a CLI. It should be linked to a CommandSet to establish a tree of commands available to the user.

func (*Command) CommandPath

func (c *Command) CommandPath() string

CommandPath returns the reference chain for this Command.

func (*Command) Does

func (c *Command) Does(commandFunc CommandFunc) *Command

Does specifies the CommandFunc that should be executed by this Command.

func (*Command) Exec

func (c *Command) Exec(args []string) error

Exec executes the command with given arguments, parsing flags.

func (*Command) Flags

func (c *Command) Flags() *flag.FlagSet

Flags returns the flag.FlagSet for this Command.

func (*Command) Parent

func (c *Command) Parent() string

Parent retrieves the parent Command name.

func (*Command) Usage

func (c *Command) Usage(format string, args ...any) *Command

Usage allows specifying a longer description of the Command that will be output when a HelpPatterns flag is passed.

The short description, flag usages, and sub-command usages will be appended to this description.

type CommandFunc

type CommandFunc = func(flags *flag.FlagSet, out *Printer) error

CommandFunc is a function that may be executed within a Command.

type CommandSet

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

CommandSet is a group of Command.

func NewCommandSet

func NewCommandSet(parent ...string) *CommandSet

NewCommandSet is used to set up a top level CommandSet as the root of a CLI's command structure.

Note: the parent(s) passed to this function will be used to populate sub-command usage information. So they should only contain the commands used to invoke this CommandSet.

Example
// The NewCommandSet function is called to get a top level command set.
// The string used should be the name used to invoke your CLI, but it could also be os.Args[0].
tlc := NewCommandSet("my-cli")

// Sub-commands can be added easily.
sub := tlc.AddCommand("sub-command", "Shows an example of a sub-command")

// The flags for a Command or CommandSet can be accessed to set up whatever flags are needed.
sub.Flags().Bool("do-something", false, "Makes the sub-command do something")

// Usage hints can be set with the Usage method. No need to mess with the usage function in flags.
// Parent command references will automatically be prepended to this string.
// In this case the actual usage string will be 'my-cli sub-command [FLAGS]'.
sub.Usage("sub-command [FLAGS]")
// Done for the example test.
sub.Printer().Redirect(os.Stdout)

// Functionality is defined with the Does method.
sub.Does(func(flags *flag.FlagSet, _ *Printer) error {
	// Flags are already parsed by the time this function is executed.
	if MustGet(flags.GetBool("do-something")) {
		// Using fmt for the example, but the Printer should be used to communicate with the user.
		fmt.Println("sub-command ran")
	}
	return nil
})

// os.Args[1:] should be passed to tlc.Exec
// Sub-commands will be matched case-insensitive.
if err := tlc.Exec([]string{"suB-ComMAnd", "--do-something"}); err != nil {
	fmt.Println("Something bad happened!")
}
fmt.Println()

// Help flags are automatically set up for each command.
_ = tlc.Exec([]string{"sub-command", "-h"})
Output:

sub-command ran

Shows an example of a sub-command

USAGE:
my-cli sub-command [FLAGS]

FLAGS
      --do-something   Makes the sub-command do something
  -h, --help           Prints this usage information

func (*CommandSet) AddCommand

func (s *CommandSet) AddCommand(key, shortUsage string, aliases ...string) *Command

AddCommand adds a sub-command to this CommandSet. The key parameter will be cleansed to remove spaces, and normalize to lower-case. Aliases may be added as a way to support shorter variants of the same Command.

func (*CommandSet) CommandUsages

func (s *CommandSet) CommandUsages() string

CommandUsages returns a string including the usage information for sub-commands in this CommandSet.

The sub-command keys will be sorted alphabetically before output.

func (*CommandSet) Exec

func (s *CommandSet) Exec(args []string) error

Exec executes this CommandSet. It's expected that the first 1+ arguments include the key/alias for a sub-command.

func (*CommandSet) Parent

func (s *CommandSet) Parent() string

Parent retrieves the parent CommandSet name.

func (*CommandSet) Printer

func (s *CommandSet) Printer() *Printer

Printer returns the cached Printer for this CommandSet.

func (*CommandSet) RespondInteractive

func (s *CommandSet) RespondInteractive() bool

RespondInteractive will launch an interactive "shell" version of the CommandSet if the InteractiveFlag is the first argument, indicating that the user is requesting interactive mode. This allows printing usage and calling sub-commands. Returns false if interactive mode was not requested by the user.

This loop may be interrupted with one of the InteractiveQuitCommands.

func (*CommandSet) RespondUsage

func (s *CommandSet) RespondUsage(format string, vals ...any) bool

RespondUsage will print usage information with the given Printer if one of HelpPatterns is given as the first argument. If usage information was printed, then true will be returned.

type PreExec

type PreExec func() error

PreExec is a function that may run before execution of a Command

type Printer

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

Printer is provided to easily establish policies for user messages. It exposes Print, Println, and Printf methods.

Printer writes to os.Stderr by default, but this can be overridden with Printer.Redirect.

func NewPrinter

func NewPrinter() *Printer

func (*Printer) Print

func (p *Printer) Print(msg ...any)

func (*Printer) Printf

func (p *Printer) Printf(format string, args ...any)

func (*Printer) Println

func (p *Printer) Println(msg ...any)

func (*Printer) Prompt

func (p *Printer) Prompt(msg string, args ...any) (string, error)

Prompt will prompt the user for input, then read and return the next line of text.

func (*Printer) PromptNoEcho

func (p *Printer) PromptNoEcho(msg string, args ...any) ([]byte, error)

PromptNoEcho will prompt the user for input, then read and return the next line of text without printing input to the terminal.

func (*Printer) Redirect

func (p *Printer) Redirect(writer io.Writer)

Redirect will make the Printer print to this output instead. Defaults to os.Stderr.

func (*Printer) RedirectInput

func (p *Printer) RedirectInput(in *os.File)

RedirectInput will make the Printer read from a different file when prompting.

type UsageError

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

UsageError is a special purpose error used to signal that usage information should be shown to the user. This is intended to be used as an error response for Command validation.

func (*UsageError) Error

func (e *UsageError) Error() string

func (*UsageError) Is

func (e *UsageError) Is(err error) bool

func (*UsageError) Unwrap

func (e *UsageError) Unwrap() error

Jump to

Keyboard shortcuts

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