Documentation
¶
Overview ¶
Package goclikit is what a cobra CLI's main calls instead of cobra.Command.Execute.
A bare cobra tree leaves four things to each program, and every program that answers them separately answers them differently. This package answers them once:
- Exit codes. Cobra returns a mistyped command line and a command that ran and failed as the same kind of error, so both exit 1. Execute classifies the first as ErrUsage and the caller selects 2.
- Alternatives. Cobra names the token it rejected and stops, without the flags it had just consulted. Execute appends the near matches. A command marked with AsNamespace refuses an unknown word with the near subcommands, and a namespace with a run function of its own returns UnknownCommand to say the same. A word naming a command further down the tree is answered with that command's full path.
- The next command. Cobra prints "Run '... --help' for usage" only where the resolved command has not silenced its own error output, which is a field set for unrelated reasons. Execute puts it in the error, so every tool prints it exactly once.
- Recovery from a missing resource. A tool that cannot list its corpus can still name the command that searches it. WithRecoveryHints records those commands and Execute attaches them, with the tool supplying a NotFoundFunc to say what a not-found looks like.
The update command is the fifth thing, and it is the reason github.com/datapointchris/goselfupdate is a dependency rather than a sibling: UpdateCommand returns the subcommand and Execute races the version check alongside whatever else was typed.
One import, one line ¶
func main() {
root := newRootCommand()
root.AddCommand(goclikit.UpdateCommand(cfg))
if err := goclikit.Execute(context.Background(), root, autoCfg); err != nil {
if !errors.Is(err, goclikit.ErrReported) {
fmt.Fprintln(os.Stderr, "error:", err)
}
if errors.Is(err, goclikit.ErrUsage) {
os.Exit(2)
}
os.Exit(1)
}
}
Nothing else in a consuming CLI needs this package. That is deliberate and it is the constraint every feature here is designed against: a tool that wants to answer a mistake its own way deletes two lines, and everything it wrote itself still compiles. A feature reaching out to call sites — an annotation helper spread across twenty files, a wrapper each command has to remember — would trade that away, so AsNamespace and WithRecoveryHints are offered for convenience and never required. A CLI may write either annotation itself.
The error constructors, UsageError and UnknownCommand, are the exception by design. Each is a value a command returns rather than a registration or a wrapper, and each carries something only the command knows: that its own check refused the line, and that the word it refused names no subcommand.
Index ¶
- Constants
- Variables
- func AsNamespace(cmd *cobra.Command) *cobra.Command
- func Execute(ctx context.Context, root *cobra.Command, config autoupdate.Config, ...) error
- func UnknownCommand(cmd *cobra.Command, word string) error
- func UpdateCommand(cfg goselfupdate.Config, options ...Options) *cobra.Command
- func UsageError(err error) error
- func WithRecoveryHints(cmd *cobra.Command, hints ...string) *cobra.Command
- type NotFoundFunc
- type Option
- type Options
Constants ¶
const NamespaceAnnotation = "goclikit.namespace"
NamespaceAnnotation is the cobra.Command annotation marking a command as a namespace: one that only groups subcommands, so any word left over once cobra has matched them is a subcommand that does not exist.
Its presence marks the command and its value is not read. AsNamespace writes "true". Exported so a CLI can write the annotation itself and keep this package out of its command files.
const RecoveryHintsAnnotation = "goclikit.recovery-hints"
RecoveryHintsAnnotation is the cobra.Command annotation carrying the commands a not-found under it should name, one per line.
Exported so a CLI can write the annotation itself and keep this package out of its command files. WithRecoveryHints is the same thing with the join done for you.
Variables ¶
var ErrReported = errors.New("already reported")
ErrReported marks an error as already written to stderr. It wraps every error UpdateCommand's command returns, and the refusal Execute returns after printing the help of a command given none of its arguments. A program whose main prints the error returned by Execute should skip anything matching this, otherwise the failure is reported twice:
if err := root.Execute(); err != nil {
if !errors.Is(err, goclikit.ErrReported) {
fmt.Fprintln(os.Stderr, "error:", err)
}
os.Exit(1)
}
Cobra's own error printing is already suppressed on the command.
var ErrUsage = errors.New("usage error")
ErrUsage marks a failure caused by how the command was typed rather than by the command running and failing: an unknown or malformed flag, an unknown subcommand, or positional arguments the command's cobra.Command.Args validator refused. Select exit code 2 for it, as the shell convention and Python's argparse do, so a caller can tell "you typed it wrong" from "it ran and failed" -- only the former is worth retrying with different arguments.
Cobra reports all three as ordinary errors, which is what flattens every failure to exit 1 without this. The argument refusal is told apart by Execute wrapping the validator of the command it is about to run, never by matching message text, which callers must never have to do.
Functions ¶
func AsNamespace ¶ added in v0.3.0
AsNamespace marks cmd as a namespace, and returns cmd so it can be attached inline in an AddCommand list.
Execute then answers a marked command line three ways:
- Bare, it prints the namespace's help and succeeds.
- With a word naming no subcommand, it refuses the word with UnknownCommand, marked ErrUsage.
- With that word followed by a flag the namespace does not declare, or by --help, it refuses the word as well. Cobra parses flags and answers --help before it validates arguments, so it would report the flag or print the namespace's help, and never mention the word.
The first two need a run function. AsNamespace gives cmd one where it has none of its own, so a tree driven by cobra's own Execute answers them too, as a test building the tree directly does. Execute gives one to a namespace whose annotation a CLI writes itself. The third needs Execute.
Annotations are not inherited, so each namespace in a tree is marked, the root included where it is one.
func Execute ¶
func Execute(ctx context.Context, root *cobra.Command, config autoupdate.Config, options ...Option) error
Execute runs root with an update check racing alongside it.
The check starts before the command and the notice prints after it, so the line is not buried in the command's output. A command that outlasts the check pays nothing for it, and one that finishes first waits for it, bounded by autoupdate.Config.Timeout. This is gh's shape, and it is the reason there is no blocking mode.
A command line cobra rejects comes back classified as ErrUsage and carrying the near-matching flags and the command that lists the valid ones, which cobra reports neither of.
func main() {
if err := goclikit.Execute(context.Background(), rootCmd, autoConfig()); err != nil {
if !errors.Is(err, goclikit.ErrReported) {
fmt.Fprintln(os.Stderr, err)
}
if errors.Is(err, goclikit.ErrUsage) {
os.Exit(2)
}
os.Exit(1)
}
}
A command marked with AsNamespace refuses a word naming none of its subcommands, whether or not a flag follows the word.
A not-found comes back carrying the commands that find a real id, for a caller that supplied WithNotFound and annotated its resource commands with WithRecoveryHints. Without the option nothing about an error changes.
Deliberately not a PersistentPreRun: cobra runs only the *closest* PersistentPreRunE in the ancestry, so a hook here would work for a root that has none and silently do nothing for one that does.
func UnknownCommand ¶ added in v0.2.0
UnknownCommand is the error for a word that names none of cmd's subcommands: the word, the subcommands near it, the full path of any command deeper under cmd that it names exactly, cmd's --version flag where the word is "version", and the command that lists every one, marked ErrUsage.
Cobra's unknown-command suggestion comes only from its root validator, which runs where the root declares no Args. A command marked with AsNamespace gets this refusal from Execute. A namespace with a run function of its own has to say the word is unknown itself, and this is that sentence with the alternatives and the pointer in it, worded the way cobra words both.
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
return printSummary(cmd)
}
return goclikit.UnknownCommand(cmd, args[0])
},
The alternatives are in the error rather than added by Execute afterwards, because only the namespace knows the word was refused for naming nothing. Read off the error's presence instead, a flag mistake or an argument-count failure on the same line would be answered as a command it never was.
func UpdateCommand ¶
func UpdateCommand(cfg goselfupdate.Config, options ...Options) *cobra.Command
UpdateCommand returns an update command for cfg.
func UsageError ¶
UsageError marks err as ErrUsage so Execute's caller selects exit code 2 for it, leaving the message alone.
For the mistakes this package cannot detect on its own: a required-flag or mutually-exclusive-flag rule a command validates in RunE. An cobra.Command.Args validator needs no mark, because Execute classifies whatever it refuses. A validator marks its refusal only to keep its own wording, which then reaches the caller unchanged.
RunE: func(cmd *cobra.Command, args []string) error {
if all && len(names) > 0 {
return goclikit.UsageError(errors.New("--all takes no names"))
}
...
},
Without this a consumer has to declare its own marker type to say the same thing, which is how four CLIs here ended up with four copies of it.
func WithRecoveryHints ¶
WithRecoveryHints records on cmd the commands a not-found under it should name, and returns cmd so it can be attached inline in an AddCommand list.
Each hint is a sentence, a colon, and the command to run.
Cobra does not inherit annotations and the lookup takes the nearest ancestor carrying them, so a subcommand acting on a second kind of id names every way in: a verb taking both an item and one of its tasks lists both.
Types ¶
type NotFoundFunc ¶
NotFoundFunc reports whether err is this tool's not-found, and the line naming what was missing.
Two answers from one call because they come from the same place. Cobra cannot tell a missing resource from a timeout, and the subject is the tool's too: only the server knows which of the two ids in `tool items untag 8 4` was the one that was absent, so a subject composed here from the arguments would name the wrong number half the time.
Returning an empty subject is the same as returning false. A tool whose not-found sometimes carries no detail — a proxy error page, an auth redirect decoding to a status and nothing else — reports it that way, and the error is left alone rather than rewritten into a resource claim nothing established.
type Option ¶
type Option func(*settings)
Option adjusts what Execute does beyond running the command tree.
Variadic rather than fields on a struct parameter, so a CLI wanting none of it calls Execute with three arguments and a feature added here never reaches the ten call sites that did not ask for it.
func WithNotFound ¶
func WithNotFound(classify NotFoundFunc) Option
WithNotFound tells Execute how to recognize this tool's not-found, which is the one thing it cannot work out for itself.
A tool backed by an HTTP API branches on its status code; one backed by a local store branches on its own sentinel:
goclikit.WithNotFound(func(err error) (string, bool) {
var apiErr *api.APIError
if !errors.As(err, &apiErr) || !apiErr.NotFound() {
return "", false
}
return apiErr.Message, true
})
Without it a not-found is an ordinary error and the recovery annotations are never read.
type Options ¶
type Options struct {
// Use overrides the command name. Defaults to "update".
//
// There is no alias option, so `upgrade` never resolves beside the one
// self-update verb and a reader never guesses which is real.
Use string
// SkipChangelog stops a successful update listing the commits between the
// two versions. The list costs one extra request, and is skipped silently
// when the source cannot produce one.
SkipChangelog bool
}
Options adjusts the generated command.