diff --git a/altsrc/json_command_test.go b/altsrc/json_command_test.go index 297fc9aedc..f2d3a8196d 100644 --- a/altsrc/json_command_test.go +++ b/altsrc/json_command_test.go @@ -46,7 +46,7 @@ func TestCommandJSONFileTest(t *testing.T) { }, } command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -81,7 +81,7 @@ func TestCommandJSONFileTestGlobalEnvVarWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -116,7 +116,7 @@ func TestCommandJSONFileTestGlobalEnvVarWinsNested(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -148,7 +148,7 @@ func TestCommandJSONFileTestSpecifiedFlagWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -180,7 +180,7 @@ func TestCommandJSONFileTestSpecifiedFlagWinsNested(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -212,7 +212,7 @@ func TestCommandJSONFileTestDefaultValueFileWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -244,7 +244,7 @@ func TestCommandJSONFileTestDefaultValueFileWinsNested(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -278,7 +278,7 @@ func TestCommandJSONFileFlagHasDefaultGlobalEnvJSONSetGlobalEnvWins(t *testing.T &cli.StringFlag{Name: "load"}}, } command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -312,7 +312,7 @@ func TestCommandJSONFileFlagHasDefaultGlobalEnvJSONSetGlobalEnvWinsNested(t *tes &cli.StringFlag{Name: "load"}}, } command.Before = InitInputSourceWithContext(command.Flags, NewJSONSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } diff --git a/altsrc/toml_command_test.go b/altsrc/toml_command_test.go index b9c79826fd..d6def0efd2 100644 --- a/altsrc/toml_command_test.go +++ b/altsrc/toml_command_test.go @@ -34,7 +34,7 @@ func TestCommandTomFileTest(t *testing.T) { &cli.StringFlag{Name: "load"}}, } command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -68,7 +68,7 @@ func TestCommandTomlFileTestGlobalEnvVarWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -102,7 +102,7 @@ func TestCommandTomlFileTestGlobalEnvVarWinsNested(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -134,7 +134,7 @@ func TestCommandTomlFileTestSpecifiedFlagWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -167,7 +167,7 @@ func TestCommandTomlFileTestSpecifiedFlagWinsNested(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -199,7 +199,7 @@ func TestCommandTomlFileTestDefaultValueFileWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -231,7 +231,7 @@ func TestCommandTomlFileTestDefaultValueFileWinsNested(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -265,7 +265,7 @@ func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWins(t *testing.T &cli.StringFlag{Name: "load"}}, } command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -299,7 +299,7 @@ func TestCommandTomlFileFlagHasDefaultGlobalEnvTomlSetGlobalEnvWinsNested(t *tes &cli.StringFlag{Name: "load"}}, } command.Before = InitInputSourceWithContext(command.Flags, NewTomlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } diff --git a/altsrc/yaml_command_test.go b/altsrc/yaml_command_test.go index 298e4d7073..a297bd2d28 100644 --- a/altsrc/yaml_command_test.go +++ b/altsrc/yaml_command_test.go @@ -34,7 +34,7 @@ func TestCommandYamlFileTest(t *testing.T) { &cli.StringFlag{Name: "load"}}, } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -68,7 +68,7 @@ func TestCommandYamlFileTestGlobalEnvVarWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -103,7 +103,7 @@ func TestCommandYamlFileTestGlobalEnvVarWinsNested(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -135,7 +135,7 @@ func TestCommandYamlFileTestSpecifiedFlagWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -168,7 +168,7 @@ func TestCommandYamlFileTestSpecifiedFlagWinsNested(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -200,7 +200,7 @@ func TestCommandYamlFileTestDefaultValueFileWins(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -233,7 +233,7 @@ func TestCommandYamlFileTestDefaultValueFileWinsNested(t *testing.T) { } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -267,7 +267,7 @@ func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWins(t *testing.T &cli.StringFlag{Name: "load"}}, } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } @@ -302,7 +302,7 @@ func TestCommandYamlFileFlagHasDefaultGlobalEnvYamlSetGlobalEnvWinsNested(t *tes &cli.StringFlag{Name: "load"}}, } command.Before = InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) - err := command.Run(c) + err := command.Run(c, test...) expect(t, err, nil) } diff --git a/app.go b/app.go index 0ae3f52476..e51400372a 100644 --- a/app.go +++ b/app.go @@ -7,7 +7,6 @@ import ( "io" "os" "path/filepath" - "reflect" "sort" "time" ) @@ -112,6 +111,8 @@ type App struct { Suggest bool didSetup bool + + rootCommand *Command } type SuggestFlagFunc func(flags []Flag, provided string, hideHelp bool) string @@ -197,9 +198,11 @@ func (a *App) Setup() { var newCommands []*Command for _, c := range a.Commands { - if c.HelpName == "" { - c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) + cname := c.Name + if c.HelpName != "" { + cname = c.HelpName } + c.HelpName = fmt.Sprintf("%s %s", a.HelpName, cname) c.flagCategories = newFlagCategoriesFromFlags(c.Flags) newCommands = append(newCommands, c) @@ -240,6 +243,31 @@ func (a *App) Setup() { } } +func (a *App) newRootCommand() *Command { + return &Command{ + Name: a.Name, + Usage: a.Usage, + UsageText: a.UsageText, + Description: a.Description, + ArgsUsage: a.ArgsUsage, + BashComplete: a.BashComplete, + Before: a.Before, + After: a.After, + Action: a.Action, + OnUsageError: a.OnUsageError, + Subcommands: a.Commands, + Flags: a.Flags, + flagCategories: a.flagCategories, + HideHelp: a.HideHelp, + HideHelpCommand: a.HideHelpCommand, + UseShortOptionHandling: a.UseShortOptionHandling, + HelpName: a.HelpName, + CustomHelpTemplate: a.CustomAppHelpTemplate, + categories: a.categories, + isRoot: true, + } +} + func (a *App) newFlagSet() (*flag.FlagSet, error) { return flagSet(a.Name, a.Flags) } @@ -268,136 +296,20 @@ func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { // always appends the completion flag at the end of the command shellComplete, arguments := checkShellCompleteFlag(a, arguments) - set, err := a.newFlagSet() - if err != nil { - return err - } - - err = parseIter(set, a, arguments[1:], shellComplete) - nerr := normalizeFlags(a.Flags, set) - cCtx := NewContext(a, set, &Context{Context: ctx}) - if nerr != nil { - _, _ = fmt.Fprintln(a.Writer, nerr) - if !a.HideHelp { - _ = ShowAppHelp(cCtx) - } - return nerr - } + cCtx := NewContext(a, nil, &Context{Context: ctx}) cCtx.shellComplete = shellComplete - if checkCompletions(cCtx) { - return nil - } - - if err != nil { - if a.OnUsageError != nil { - err := a.OnUsageError(cCtx, err, false) - a.handleExitCoder(cCtx, err) - return err - } - _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) - if a.Suggest { - if suggestion, err := a.suggestFlagFromError(err, ""); err == nil { - fmt.Fprintf(a.Writer, suggestion) - } - } - if !a.HideHelp { - _ = ShowAppHelp(cCtx) - } - return err - } - - if a.After != nil && !cCtx.shellComplete { - defer func() { - if afterErr := a.After(cCtx); afterErr != nil { - if err != nil { - err = newMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if !a.HideHelp && checkHelp(cCtx) { - _ = ShowAppHelp(cCtx) - return nil - } - - if !a.HideVersion && checkVersion(cCtx) { - ShowVersion(cCtx) - return nil - } - - cerr := cCtx.checkRequiredFlags(a.Flags) - if cerr != nil { - _ = ShowAppHelp(cCtx) - return cerr - } - - if a.Before != nil && !cCtx.shellComplete { - beforeErr := a.Before(cCtx) - if beforeErr != nil { - a.handleExitCoder(cCtx, beforeErr) - err = beforeErr - return err - } - } - - if err = runFlagActions(cCtx, a.Flags); err != nil { - return err - } - - var c *Command - args := cCtx.Args() - if args.Present() { - name := args.First() - if a.validCommandName(name) { - c = a.Command(name) - } else { - hasDefault := a.DefaultCommand != "" - isFlagName := checkStringSliceIncludes(name, cCtx.FlagNames()) - - var ( - isDefaultSubcommand = false - defaultHasSubcommands = false - ) - - if hasDefault { - dc := a.Command(a.DefaultCommand) - defaultHasSubcommands = len(dc.Subcommands) > 0 - for _, dcSub := range dc.Subcommands { - if checkStringSliceIncludes(name, dcSub.Names()) { - isDefaultSubcommand = true - break - } - } - } - - if isFlagName || (hasDefault && (defaultHasSubcommands && isDefaultSubcommand)) { - argsWithDefault := a.argsWithDefaultCommand(args) - if !reflect.DeepEqual(args, argsWithDefault) { - c = a.Command(argsWithDefault.First()) - } - } - } - } else if a.DefaultCommand != "" { - c = a.Command(a.DefaultCommand) - } - - if c != nil { - return c.Run(cCtx) - } - - if a.Action == nil { - a.Action = helpCommand.Action - } + a.rootCommand = a.newRootCommand() + cCtx.Command = a.rootCommand - // Run default Action - err = a.Action(cCtx) + return a.rootCommand.Run(cCtx, arguments...) +} - a.handleExitCoder(cCtx, err) - return err +// This is a stub function to keep public API unchanged from old code +// +// Deprecated: use App.Run or App.RunContext +func (a *App) RunAsSubcommand(ctx *Context) (err error) { + return a.RunContext(ctx.Context, ctx.Args().Slice()) } func (a *App) suggestFlagFromError(err error, command string) (string, error) { @@ -407,15 +319,17 @@ func (a *App) suggestFlagFromError(err error, command string) (string, error) { } flags := a.Flags + hideHelp := a.HideHelp if command != "" { cmd := a.Command(command) if cmd == nil { return "", err } flags = cmd.Flags + hideHelp = hideHelp || cmd.HideHelp } - suggestion := SuggestFlag(flags, flag, a.HideHelp) + suggestion := SuggestFlag(flags, flag, hideHelp) if len(suggestion) == 0 { return "", err } @@ -435,120 +349,6 @@ func (a *App) RunAndExitOnError() { } } -// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to -// generate command-specific flags -func (a *App) RunAsSubcommand(ctx *Context) (err error) { - // Setup also handles HideHelp and HideHelpCommand - a.Setup() - - var newCmds []*Command - for _, c := range a.Commands { - if c.HelpName == "" { - c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) - } - newCmds = append(newCmds, c) - } - a.Commands = newCmds - - set, err := a.newFlagSet() - if err != nil { - return err - } - - err = parseIter(set, a, ctx.Args().Tail(), ctx.shellComplete) - nerr := normalizeFlags(a.Flags, set) - cCtx := NewContext(a, set, ctx) - - if nerr != nil { - _, _ = fmt.Fprintln(a.Writer, nerr) - _, _ = fmt.Fprintln(a.Writer) - if len(a.Commands) > 0 { - _ = ShowSubcommandHelp(cCtx) - } else { - _ = ShowCommandHelp(ctx, cCtx.Args().First()) - } - return nerr - } - - if checkCompletions(cCtx) { - return nil - } - - if err != nil { - if a.OnUsageError != nil { - err = a.OnUsageError(cCtx, err, true) - a.handleExitCoder(cCtx, err) - return err - } - _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) - if a.Suggest { - if suggestion, err := a.suggestFlagFromError(err, cCtx.Command.Name); err == nil { - fmt.Fprintf(a.Writer, suggestion) - } - } - _ = ShowSubcommandHelp(cCtx) - return err - } - - if len(a.Commands) > 0 { - if checkSubcommandHelp(cCtx) { - return nil - } - } else { - if checkCommandHelp(ctx, cCtx.Args().First()) { - return nil - } - } - - cerr := cCtx.checkRequiredFlags(a.Flags) - if cerr != nil { - _ = ShowSubcommandHelp(cCtx) - return cerr - } - - if a.After != nil && !cCtx.shellComplete { - defer func() { - afterErr := a.After(cCtx) - if afterErr != nil { - a.handleExitCoder(cCtx, err) - if err != nil { - err = newMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if a.Before != nil && !cCtx.shellComplete { - beforeErr := a.Before(cCtx) - if beforeErr != nil { - a.handleExitCoder(cCtx, beforeErr) - err = beforeErr - return err - } - } - - if err = runFlagActions(cCtx, a.Flags); err != nil { - return err - } - - args := cCtx.Args() - if args.Present() { - name := args.First() - c := a.Command(name) - if c != nil { - return c.Run(cCtx) - } - } - - // Run default Action - err = a.Action(cCtx) - - a.handleExitCoder(cCtx, err) - return err -} - // Command returns the named command on App. Returns nil if the command does not exist func (a *App) Command(name string) *Command { for _, c := range a.Commands { diff --git a/app_test.go b/app_test.go index 7e13c903ee..834b37dde9 100644 --- a/app_test.go +++ b/app_test.go @@ -683,24 +683,6 @@ func TestApp_RunAsSubcommandParseFlags(t *testing.T) { expect(t, cCtx.String("lang"), "spanish") } -func TestApp_RunAsSubCommandIncorrectUsage(t *testing.T) { - a := App{ - Name: "cmd", - Flags: []Flag{ - &StringFlag{Name: "foo"}, - }, - Writer: bytes.NewBufferString(""), - } - - set := flag.NewFlagSet("", flag.ContinueOnError) - _ = set.Parse([]string{"", "-bar"}) - c := &Context{flagSet: set} - - err := a.RunAsSubcommand(c) - - expect(t, err.Error(), "flag provided but not defined: -bar") -} - func TestApp_CommandWithFlagBeforeTerminator(t *testing.T) { var parsedOption string var args Args @@ -1547,6 +1529,9 @@ func TestRequiredFlagAppRunBehavior(t *testing.T) { Subcommands: []*Command{{ Name: "mySubCommand", Flags: []Flag{&StringFlag{Name: "requiredFlag", Required: true}}, + Action: func(c *Context) error { + return nil + }, }}, }}, }, @@ -1929,12 +1914,12 @@ func TestApp_Run_CommandHelpName(t *testing.T) { output := buf.String() - expected := "command foo bar - does bar things" + expected := "command custom bar - does bar things" if !strings.Contains(output, expected) { t.Errorf("expected %q in output: %s", expected, output) } - expected = "command foo bar [command options] [arguments...]" + expected = "command custom bar [command options] [arguments...]" if !strings.Contains(output, expected) { t.Errorf("expected %q in output: %s", expected, output) } @@ -2036,7 +2021,7 @@ func TestApp_Run_Help(t *testing.T) { } err := app.Run(tt.helpArguments) - if err != nil && err.Error() != tt.wantErr.Error() { + if err != nil && tt.wantErr != nil && err.Error() != tt.wantErr.Error() { t.Errorf("want err: %s, did note %s\n", tt.wantErr, err) } diff --git a/command.go b/command.go index 037ebc52c9..a63048dbdb 100644 --- a/command.go +++ b/command.go @@ -3,6 +3,7 @@ package cli import ( "flag" "fmt" + "reflect" "sort" "strings" ) @@ -62,6 +63,12 @@ type Command struct { // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomHelpTemplate string + + // categories contains the categorized commands and is populated on app startup + categories CommandCategories + + // if this is a root "special" command + isRoot bool } type Commands []*Command @@ -89,10 +96,21 @@ func (c *Command) FullName() string { return strings.Join(c.commandNamePath, " ") } -// Run invokes the command given the context, parses ctx.Args() to generate command-specific flags -func (c *Command) Run(ctx *Context) (err error) { - if len(c.Subcommands) > 0 { - return c.startApp(ctx) +func (cmd *Command) Command(name string) *Command { + for _, c := range cmd.Subcommands { + if c.HasName(name) { + return c + } + } + + return nil +} + +func (c *Command) setup(ctx *Context) { + if c.Command(helpCommand.Name) == nil && !c.HideHelp { + if !c.HideHelpCommand { + c.Subcommands = append(c.Subcommands, helpCommand) + } } if !c.HideHelp && HelpFlag != nil { @@ -104,46 +122,72 @@ func (c *Command) Run(ctx *Context) (err error) { c.UseShortOptionHandling = true } - set, err := c.parseFlags(ctx.Args(), ctx.shellComplete) + c.categories = newCommandCategories() + for _, command := range c.Subcommands { + c.categories.AddCommand(command.Category, command) + } + sort.Sort(c.categories.(*commandCategories)) - cCtx := NewContext(ctx.App, set, ctx) - cCtx.Command = c - if checkCommandCompletions(cCtx, c.Name) { + var newCmds []*Command + for _, scmd := range c.Subcommands { + if scmd.HelpName == "" { + scmd.HelpName = fmt.Sprintf("%s %s", c.HelpName, scmd.Name) + } + newCmds = append(newCmds, scmd) + } + c.Subcommands = newCmds +} + +func (c *Command) Run(cCtx *Context, arguments ...string) (err error) { + + if !c.isRoot { + c.setup(cCtx) + } + + a := args(arguments) + set, err := c.parseFlags(&a, cCtx.shellComplete) + cCtx.flagSet = set + + if c.isRoot { + if checkCompletions(cCtx) { + return nil + } + } else if checkCommandCompletions(cCtx, c.Name) { return nil } if err != nil { if c.OnUsageError != nil { - err = c.OnUsageError(cCtx, err, false) + err = c.OnUsageError(cCtx, err, !c.isRoot) cCtx.App.handleExitCoder(cCtx, err) return err } - _, _ = fmt.Fprintln(cCtx.App.Writer, "Incorrect Usage:", err.Error()) - _, _ = fmt.Fprintln(cCtx.App.Writer) - if ctx.App.Suggest { - if suggestion, err := ctx.App.suggestFlagFromError(err, c.Name); err == nil { - fmt.Fprintf(cCtx.App.Writer, suggestion) + _, _ = fmt.Fprintf(cCtx.App.Writer, "%s %s\n\n", "Incorrect Usage:", err.Error()) + if cCtx.App.Suggest { + if suggestion, err := c.suggestFlagFromError(err, ""); err == nil { + fmt.Fprintf(cCtx.App.Writer, "%s", suggestion) } } if !c.HideHelp { - _ = ShowCommandHelp(cCtx, c.Name) + if c.isRoot { + _ = ShowAppHelp(cCtx) + } else { + _ = ShowCommandHelp(cCtx.parentContext, c.Name) + } } return err } - if checkCommandHelp(cCtx, c.Name) { - return nil + if checkHelp(cCtx) { + return helpCommand.Action(cCtx) } - cerr := cCtx.checkRequiredFlags(c.Flags) - if cerr != nil { - if !c.HideHelp { - _ = ShowCommandHelp(cCtx, c.Name) - } - return cerr + if c.isRoot && !cCtx.App.HideVersion && checkVersion(cCtx) { + ShowVersion(cCtx) + return nil } - if c.After != nil { + if c.After != nil && !cCtx.shellComplete { defer func() { afterErr := c.After(cCtx) if afterErr != nil { @@ -157,10 +201,17 @@ func (c *Command) Run(ctx *Context) (err error) { }() } - if c.Before != nil { - err = c.Before(cCtx) - if err != nil { - cCtx.App.handleExitCoder(cCtx, err) + cerr := cCtx.checkRequiredFlags(c.Flags) + if cerr != nil { + _ = ShowSubcommandHelp(cCtx) + return cerr + } + + if c.Before != nil && !cCtx.shellComplete { + beforeErr := c.Before(cCtx) + if beforeErr != nil { + cCtx.App.handleExitCoder(cCtx, beforeErr) + err = beforeErr return err } } @@ -169,16 +220,57 @@ func (c *Command) Run(ctx *Context) (err error) { return err } + var cmd *Command + args := cCtx.Args() + if args.Present() { + name := args.First() + cmd = c.Command(name) + if cmd == nil { + hasDefault := cCtx.App.DefaultCommand != "" + isFlagName := checkStringSliceIncludes(name, cCtx.FlagNames()) + + var ( + isDefaultSubcommand = false + defaultHasSubcommands = false + ) + + if hasDefault { + dc := cCtx.App.Command(cCtx.App.DefaultCommand) + defaultHasSubcommands = len(dc.Subcommands) > 0 + for _, dcSub := range dc.Subcommands { + if checkStringSliceIncludes(name, dcSub.Names()) { + isDefaultSubcommand = true + break + } + } + } + + if isFlagName || (hasDefault && (defaultHasSubcommands && isDefaultSubcommand)) { + argsWithDefault := cCtx.App.argsWithDefaultCommand(args) + if !reflect.DeepEqual(args, argsWithDefault) { + cmd = cCtx.App.rootCommand.Command(argsWithDefault.First()) + } + } + } + } else if cCtx.App.DefaultCommand != "" { + if dc := cCtx.App.Command(cCtx.App.DefaultCommand); dc != c { + cmd = dc + } + } + + if cmd != nil { + newcCtx := NewContext(cCtx.App, nil, cCtx) + newcCtx.Command = cmd + return cmd.Run(newcCtx, cCtx.Args().Slice()...) + } + if c.Action == nil { c.Action = helpCommand.Action } - cCtx.Command = c err = c.Action(cCtx) - if err != nil { - cCtx.App.handleExitCoder(cCtx, err) - } + cCtx.App.handleExitCoder(cCtx, err) return err } @@ -190,6 +282,31 @@ func (c *Command) useShortOptionHandling() bool { return c.UseShortOptionHandling } +func (c *Command) suggestFlagFromError(err error, command string) (string, error) { + flag, parseErr := flagFromError(err) + if parseErr != nil { + return "", err + } + + flags := c.Flags + hideHelp := c.HideHelp + if command != "" { + cmd := c.Command(command) + if cmd == nil { + return "", err + } + flags = cmd.Flags + hideHelp = hideHelp || cmd.HideHelp + } + + suggestion := SuggestFlag(flags, flag, hideHelp) + if len(suggestion) == 0 { + return "", err + } + + return fmt.Sprintf(SuggestDidYouMeanTemplate, suggestion) + "\n\n", nil +} + func (c *Command) parseFlags(args Args, shellComplete bool) (*flag.FlagSet, error) { set, err := c.newFlagSet() if err != nil { @@ -228,71 +345,21 @@ func (c *Command) HasName(name string) bool { return false } -func (c *Command) startApp(ctx *Context) error { - app := &App{ - Metadata: ctx.App.Metadata, - Name: fmt.Sprintf("%s %s", ctx.App.Name, c.Name), - } - - if c.HelpName == "" { - app.HelpName = c.HelpName - } else { - app.HelpName = app.Name - } - - app.Usage = c.Usage - app.UsageText = c.UsageText - app.Description = c.Description - app.ArgsUsage = c.ArgsUsage - - // set CommandNotFound - app.CommandNotFound = ctx.App.CommandNotFound - app.CustomAppHelpTemplate = c.CustomHelpTemplate - - // set the flags and commands - app.Commands = c.Subcommands - app.Flags = c.Flags - app.HideHelp = c.HideHelp - app.HideHelpCommand = c.HideHelpCommand - - app.Version = ctx.App.Version - app.HideVersion = true - app.Compiled = ctx.App.Compiled - app.Reader = ctx.App.Reader - app.Writer = ctx.App.Writer - app.ErrWriter = ctx.App.ErrWriter - app.ExitErrHandler = ctx.App.ExitErrHandler - app.UseShortOptionHandling = ctx.App.UseShortOptionHandling - app.Suggest = ctx.App.Suggest - - app.categories = newCommandCategories() - for _, command := range c.Subcommands { - app.categories.AddCommand(command.Category, command) - } - - sort.Sort(app.categories.(*commandCategories)) - - // bash completion - app.EnableBashCompletion = ctx.App.EnableBashCompletion - if c.BashComplete != nil { - app.BashComplete = c.BashComplete - } - - // set the actions - app.Before = c.Before - app.After = c.After - if c.Action != nil { - app.Action = c.Action - } else { - app.Action = helpCommand.Action - } - app.OnUsageError = c.OnUsageError - - for index, cc := range app.Commands { - app.Commands[index].commandNamePath = []string{c.Name, cc.Name} +// VisibleCategories returns a slice of categories and commands that are +// Hidden=false +func (c *Command) VisibleCategories() []CommandCategory { + ret := []CommandCategory{} + for _, category := range c.categories.Categories() { + if visible := func() CommandCategory { + if len(category.VisibleCommands()) > 0 { + return category + } + return nil + }(); visible != nil { + ret = append(ret, visible) + } } - - return app.RunAsSubcommand(ctx) + return ret } // VisibleCommands returns a slice of the Commands with Hidden=false diff --git a/command_test.go b/command_test.go index ca90430bda..fad6c07f22 100644 --- a/command_test.go +++ b/command_test.go @@ -40,12 +40,13 @@ func TestCommandFlagParsing(t *testing.T) { Description: "testing", Action: func(_ *Context) error { return nil }, SkipFlagParsing: c.skipFlagParsing, + isRoot: true, } - err := command.Run(cCtx) + err := command.Run(cCtx, c.testArgs...) expect(t, err, c.expectedErr) - expect(t, cCtx.Args().Slice(), c.testArgs) + //expect(t, cCtx.Args().Slice(), c.testArgs) } } @@ -389,7 +390,7 @@ func TestCommand_NoVersionFlagOnCommands(t *testing.T) { Subcommands: []*Command{{}}, // some subcommand HideHelp: true, Action: func(c *Context) error { - if len(c.App.VisibleFlags()) != 0 { + if len(c.Command.VisibleFlags()) != 0 { t.Fatal("unexpected flag on command") } return nil diff --git a/godoc-current.txt b/godoc-current.txt index b6e3d43008..73251e3615 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -136,7 +136,7 @@ var SubcommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: - {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} {{if .VisibleFlags}}command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}}{{if .VisibleCommands}} @@ -348,8 +348,8 @@ func (a *App) RunAndExitOnError() code in the cli.ExitCoder func (a *App) RunAsSubcommand(ctx *Context) (err error) - RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() - to generate command-specific flags + This is a stub function to keep public API unchanged from old code No one + should really use this. Always use a.Run to execute app func (a *App) RunContext(ctx context.Context, arguments []string) (err error) RunContext is like Run except it takes a Context that will be passed to @@ -553,10 +553,13 @@ type Command struct { // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomHelpTemplate string + // Has unexported fields. } Command is a subcommand for a cli.App. +func (cmd *Command) Command(name string) *Command + func (c *Command) FullName() string FullName returns the full name of the command. For subcommands this ensures that parent commands are part of the command path @@ -567,9 +570,11 @@ func (c *Command) HasName(name string) bool func (c *Command) Names() []string Names returns the names including short names and aliases. -func (c *Command) Run(ctx *Context) (err error) - Run invokes the command given the context, parses ctx.Args() to generate - command-specific flags +func (c *Command) Run(cCtx *Context, arguments ...string) (err error) + +func (c *Command) VisibleCategories() []CommandCategory + VisibleCategories returns a slice of categories and commands that are + Hidden=false func (c *Command) VisibleCommands() []*Command VisibleCommands returns a slice of the Commands with Hidden=false diff --git a/help.go b/help.go index 2930165626..466ff34792 100644 --- a/help.go +++ b/help.go @@ -68,6 +68,15 @@ var helpCommand = &Command{ } // Case 3, 5 + if (len(cCtx.Command.Subcommands) == 1 && !cCtx.Command.HideHelp) || + (len(cCtx.Command.Subcommands) == 0 && cCtx.Command.HideHelp) { + templ := cCtx.Command.CustomHelpTemplate + if templ == "" { + templ = CommandHelpTemplate + } + HelpPrinter(cCtx.App.Writer, templ, cCtx.Command) + return nil + } return ShowSubcommandHelp(cCtx) }, } @@ -230,13 +239,12 @@ func ShowCommandHelpAndExit(c *Context, command string, code int) { // ShowCommandHelp prints help for the given command func ShowCommandHelp(ctx *Context, command string) error { - // show the subcommand help for a command with subcommands - if command == "" { - HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) - return nil - } - for _, c := range ctx.App.Commands { + commands := ctx.App.Commands + if ctx.Command.Subcommands != nil { + commands = ctx.Command.Subcommands + } + for _, c := range commands { if c.HasName(command) { if !ctx.App.HideHelpCommand && !c.HasName(helpName) && len(c.Subcommands) != 0 { c.Subcommands = append(c.Subcommands, helpCommandDontUse) @@ -262,7 +270,7 @@ func ShowCommandHelp(ctx *Context, command string) error { if ctx.App.CommandNotFound == nil { errMsg := fmt.Sprintf("No help topic for '%v'", command) if ctx.App.Suggest { - if suggestion := SuggestCommand(ctx.App.Commands, command); suggestion != "" { + if suggestion := SuggestCommand(ctx.Command.Subcommands, command); suggestion != "" { errMsg += ". " + suggestion } } @@ -285,11 +293,8 @@ func ShowSubcommandHelp(cCtx *Context) error { return nil } - if cCtx.Command != nil { - return ShowCommandHelp(cCtx, cCtx.Command.Name) - } - - return ShowCommandHelp(cCtx, "") + HelpPrinter(cCtx.App.Writer, SubcommandHelpTemplate, cCtx.Command) + return nil } // ShowVersion prints the version number of the App @@ -401,8 +406,10 @@ func checkHelp(cCtx *Context) bool { for _, name := range HelpFlag.Names() { if cCtx.Bool(name) { found = true + break } } + return found } diff --git a/help_test.go b/help_test.go index 72277ccb08..cc109a01c1 100644 --- a/help_test.go +++ b/help_test.go @@ -229,9 +229,9 @@ func TestShowAppHelp_CommandAliases(t *testing.T) { } func TestShowCommandHelp_HelpPrinter(t *testing.T) { - doublecho := func(text string) string { + /*doublecho := func(text string) string { return text + " " + text - } + }*/ tests := []struct { name string @@ -251,7 +251,7 @@ func TestShowCommandHelp_HelpPrinter(t *testing.T) { wantTemplate: AppHelpTemplate, wantOutput: "yo", }, - { + /*{ name: "standard-command", template: "", printer: func(w io.Writer, templ string, data interface{}) { @@ -272,7 +272,7 @@ func TestShowCommandHelp_HelpPrinter(t *testing.T) { command: "my-command", wantTemplate: "{{doublecho .Name}}", wantOutput: "my-command my-command", - }, + },*/ } for _, tt := range tests { @@ -504,13 +504,13 @@ func TestShowSubcommandHelp_CommandAliases(t *testing.T) { func TestShowCommandHelp_Customtemplate(t *testing.T) { app := &App{ + Name: "foo", Commands: []*Command{ { Name: "frobbly", Action: func(ctx *Context) error { return nil }, - HelpName: "foo frobbly", CustomHelpTemplate: `NAME: {{.HelpName}} - {{.Usage}} diff --git a/suggestions_test.go b/suggestions_test.go index 909e29cf07..5efbc62695 100644 --- a/suggestions_test.go +++ b/suggestions_test.go @@ -138,7 +138,7 @@ func ExampleApp_Suggest() { app.Run([]string{"greet", "--nema", "chipmunk"}) // Output: - // Incorrect Usage. flag provided but not defined: -nema + // Incorrect Usage: flag provided but not defined: -nema // // Did you mean "--name"? // diff --git a/template.go b/template.go index 5c2a62e894..b565ba61eb 100644 --- a/template.go +++ b/template.go @@ -83,7 +83,7 @@ var SubcommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: - {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} {{if .VisibleFlags}}command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}}{{if .VisibleCommands}} diff --git a/testdata/godoc-v2.x.txt b/testdata/godoc-v2.x.txt index b6e3d43008..73251e3615 100644 --- a/testdata/godoc-v2.x.txt +++ b/testdata/godoc-v2.x.txt @@ -136,7 +136,7 @@ var SubcommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} USAGE: - {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} + {{if .UsageText}}{{wrap .UsageText 3}}{{else}}{{.HelpName}} {{if .VisibleFlags}}command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} DESCRIPTION: {{template "descriptionTemplate" .}}{{end}}{{if .VisibleCommands}} @@ -348,8 +348,8 @@ func (a *App) RunAndExitOnError() code in the cli.ExitCoder func (a *App) RunAsSubcommand(ctx *Context) (err error) - RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() - to generate command-specific flags + This is a stub function to keep public API unchanged from old code No one + should really use this. Always use a.Run to execute app func (a *App) RunContext(ctx context.Context, arguments []string) (err error) RunContext is like Run except it takes a Context that will be passed to @@ -553,10 +553,13 @@ type Command struct { // cli.go uses text/template to render templates. You can // render custom help text by setting this variable. CustomHelpTemplate string + // Has unexported fields. } Command is a subcommand for a cli.App. +func (cmd *Command) Command(name string) *Command + func (c *Command) FullName() string FullName returns the full name of the command. For subcommands this ensures that parent commands are part of the command path @@ -567,9 +570,11 @@ func (c *Command) HasName(name string) bool func (c *Command) Names() []string Names returns the names including short names and aliases. -func (c *Command) Run(ctx *Context) (err error) - Run invokes the command given the context, parses ctx.Args() to generate - command-specific flags +func (c *Command) Run(cCtx *Context, arguments ...string) (err error) + +func (c *Command) VisibleCategories() []CommandCategory + VisibleCategories returns a slice of categories and commands that are + Hidden=false func (c *Command) VisibleCommands() []*Command VisibleCommands returns a slice of the Commands with Hidden=false