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 ¶
- Constants
- Variables
- func AddGlobalPreExec(fn PreExec)
- func MapArgs(args []string, minArgs int, targets ...*string) error
- func MustGet[T any](val T, err error) T
- func NewUsageError(format string, args ...any) error
- type Command
- func (c *Command) AddCommand(name, summary string, aliases ...string) *Command
- func (c *Command) AddUsageExample(example string)
- func (c *Command) CommandPath() string
- func (c *Command) Does(commandFunc CommandFunc) *Command
- func (c *Command) Exec(args []string) error
- func (c *Command) Flags() *Flags
- func (c *Command) Printer() *Printer
- func (c *Command) RespondInteractive() bool
- func (c *Command) SetUsageProse(format string, args ...any) *Command
- type CommandFunc
- type Flags
- type PreExec
- type Printer
- func (p *Printer) Fatal(msg ...any)
- func (p *Printer) Fatalf(format string, args ...any)
- func (p *Printer) Fatalln(msg ...any)
- func (p *Printer) MustPrompt(msg string, args ...any) string
- func (p *Printer) MustPromptNoEcho(msg string, args ...any) []byte
- func (p *Printer) NextLine() (string, error)
- func (p *Printer) Print(msg ...any)
- func (p *Printer) Printf(format string, args ...any)
- func (p *Printer) Println(msg ...any)
- func (p *Printer) Prompt(msg string, args ...any) (string, error)
- func (p *Printer) PromptNoEcho(msg string, args ...any) ([]byte, error)
- func (p *Printer) Redirect(writer io.Writer)
- func (p *Printer) RedirectInput(in *os.File)
- type UsageError
Examples ¶
Constants ¶
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 ¶
var ( ErrorExitCode = 1 // Exit code when Command errors reported. UsageErrorExitCode = 2 // Exit code when a UsageError is reported. LongHelpFlag = "help" ShortHelpFlag = "h" FlagsErrorHandling = pflag.ContinueOnError FlagsInterspersed bool DefaultPrinterOutput = os.Stderr )
var ( InteractiveFlag = "-i" // InteractiveFlag specifies the flag that the user should pass to trigger [Command.RespondInteractive]. InteractiveQuitCommands = []string{"quit", "x"} // InteractiveQuitCommands is a slice of strings that should escape from interactive mode. )
var (
ErrArgMap = errors.New("failed to map argument(s)")
)
var ErrNoExecution = errors.New("no execution attached to this command")
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 ¶
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 ¶
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 ¶
NewUsageError is used to create a UsageError. The format and args parameters are passed to fmt.Errorf to create the underlying error.
Example ¶
testSettings()
tlc := TopLevelCommandWithName("parent")
cmd := tlc.AddCommand("another", "Another command!")
cmd.AddUsageExample("[FLAGS]")
cmd.Does(func(flags *Flags, out *Printer) error {
return NewUsageError("test usage error")
})
// Error not handled for brevity
_ = tlc.Exec([]string{"another"})
Output: usage error: test usage error Another command! USAGE: parent another [FLAGS] FLAGS: -h, --help Prints this usage information
Types ¶
type Command ¶
type Command struct {
// contains filtered or unexported fields
}
Command is an executable function in a CLI. It should be linked to a Command to establish a tree of commands available to the user.
func TopLevelCommand ¶
func TopLevelCommand() *Command
func TopLevelCommandWithName ¶
Example ¶
// The TopLevelCommand 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 := TopLevelCommandWithName("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 AddUsageExample 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.AddUsageExample("[FLAGS]")
// Done for the example test.
sub.Printer().Redirect(os.Stdout)
// Functionality is defined with the Does method.
sub.Does(func(flags *Flags, _ *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 (*Command) AddCommand ¶
AddCommand adds a sub-command to this Command. The name 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 (*Command) AddUsageExample ¶
AddUsageExample adds an example of how the Command can be invoked, using placeholders for arguments and flags.
Example:
FILE [MODE] [FLAGS]
Parent commands and this command name will be used to prefix examples.
func (*Command) CommandPath ¶
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) RespondInteractive ¶
RespondInteractive will launch an interactive "shell" version of the Command 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.
type CommandFunc ¶
CommandFunc is a function that may be executed within a Command.
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) MustPromptNoEcho ¶
func (*Printer) Prompt ¶
Prompt will prompt the user for input, then read and return the next line of text.
func (*Printer) PromptNoEcho ¶
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 ¶
Redirect will make the Printer print to this output instead. Defaults to os.Stderr.
func (*Printer) RedirectInput ¶
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