❯ go-clikit
v0.10.0 pkg.go.dev ↗

guides › make a command tree strict

Make a command tree strict

Make a cobra tree refuse a misspelt command, word the errors of the command line, and give errors exit codes.

cobra is lenient where a script needs it strict. A command that only holds subcommands prints its help and succeeds for a word that names none of them, so a script with a misspelt command goes on to its next step. cobratree.Strict fixes this once the tree is built.

Procedure#

  1. Build the tree. Add the last command.
  2. Call Strict on the root. Give it cobratree.Moved for the commands that you renamed, and cobratree.Topics for the help topics that are no command.
  3. Set cobratree.FlagError as the FlagErrorFunc of the root.
  4. In a command, refuse a value with cobratree.InvalidValue or cobratree.Usagef.
  5. Where the cause of a failure is known, give its exit status with cobratree.Coded.
  6. In main, map the error to the exit status: cobratree.IsUsage first, commonly 2, then cobratree.Status.
root := &cobra.Command{Use: "prog", SilenceErrors: true, SilenceUsage: true}
get := &cobra.Command{Use: "get", Short: "Show things"}
get.AddCommand(&cobra.Command{
	Use:  "cluster [NAME]",
	Args: cobra.MaximumNArgs(1),
	RunE: func(*cobra.Command, []string) error { return nil },
})
root.AddCommand(get)
cobratree.Strict(root, cobratree.Moved(map[string]string{"status": "get cluster"}))

for _, args := range [][]string{
	{"gte"},
	{"status"},
	{"help", "get", "clustr"},
	{"get", "cluster", "a", "b"},
	{"get", "cluster", "--bogus"},
} {
	root.SetArgs(args)
	err := root.Execute()
	code := 1
	if cobratree.IsUsage(err) {
		code = 2
	}
	fmt.Printf("%d %v\n", code, err)
}
2 unknown command "gte" for "prog" (did you mean "get"?)
2 unknown command "status" for "prog" (it is now "get cluster")
2 unknown help topic "get clustr": unknown command "clustr" for "prog get" (did you mean "cluster"?)
2 accepts at most 1 arg(s), received 2 (usage: prog get cluster [NAME])
2 unknown flag: --bogus
ExampleStrict in cobratree/example_test.go. CI runs it and checks its output.

What the user sees:

prog gte          unknown command "gte" for "prog" (did you mean "get"?)
prog status       unknown command "status" for "prog" (it is now "get cluster")
prog power on     requires at least 1 arg(s), only received 0 (usage: prog power on NODES)

The texts of these errors are an interface (decisions 24 and 29).

The version#

cobratree.ReadBuildInfo returns what the toolchain recorded: the module version, the commit and its time, and if the checkout was modified. cobratree.BuildInfo.Resolve puts in place what a release build set with -ldflags.

var version, commit string // set with -ldflags "-X main.version=v1.2.3 -X main.commit=…"
bi := cobratree.ReadBuildInfo().Resolve(version, commit)
line := "prog " + cmp.Or(bi.Version, "dev")
if rev := bi.Short(7); rev != "" {
	line += " (" + rev + ")"
}
fmt.Println(line)
ExampleBuildInfo_Resolve in cobratree/example_test.go. CI compiles it.

Test the tree#

cobratreetest.Run runs a command line in a test and keeps the result apart from the diagnostics. cobratreetest.CheckCommandLines checks that each command line of the manual names a command and flags that exist.

_ = func(t *testing.T) {
	stdout, stderr, err := cobratreetest.Run(t, newTree(), "get", "cluster")
	if err != nil || stdout != "NAME  NODES\n" || stderr != "prog: no realm given, using default\n" {
		t.Errorf("prog get cluster: %q, %q, %v", stdout, stderr, err)
	}
}
ExampleRun in cobratree/cobratreetest/example_test.go. CI compiles it.