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.
- 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. [WithNotFoundHints] 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 imports 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 [WithNotFoundHints] is offered for convenience and never required. A CLI may write the annotation itself.
Index ¶
- Constants
- Variables
- func Execute(ctx context.Context, root *cobra.Command, config autoupdate.Config, ...) 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 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 wraps every error UpdateCommand's command returns, marking it as already written to stderr. 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, or an unknown subcommand. 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 both as ordinary errors, which is what flattens every failure to exit 1 without this.
Argument-count and custom cobra.Command.Args validation failures are not classified: cobra returns them as plain errors indistinguishable from a RunE failure without matching on message text, which callers must never have to do.
Functions ¶
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 a fast command pays nothing and the line is not buried in the command's output. 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 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 UpdateCommand ¶
func UpdateCommand(cfg goselfupdate.Config, options ...Options) *cobra.Command
New 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: an argument-count or custom cobra.Command.Args failure, which cobra returns indistinguishably from a RunE failure, and a required-flag or mutually-exclusive-flag rule a command validates itself.
Args: func(cmd *cobra.Command, args []string) error {
if len(args) > 1 {
return goclikit.UsageError(fmt.Errorf("unknown command %q", args[0]))
}
return nil
},
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 deliberately no alias knob. `update` is the fleet's one
// self-update verb, and an alias is what let `upgrade` coexist with it
// across every CLI without anyone choosing it.
Use string
// Changelog prints the commits between the two versions after a
// successful update. Enabled by default; it costs one extra request and is
// skipped silently when the source cannot produce one.
SkipChangelog bool
}
Options adjusts the generated command.