From 281df7e53f73f4c66e0af83e1ea69d2c4ad410a8 Mon Sep 17 00:00:00 2001 From: Tom Fleet Date: Mon, 26 Aug 2024 14:48:41 +0100 Subject: [PATCH] Improve unrecognised subcommand handling (#90) --- args.go | 9 +++++++++ args_test.go | 21 +++++++++++++++++++++ command.go | 9 +++++++++ 3 files changed, 39 insertions(+) diff --git a/args.go b/args.go index a7b5628..da00aac 100644 --- a/args.go +++ b/args.go @@ -26,6 +26,15 @@ func AnyArgs() ArgValidator { func NoArgs() ArgValidator { return func(cmd *Command, args []string) error { if len(args) > 0 { + if len(cmd.subcommands) > 0 { + // Maybe it's a typo of a subcommand + return fmt.Errorf( + "unknown subcommand %q for command %q, available subcommands: %v", + args[0], + cmd.name, + cmd.subcommandNames(), + ) + } return fmt.Errorf("command %s accepts no arguments but got %v", cmd.name, args) } return nil diff --git a/args_test.go b/args_test.go index 39ee1bd..663dccc 100644 --- a/args_test.go +++ b/args_test.go @@ -58,6 +58,27 @@ func TestArgValidators(t *testing.T) { wantErr: true, errMsg: "command test accepts no arguments but got [some args here]", }, + { + name: "noargs subcommand", + options: []cli.Option{ + cli.Args([]string{"subb", "args", "here"}), // Note: subb is typo of sub + cli.Run(func(cmd *cli.Command, args []string) error { + fmt.Fprintln(cmd.Stdout(), "Hello from noargs") + return nil + }), + cli.Allow(cli.NoArgs()), + cli.SubCommands( + func() (*cli.Command, error) { + return cli.New( + "sub", + cli.Run(func(cmd *cli.Command, args []string) error { return nil }), + ) + }, + ), + }, + wantErr: true, + errMsg: `unknown subcommand "subb" for command "test", available subcommands: [sub]`, + }, { name: "minargs pass", options: []cli.Option{ diff --git a/command.go b/command.go index 8de8b64..f9deeca 100644 --- a/command.go +++ b/command.go @@ -333,6 +333,15 @@ func (c *Command) hasShortFlag(name string) bool { return flag.DefaultValueNoArg != "" } +// subcommandNames returns a list of all the names of the current command's registered subcommands. +func (c *Command) subcommandNames() []string { + names := make([]string, 0, len(c.subcommands)) + for _, sub := range c.subcommands { + names = append(names, sub.name) + } + return names +} + // findRequestedCommand uses the raw arguments and the command tree to determine what // (if any) subcommand is being requested and return that command along with the arguments // that were meant for it.