diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 8860de6db5..db9528bf09 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version: 1.22 - name: Build run: go build -v ./... diff --git a/.gitignore b/.gitignore index 1069f17531..50de1b9ac9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ *.coverprofile node_modules/ -*.test \ No newline at end of file +*.test +.idea/ +.vscode/ \ No newline at end of file diff --git a/app.go b/app.go index 3a0f25f5ed..6ebfa19adb 100644 --- a/app.go +++ b/app.go @@ -12,14 +12,7 @@ import ( var ( changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md" - appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL) - - contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." - - errInvalidActionType = NewExitError("ERROR invalid Action type. "+ - fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+ - fmt.Sprintf("See %s", appActionDeprecationURL), 2) ) // App is the main structure of a cli application. It is recommended that @@ -63,9 +56,27 @@ type App struct { After AfterFunc // The action to execute when no subcommands are specified - // Expects a `cli.ActionFunc` but will accept the *deprecated* signature of `func(*cli.Context) {}` - // *Note*: support for the deprecated `Action` signature will be removed in a future version - Action interface{} + Action ActionFunc + + // DefaultBefore executes before the app, commands, and subcommands that do + // not specify a BeforeFunc of their own + DefaultBefore BeforeFunc + // DefaultAction is the action executed by the app, commands, and subcommands + // that do not specify an action of their own + DefaultAction ActionFunc + // DefaultAfter executes after the app, commands, and subcommands that do not + // specify an AfterFunc of their own + DefaultAfter AfterFunc + // DefaultOnUsageError is executed on a usage error by the app, commands, and + // subcommands that do not specify an OnUsageError of their own + DefaultOnUsageError OnUsageErrorFunc + // GlobalHideHelp hides the help flag for the app, all commands, and all subcommands + GlobalHideHelp bool + // GlobalHideHelpCommand hides the help command for the app, all commands, and all subcommands + GlobalHideHelpCommand bool + // GlobalFlags are flags that can be used by the app, any command, or any + // subcommand, unless command.NoGlobalFlags is true + GlobalFlags []Flag // Execute this function if the proper command cannot be found CommandNotFound CommandNotFoundFunc @@ -149,11 +160,16 @@ func (a *App) Setup() { } a.Commands = newCmds + // make the app-wide GlobalFlags usable by the app itself + for _, fl := range a.GlobalFlags { + a.appendFlag(fl) + } + if a.Command(helpCommand.Name) == nil { - if !a.HideHelpCommand { + if !a.hideHelpCommand() { a.Commands = append(a.Commands, helpCommand) } - if !a.HideHelp && (HelpFlag != BoolFlag{}) { + if !a.hideHelp() && (HelpFlag != BoolFlag{}) { a.appendFlag(HelpFlag) } } @@ -211,8 +227,8 @@ func (a *App) Run(arguments []string) (err error) { } if err != nil { - if a.OnUsageError != nil { - err := a.OnUsageError(context, err, false) + if onUsageError := a.resolveOnUsageError(); onUsageError != nil { + err := onUsageError(context, err, false) HandleExitCoder(err) return err } @@ -220,7 +236,7 @@ func (a *App) Run(arguments []string) (err error) { return err } - if !a.HideHelp && checkHelp(context) { + if !a.hideHelp() && checkHelp(context) { ShowAppHelp(context) return nil } @@ -230,9 +246,9 @@ func (a *App) Run(arguments []string) (err error) { return nil } - if a.After != nil { + if after := a.resolveAfter(); after != nil { defer func() { - if afterErr := a.After(context); afterErr != nil { + if afterErr := after(context); afterErr != nil { if err != nil { err = NewMultiError(err, afterErr) } else { @@ -242,8 +258,8 @@ func (a *App) Run(arguments []string) (err error) { }() } - if a.Before != nil { - beforeErr := a.Before(context) + if before := a.resolveBefore(); before != nil { + beforeErr := before(context) if beforeErr != nil { fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) HandleExitCoder(beforeErr) @@ -261,12 +277,8 @@ func (a *App) Run(arguments []string) (err error) { } } - if a.Action == nil { - a.Action = helpCommand.Action - } - // Run default Action - err = HandleAction(a.Action, context) + err = a.resolveAction()(context) HandleExitCoder(err) return err @@ -290,10 +302,10 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { // append help to commands if len(a.Commands) > 0 { if a.Command(helpCommand.Name) == nil { - if !a.HideHelpCommand { + if !a.hideHelpCommand() { a.Commands = append(a.Commands, helpCommand) } - if !a.HideHelp && (HelpFlag != BoolFlag{}) { + if !a.hideHelp() && (HelpFlag != BoolFlag{}) { a.appendFlag(HelpFlag) } } @@ -330,8 +342,8 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } if err != nil { - if a.OnUsageError != nil { - err = a.OnUsageError(context, err, true) + if onUsageError := a.resolveOnUsageError(); onUsageError != nil { + err = onUsageError(context, err, true) HandleExitCoder(err) return err } @@ -349,9 +361,9 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } } - if a.After != nil { + if after := a.resolveAfter(); after != nil { defer func() { - afterErr := a.After(context) + afterErr := after(context) if afterErr != nil { HandleExitCoder(err) if err != nil { @@ -363,8 +375,8 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { }() } - if a.Before != nil { - beforeErr := a.Before(context) + if before := a.resolveBefore(); before != nil { + beforeErr := before(context) if beforeErr != nil { HandleExitCoder(beforeErr) err = beforeErr @@ -382,7 +394,7 @@ func (a *App) RunAsSubcommand(ctx *Context) (err error) { } // Run default Action - err = HandleAction(a.Action, context) + err = a.resolveAction()(context) HandleExitCoder(err) return err @@ -464,6 +476,50 @@ func (a *App) appendFlag(flag Flag) { } } +// hideHelp reports whether the built-in help flag should be hidden for this +// app, honoring both the app-specific HideHelp and the app-wide GlobalHideHelp. +func (a *App) hideHelp() bool { + return a.GlobalHideHelp || a.HideHelp +} + +// hideHelpCommand reports whether the built-in help command should be hidden +// for this app, honoring both the app-specific HideHelpCommand and the +// app-wide GlobalHideHelpCommand. +func (a *App) hideHelpCommand() bool { + return a.GlobalHideHelpCommand || a.HideHelpCommand +} + +func (a *App) resolveBefore() BeforeFunc { + if a.Before != nil { + return a.Before + } + return a.DefaultBefore +} + +func (a *App) resolveAfter() AfterFunc { + if a.After != nil { + return a.After + } + return a.DefaultAfter +} + +func (a *App) resolveAction() ActionFunc { + if a.Action != nil { + return a.Action + } + if a.DefaultAction != nil { + return a.DefaultAction + } + return helpCommand.Action +} + +func (a *App) resolveOnUsageError() OnUsageErrorFunc { + if a.OnUsageError != nil { + return a.OnUsageError + } + return a.DefaultOnUsageError +} + // Author represents someone who has contributed to a cli project. type Author struct { Name string // The Authors name @@ -479,19 +535,3 @@ func (a Author) String() string { return fmt.Sprintf("%v%v", a.Name, e) } - -// HandleAction attempts to figure out which Action signature was used. If -// it's an ActionFunc or a func with the legacy signature for Action, the func -// is run! -func HandleAction(action interface{}, context *Context) (err error) { - if a, ok := action.(ActionFunc); ok { - return a(context) - } else if a, ok := action.(func(*Context) error); ok { - return a(context) - } else if a, ok := action.(func(*Context)); ok { // deprecated function signature - a(context) - return nil - } else { - return errInvalidActionType - } -} diff --git a/app_test.go b/app_test.go index 06cd5324bb..b38927d87a 100644 --- a/app_test.go +++ b/app_test.go @@ -1579,90 +1579,6 @@ func TestCustomHelpVersionFlags(t *testing.T) { } } -func TestHandleAction_WithNonFuncAction(t *testing.T) { - app := NewApp() - app.Action = 42 - fs, err := flagSet(app.Name, app.Flags) - if err != nil { - t.Errorf("error creating FlagSet: %s", err) - } - err = HandleAction(app.Action, NewContext(app, fs, nil)) - - if err == nil { - t.Fatalf("expected to receive error from Run, got none") - } - - exitErr, ok := err.(*ExitError) - - if !ok { - t.Fatalf("expected to receive a *ExitError") - } - - if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action type.") { - t.Fatalf("expected an unknown Action error, but got: %v", exitErr.Error()) - } - - if exitErr.ExitCode() != 2 { - t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode()) - } -} - -func TestHandleAction_WithInvalidFuncSignature(t *testing.T) { - app := NewApp() - app.Action = func() string { return "" } - fs, err := flagSet(app.Name, app.Flags) - if err != nil { - t.Errorf("error creating FlagSet: %s", err) - } - err = HandleAction(app.Action, NewContext(app, fs, nil)) - - if err == nil { - t.Fatalf("expected to receive error from Run, got none") - } - - exitErr, ok := err.(*ExitError) - - if !ok { - t.Fatalf("expected to receive a *ExitError") - } - - if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action type") { - t.Fatalf("expected an unknown Action error, but got: %v", exitErr.Error()) - } - - if exitErr.ExitCode() != 2 { - t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode()) - } -} - -func TestHandleAction_WithInvalidFuncReturnSignature(t *testing.T) { - app := NewApp() - app.Action = func(_ *Context) (int, error) { return 0, nil } - fs, err := flagSet(app.Name, app.Flags) - if err != nil { - t.Errorf("error creating FlagSet: %s", err) - } - err = HandleAction(app.Action, NewContext(app, fs, nil)) - - if err == nil { - t.Fatalf("expected to receive error from Run, got none") - } - - exitErr, ok := err.(*ExitError) - - if !ok { - t.Fatalf("expected to receive a *ExitError") - } - - if !strings.HasPrefix(exitErr.Error(), "ERROR invalid Action type") { - t.Fatalf("expected an invalid Action signature error, but got: %v", exitErr.Error()) - } - - if exitErr.ExitCode() != 2 { - t.Fatalf("expected error exit code to be 2, but got: %v", exitErr.ExitCode()) - } -} - func TestHandleAction_WithUnknownPanic(t *testing.T) { defer func() { refute(t, recover(), nil) }() @@ -1677,7 +1593,7 @@ func TestHandleAction_WithUnknownPanic(t *testing.T) { if err != nil { t.Errorf("error creating FlagSet: %s", err) } - HandleAction(app.Action, NewContext(app, fs, nil)) + app.Action(NewContext(app, fs, nil)) } func TestShellCompletionForIncompleteFlags(t *testing.T) { @@ -1723,21 +1639,3 @@ func TestShellCompletionForIncompleteFlags(t *testing.T) { t.Errorf("app should not return an error: %s", err) } } - -func TestHandleActionActuallyWorksWithActions(t *testing.T) { - var f ActionFunc - called := false - f = func(c *Context) error { - called = true - return nil - } - - err := HandleAction(f, nil) - if err != nil { - t.Errorf("Should not have errored: %v", err) - } - - if !called { - t.Errorf("Function was not called") - } -} diff --git a/command.go b/command.go index a980862bed..3bf7a0452d 100644 --- a/command.go +++ b/command.go @@ -3,6 +3,7 @@ package cli import ( "fmt" "io/ioutil" + "slices" "sort" "strings" ) @@ -34,9 +35,7 @@ type Command struct { // It is run even if Action() panics After AfterFunc // The function to call when this command is invoked - Action interface{} - // TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind - // of deprecation period has passed, maybe? + Action ActionFunc // Execute this function if a usage error occurs. OnUsageError OnUsageErrorFunc @@ -44,6 +43,8 @@ type Command struct { Subcommands Commands // List of flags to parse Flags []Flag + // Do not append the app-wide App.GlobalFlags to this command's flags + NoGlobalFlags bool // Treat all flags as normal arguments if true SkipFlagParsing bool // Skip argument reordering which attempts to move flags before arguments, @@ -64,6 +65,9 @@ type Command struct { HelpName string commandNamePath []string + // app-wide GlobalFlags, populated at help-render time + globalFlags []Flag + // Default prompt, specific to OS Prompt string // Command to set the environment variable, specific to OS @@ -117,7 +121,10 @@ func (c Command) Run(ctx *Context) (err error) { return c.startApp(ctx) } - if !c.HideHelp && (HelpFlag != BoolFlag{}) { + // combine the command flags with any app-wide GlobalFlags + c.Flags = c.resolveFlags(ctx) + + if !c.hideHelp(ctx) && (HelpFlag != BoolFlag{}) { // append help to flags c.Flags = append( c.Flags, @@ -197,8 +204,8 @@ func (c Command) Run(ctx *Context) (err error) { } if err != nil { - if c.OnUsageError != nil { - err := c.OnUsageError(context, err, false) + if onUsageError := c.resolveOnUsageError(context); onUsageError != nil { + err := onUsageError(context, err, false) HandleExitCoder(err) return err } @@ -211,9 +218,9 @@ func (c Command) Run(ctx *Context) (err error) { return nil } - if c.After != nil { + if after := c.resolveAfter(context); after != nil { defer func() { - afterErr := c.After(context) + afterErr := after(context) if afterErr != nil { HandleExitCoder(err) if err != nil { @@ -225,8 +232,8 @@ func (c Command) Run(ctx *Context) (err error) { }() } - if c.Before != nil { - err = c.Before(context) + if before := c.resolveBefore(context); before != nil { + err = before(context) if err != nil { fmt.Fprintln(context.App.Writer, err) fmt.Fprintln(context.App.Writer) @@ -235,12 +242,9 @@ func (c Command) Run(ctx *Context) (err error) { } } - if c.Action == nil { - c.Action = helpSubcommand.Action - } - - err = HandleAction(c.Action, context) + c.Action = c.resolveAction(context) + err = c.Action(context) if err != nil { HandleExitCoder(err) } @@ -304,9 +308,18 @@ func (c Command) startApp(ctx *Context) error { // set the flags and commands app.Commands = c.Subcommands - app.Flags = c.Flags - app.HideHelp = c.HideHelp - app.HideHelpCommand = c.HideHelpCommand + app.Flags = c.resolveFlags(ctx) + app.HideHelp = c.hideHelp(ctx) + app.HideHelpCommand = c.hideHelpCommand(ctx) + + // propagate the app-wide globals so nested commands inherit them + app.DefaultBefore = ctx.App.DefaultBefore + app.DefaultAction = ctx.App.DefaultAction + app.DefaultAfter = ctx.App.DefaultAfter + app.DefaultOnUsageError = ctx.App.DefaultOnUsageError + app.GlobalHideHelp = ctx.App.GlobalHideHelp + app.GlobalHideHelpCommand = ctx.App.GlobalHideHelpCommand + app.GlobalFlags = ctx.App.GlobalFlags app.Version = ctx.App.Version app.HideVersion = ctx.App.HideVersion @@ -331,13 +344,10 @@ func (c Command) startApp(ctx *Context) error { } // set the actions - app.Before = c.Before - app.After = c.After - if c.Action != nil { - app.Action = c.Action - } else { - app.Action = helpSubcommand.Action - } + app.Before = c.resolveBefore(ctx) + app.After = c.resolveAfter(ctx) + app.OnUsageError = c.resolveOnUsageError(ctx) + app.Action = c.resolveAction(ctx) for index, cc := range app.Commands { app.Commands[index].commandNamePath = []string{c.Name, cc.Name} @@ -346,8 +356,64 @@ func (c Command) startApp(ctx *Context) error { return app.RunAsSubcommand(ctx) } +func (c Command) hideHelp(ctx *Context) bool { + return ctx.App.GlobalHideHelp || c.HideHelp +} + +func (c Command) hideHelpCommand(ctx *Context) bool { + return ctx.App.GlobalHideHelpCommand || c.HideHelpCommand +} + +func (c Command) resolveBefore(ctx *Context) BeforeFunc { + if c.Before != nil { + return c.Before + } + return ctx.App.DefaultBefore +} + +func (c Command) resolveAfter(ctx *Context) AfterFunc { + if c.After != nil { + return c.After + } + return ctx.App.DefaultAfter +} + +func (c Command) resolveOnUsageError(ctx *Context) OnUsageErrorFunc { + if c.OnUsageError != nil { + return c.OnUsageError + } + return ctx.App.DefaultOnUsageError +} + +func (c Command) resolveAction(ctx *Context) ActionFunc { + if c.Action != nil { + return c.Action + } + if ctx.App.DefaultAction != nil { + return ctx.App.DefaultAction + } + return helpSubcommand.Action +} + +func (c Command) resolveFlags(ctx *Context) []Flag { + if c.NoGlobalFlags { + return c.Flags + } + return slices.Concat(c.Flags, ctx.App.GlobalFlags) +} + // VisibleFlags returns a slice of the Flags with Hidden=false func (c Command) VisibleFlags() []Flag { + flags := slices.Concat(c.Flags, c.globalFlags) + if !c.HideHelp && (HelpFlag != BoolFlag{}) { + // append help to flags + flags = append(flags, HelpFlag) + } + return visibleFlags(flags) +} + +// VisibleLocalFlags returns a slice of the non-global Flags with Hidden=false +func (c Command) VisibleLocalFlags() []Flag { flags := c.Flags if !c.HideHelp && (HelpFlag != BoolFlag{}) { // append help to flags @@ -358,3 +424,8 @@ func (c Command) VisibleFlags() []Flag { } return visibleFlags(flags) } + +// VisibleGlobalFlags returns a slice of the global Flags with Hidden=false +func (c Command) VisibleGlobalFlags() []Flag { + return visibleFlags(c.globalFlags) +} diff --git a/go.mod b/go.mod index b2b7d73cd9..3e3aae9630 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/minio/cli -go 1.12 +go 1.22 require ( github.com/BurntSushi/toml v0.3.1 diff --git a/help.go b/help.go index 42dc59fa2a..2b1c041e80 100644 --- a/help.go +++ b/help.go @@ -198,6 +198,11 @@ func ShowCommandHelp(ctx *Context, command string) error { c.EnableHistory = defaultEnableHistory } + // expose global flags to the help template, honoring NoGlobalFlags + if !c.NoGlobalFlags { + c.globalFlags = ctx.App.GlobalFlags + } + if c.HasName(command) { if c.CustomHelpTemplate != "" { HelpPrinterCustom(ctx.App.HelpWriter, c.CustomHelpTemplate, c, nil) diff --git a/help_test.go b/help_test.go index db00613062..84d2709cd2 100644 --- a/help_test.go +++ b/help_test.go @@ -123,7 +123,7 @@ func Test_helpCommand_Action_ErrorIfNoTopic(t *testing.T) { c := NewContext(app, set, nil) - err := helpCommand.Action.(func(*Context) error)(c) + err := helpCommand.Action(c) if err == nil { t.Fatalf("expected error from helpCommand.Action(), but got nil") @@ -168,7 +168,7 @@ func Test_helpSubcommand_Action_ErrorIfNoTopic(t *testing.T) { c := NewContext(app, set, nil) - err := helpSubcommand.Action.(func(*Context) error)(c) + err := helpSubcommand.Action(c) if err == nil { t.Fatalf("expected error from helpCommand.Action(), but got nil") @@ -301,6 +301,72 @@ EXAMPLES: } } +func TestShowCommandHelp_VisibleGlobalFlags(t *testing.T) { + customTemplate := `NAME: + {{.HelpName}} - {{.Usage}} + +FLAGS: + {{range .VisibleFlags}}{{.}} + {{end}}` + + globalFlags := []Flag{ + StringFlag{Name: "config", Usage: "path to config"}, + BoolFlag{Name: "hidden-global", Hidden: true}, + } + + t.Run("included by default", func(t *testing.T) { + app := &App{ + GlobalFlags: globalFlags, + Commands: []Command{ + { + Name: "frobbly", + HelpName: "foo frobbly", + Action: func(*Context) error { return nil }, + Flags: []Flag{StringFlag{Name: "local", Usage: "a local flag"}}, + CustomHelpTemplate: customTemplate, + }, + }, + } + + output := &bytes.Buffer{} + app.HelpWriter = output + app.Run([]string{"foo", "help", "frobbly"}) + + if !strings.Contains(output.String(), "--config") { + t.Errorf("expected output to include global flag --config; got: %q", output.String()) + } + if !strings.Contains(output.String(), "--local") { + t.Errorf("expected output to include command flag --local; got: %q", output.String()) + } + if strings.Contains(output.String(), "--hidden-global") { + t.Errorf("expected output to exclude hidden global flag; got: %q", output.String()) + } + }) + + t.Run("excluded when NoGlobalFlags", func(t *testing.T) { + app := &App{ + GlobalFlags: globalFlags, + Commands: []Command{ + { + Name: "frobbly", + HelpName: "foo frobbly", + Action: func(*Context) error { return nil }, + NoGlobalFlags: true, + CustomHelpTemplate: customTemplate, + }, + }, + } + + output := &bytes.Buffer{} + app.HelpWriter = output + app.Run([]string{"foo", "help", "frobbly"}) + + if strings.Contains(output.String(), "--config") { + t.Errorf("expected output to exclude global flag --config; got: %q", output.String()) + } + }) +} + func TestShowAppHelp_HiddenCommand(t *testing.T) { app := &App{ Commands: []Command{