From e77d5b82a68d2240cda8147a4566d0cd3fdc1d68 Mon Sep 17 00:00:00 2001 From: Mohit Panchariya Date: Wed, 18 Jun 2025 23:00:59 +0530 Subject: [PATCH 01/13] Invoke OnUsageError when missing required flags In case of missing required flags, the OnUsageError function will now be invoked instead of directly returning the error. --- command_run.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/command_run.go b/command_run.go index 24b7935166..44ffc22349 100644 --- a/command_run.go +++ b/command_run.go @@ -314,7 +314,11 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context if err := cmd.checkAllRequiredFlags(); err != nil { cmd.isInError = true - _ = ShowSubcommandHelp(cmd) + if cmd.OnUsageError != nil { + err = cmd.OnUsageError(ctx, cmd, err, cmd.parent != nil) + } else { + _ = ShowSubcommandHelp(cmd) + } return ctx, err } From 2f55f51a5594bbbf86590619434d4e4a2d5f9749 Mon Sep 17 00:00:00 2001 From: Mohit Panchariya Date: Fri, 20 Jun 2025 12:32:37 +0530 Subject: [PATCH 02/13] Add test --- command_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/command_test.go b/command_test.go index b9e69bc307..6df83cdb75 100644 --- a/command_test.go +++ b/command_test.go @@ -4096,6 +4096,21 @@ func TestCheckRequiredFlags(t *testing.T) { } } +func TestCheckRequiredFlagsWithOnUsageError(t *testing.T) { + expectedError := errors.New("OnUsageError") + cmd := &Command{ + Name: "foo", + Flags: []Flag{ + &StringFlag{Name: "requiredFlag", Required: true}, + }, + OnUsageError: func(_ context.Context, _ *Command, _ error, _ bool) error { + return expectedError + }, + } + actualError := cmd.Run(buildTestContext(t), []string{"requiredFlag"}) + require.ErrorIs(t, actualError, expectedError) +} + func TestCommand_ParentCommand_Set(t *testing.T) { cmd := &Command{ parent: &Command{ From 1562c231a6503023148c89f30b40a5c42ad6c4fb Mon Sep 17 00:00:00 2001 From: almas-x Date: Thu, 5 Jun 2025 14:23:19 +0800 Subject: [PATCH 03/13] feat: export help display functions as variables to allow custom help display logic --- godoc-current.txt | 18 +++++++++--------- help.go | 17 ++++++++++++----- testdata/godoc-v3.x.txt | 18 +++++++++--------- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/godoc-current.txt b/godoc-current.txt index edd42feb88..aa3ca2d73c 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -131,6 +131,15 @@ COPYRIGHT: cli.go uses text/template to render templates. You can render custom help text by setting this variable. +var ShowAppHelp = showAppHelp + ShowAppHelp is an action that displays the help + +var ShowCommandHelp = showCommandHelp + ShowCommandHelp prints help for the given command + +var ShowSubcommandHelp = showSubcommandHelp + ShowSubcommandHelp prints help for the given subcommand + var SubcommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} @@ -195,22 +204,13 @@ func HandleExitCoder(err error) This function is the default error-handling behavior for an App. -func ShowAppHelp(cmd *Command) error - ShowAppHelp is an action that displays the help. - func ShowAppHelpAndExit(cmd *Command, exitCode int) ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. -func ShowCommandHelp(ctx context.Context, cmd *Command, commandName string) error - ShowCommandHelp prints help for the given command - func ShowCommandHelpAndExit(ctx context.Context, cmd *Command, command string, code int) ShowCommandHelpAndExit - exits with code after showing help -func ShowSubcommandHelp(cmd *Command) error - ShowSubcommandHelp prints help for the given subcommand - func ShowSubcommandHelpAndExit(cmd *Command, exitCode int) ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits with exit code. diff --git a/help.go b/help.go index c039a5d02b..42e14a854d 100644 --- a/help.go +++ b/help.go @@ -44,6 +44,15 @@ var HelpPrinterCustom helpPrinterCustom = printHelpCustom // VersionPrinter prints the version for the App var VersionPrinter = printVersion +// ShowAppHelp is an action that displays the help +var ShowAppHelp = showAppHelp + +// ShowCommandHelp prints help for the given command +var ShowCommandHelp = showCommandHelp + +// ShowSubcommandHelp prints help for the given subcommand +var ShowSubcommandHelp = showSubcommandHelp + func buildHelpCommand(withAction bool) *Command { cmd := &Command{ Name: helpName, @@ -131,7 +140,7 @@ func ShowAppHelpAndExit(cmd *Command, exitCode int) { } // ShowAppHelp is an action that displays the help. -func ShowAppHelp(cmd *Command) error { +func showAppHelp(cmd *Command) error { tmpl := cmd.CustomRootCommandHelpTemplate if tmpl == "" { tracef("using RootCommandHelpTemplate") @@ -270,8 +279,7 @@ func ShowCommandHelpAndExit(ctx context.Context, cmd *Command, command string, c os.Exit(code) } -// ShowCommandHelp prints help for the given command -func ShowCommandHelp(ctx context.Context, cmd *Command, commandName string) error { +func showCommandHelp(ctx context.Context, cmd *Command, commandName string) error { for _, subCmd := range cmd.Commands { if !subCmd.HasName(commandName) { continue @@ -322,8 +330,7 @@ func ShowSubcommandHelpAndExit(cmd *Command, exitCode int) { os.Exit(exitCode) } -// ShowSubcommandHelp prints help for the given subcommand -func ShowSubcommandHelp(cmd *Command) error { +func showSubcommandHelp(cmd *Command) error { HelpPrinter(cmd.Root().Writer, SubcommandHelpTemplate, cmd) return nil } diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index edd42feb88..aa3ca2d73c 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -131,6 +131,15 @@ COPYRIGHT: cli.go uses text/template to render templates. You can render custom help text by setting this variable. +var ShowAppHelp = showAppHelp + ShowAppHelp is an action that displays the help + +var ShowCommandHelp = showCommandHelp + ShowCommandHelp prints help for the given command + +var ShowSubcommandHelp = showSubcommandHelp + ShowSubcommandHelp prints help for the given subcommand + var SubcommandHelpTemplate = `NAME: {{template "helpNameTemplate" .}} @@ -195,22 +204,13 @@ func HandleExitCoder(err error) This function is the default error-handling behavior for an App. -func ShowAppHelp(cmd *Command) error - ShowAppHelp is an action that displays the help. - func ShowAppHelpAndExit(cmd *Command, exitCode int) ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. -func ShowCommandHelp(ctx context.Context, cmd *Command, commandName string) error - ShowCommandHelp prints help for the given command - func ShowCommandHelpAndExit(ctx context.Context, cmd *Command, command string, code int) ShowCommandHelpAndExit - exits with code after showing help -func ShowSubcommandHelp(cmd *Command) error - ShowSubcommandHelp prints help for the given subcommand - func ShowSubcommandHelpAndExit(cmd *Command, exitCode int) ShowSubcommandHelpAndExit - Prints help for the given subcommand and exits with exit code. From 695e6b56dab7c97b48d9903662b6c3a8a1b993c4 Mon Sep 17 00:00:00 2001 From: Mohit Panchariya Date: Sat, 28 Jun 2025 14:21:22 +0530 Subject: [PATCH 04/13] Re-trigger CI From 2f6400c507559c7e3f06f3c79cd38eb351162cf2 Mon Sep 17 00:00:00 2001 From: Naveen Gogineni Date: Sun, 13 Jul 2025 20:27:06 -0400 Subject: [PATCH 05/13] Fix:(issue_2169) Allow trim space for string slice flags --- command_test.go | 3 +++ flag_slice_base.go | 15 ++++++++++++++- flag_test.go | 6 ++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/command_test.go b/command_test.go index 6df83cdb75..6a2c0998f2 100644 --- a/command_test.go +++ b/command_test.go @@ -4311,6 +4311,9 @@ func TestCommandReadArgsFromStdIn(t *testing.T) { }, &StringSliceFlag{ Name: "ssf", + Config: StringConfig{ + TrimSpace: true, + }, }, } diff --git a/flag_slice_base.go b/flag_slice_base.go index b97c4ff4f2..3e7b049ea7 100644 --- a/flag_slice_base.go +++ b/flag_slice_base.go @@ -47,8 +47,21 @@ func (i *SliceBase[T, C, VC]) Set(value string) error { return nil } + trimSpace := true + // hack. How do we know if we should trim spaces? + // it makes sense only for string slice flags which have + // an option to not trim spaces. So by default we trim spaces + // otherwise we let the underlying value type handle it. + var t T + if reflect.TypeOf(t).Kind() == reflect.String { + trimSpace = false + } + for _, s := range flagSplitMultiValues(value) { - if err := i.value.Set(strings.TrimSpace(s)); err != nil { + if trimSpace { + s = strings.TrimSpace(s) + } + if err := i.value.Set(s); err != nil { return err } *i.slice = append(*i.slice, i.value.Get().(T)) diff --git a/flag_test.go b/flag_test.go index f6b0578334..24c7411b43 100644 --- a/flag_test.go +++ b/flag_test.go @@ -350,6 +350,12 @@ func TestFlagsFromEnv(t *testing.T) { output: []string{"foo", "bar"}, fl: &StringSliceFlag{Name: "names", Sources: EnvVars("NAMES"), Config: StringConfig{TrimSpace: true}}, }, + { + name: "StringSliceFlag valid without TrimSpace", + input: "foo , bar ", + output: []string{"foo ", " bar "}, + fl: &StringSliceFlag{Name: "names", Sources: EnvVars("NAMES")}, + }, { name: "StringMapFlag valid", From 6e08c8041f0e54841f4c4107389965301c445686 Mon Sep 17 00:00:00 2001 From: Jonathan Llovet Date: Fri, 8 Aug 2025 21:26:05 -0400 Subject: [PATCH 06/13] Add installation instructions for gfmrun --- docs/CONTRIBUTING.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index b52a186b27..d16f5efd5a 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -66,6 +66,18 @@ verify one's changes are harmonious in nature. The same steps are also run durin [continuous integration phase](https://github.com/urfave/cli/blob/main/.github/workflows/test.yml). +`gfmrun` is required to run the examples, and without it `make all` may fail. + +You can find `gfmrun` here: + +- [urfave/gfmrun](https://github.com/urfave/gfmrun) + +To install `gfmrun`, you can use `go install`: + +``` +go install github.com/urfave/gfmrun/cmd/gfmrun@latest +``` + In the event that the `v3diff` target exits non-zero, this is a signal that the public API surface area has changed. If the changes are acceptable, then manually running the approval step will "promote" the current `go doc` output: From f069d9eeb9b325304c81fa908a6958314c916dc3 Mon Sep 17 00:00:00 2001 From: Jonathan Llovet Date: Fri, 8 Aug 2025 23:01:24 -0400 Subject: [PATCH 07/13] Add example of flag groups to docs --- docs/v3/examples/flags/advanced.md | 101 +++++++++++++++++++++++++++++ 1 file changed, 101 insertions(+) diff --git a/docs/v3/examples/flags/advanced.md b/docs/v3/examples/flags/advanced.md index f727217811..e5a5fff7e5 100644 --- a/docs/v3/examples/flags/advanced.md +++ b/docs/v3/examples/flags/advanced.md @@ -334,6 +334,107 @@ If the command is run without the `lang` flag, the user will see the following m Required flag "lang" not set ``` +#### Flag Groups + +You can make groups of flags that are mutually exclusive of each other. +This provides the ability to provide configuration options out of which +only one can be defined on the command line. + +Take for example this app that looks up a user using one of multiple options: + + +```go +package main + +import ( + "context" + "encoding/json" + "fmt" + "log" + "os" + + "github.com/urfave/cli/v3" +) + +func main() { + cmd := &cli.Command{ + Name: "authors", + MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{ + { + Required: true, + Flags: [][]cli.Flag{ + { + &cli.StringFlag{ + Name: "login", + Usage: "the username of the user", + }, + }, + { + &cli.StringFlag{ + Name: "id", + Usage: "the user id (defaults to 'me' for current user)", + }, + }, + }, + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + u, err := getUser(ctx, cmd) + if err != nil { + return err + } + data, err := json.Marshal(u) + if err != nil { + return err + } + fmt.Println(string(data)) + return nil + }, + } + + if err := cmd.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) + } +} + +type User struct { + Id string `json:"id"` + Login string `json:"login"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` +} + +// Mock function that returns a static user value. +// Would retrieve a user from an API or database with other functions. +func getUser(ctx context.Context, cmd *cli.Command) (User, error) { + u := User{ + Id: "abc123", + Login: "vwoolf@example.com", + FirstName: "Virginia", + LastName: "Woolf", + } + if login := cmd.String("login"); login != "" { + fmt.Printf("Getting user by login: %s\n", login) + u.Login = login + } + if id := cmd.String("id"); id != "" { + fmt.Printf("Getting user by id: %s\n", id) + u.Id = id + } + return u, nil +} +``` + +If the command is run without either the `login` or `id` flag, the user will +see the following message + +``` +one of these flags needs to be provided: login, id +``` + + #### Default Values for help output Sometimes it's useful to specify a flag's default help-text value within the From 47044b978a438e12a1b43dad93382a994b9222b2 Mon Sep 17 00:00:00 2001 From: dearchap Date: Sat, 9 Aug 2025 17:57:26 -0400 Subject: [PATCH 08/13] Update docs/CONTRIBUTING.md --- docs/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index d16f5efd5a..4cf546f2eb 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -66,7 +66,7 @@ verify one's changes are harmonious in nature. The same steps are also run durin [continuous integration phase](https://github.com/urfave/cli/blob/main/.github/workflows/test.yml). -`gfmrun` is required to run the examples, and without it `make all` may fail. +`gfmrun` is required to run the examples, and without it `make all` will fail. You can find `gfmrun` here: From c9237757d0d955749651659243db388a02a2ed11 Mon Sep 17 00:00:00 2001 From: Dan Buch Date: Mon, 11 Aug 2025 21:13:50 -0400 Subject: [PATCH 09/13] Ensure public vars reference public types and fix remaining "App" names to use "RootCommand" instead. --- command.go | 6 +- command_run.go | 6 +- docs/v3/examples/full-api-example.md | 4 +- docs/v3/examples/help/generated-help-text.md | 9 +- errors.go | 11 +- fish.go | 2 +- godoc-current.txt | 131 ++++++++++++------- help.go | 77 ++++++----- help_test.go | 74 +++++------ testdata/godoc-v3.x.txt | 131 ++++++++++++------- 10 files changed, 270 insertions(+), 181 deletions(-) diff --git a/command.go b/command.go index 541081a59c..d7b05637d2 100644 --- a/command.go +++ b/command.go @@ -85,9 +85,9 @@ type Command struct { Writer io.Writer `json:"-"` // ErrWriter writes error output ErrWriter io.Writer `json:"-"` - // ExitErrHandler processes any error encountered while running an App before - // it is returned to the caller. If no function is provided, HandleExitCoder - // is used as the default behavior. + // ExitErrHandler processes any error encountered while running a Command before it is + // returned to the caller. If no function is provided, HandleExitCoder is used as the + // default behavior. ExitErrHandler ExitErrHandlerFunc `json:"-"` // Other custom info Metadata map[string]interface{} `json:"metadata"` diff --git a/command_run.go b/command_run.go index 44ffc22349..6b2abc1b90 100644 --- a/command_run.go +++ b/command_run.go @@ -175,9 +175,9 @@ func (cmd *Command) run(ctx context.Context, osArgs []string) (_ context.Context } if !cmd.hideHelp() { if cmd.parent == nil { - tracef("running ShowAppHelp") - if err := ShowAppHelp(cmd); err != nil { - tracef("SILENTLY IGNORING ERROR running ShowAppHelp %[1]v (cmd=%[2]q)", err, cmd.Name) + tracef("running ShowRootCommandHelp") + if err := ShowRootCommandHelp(cmd); err != nil { + tracef("SILENTLY IGNORING ERROR running ShowRootCommandHelp %[1]v (cmd=%[2]q)", err, cmd.Name) } } else { tracef("running ShowCommandHelp with %[1]q", cmd.Name) diff --git a/docs/v3/examples/full-api-example.md b/docs/v3/examples/full-api-example.md index fb574141ae..9857e89826 100644 --- a/docs/v3/examples/full-api-example.md +++ b/docs/v3/examples/full-api-example.md @@ -185,9 +185,9 @@ func main() { return nil }, Action: func(ctx context.Context, cmd *cli.Command) error { - cli.DefaultAppComplete(ctx, cmd) + cli.DefaultRootCommandComplete(ctx, cmd) cli.HandleExitCoder(errors.New("not an exit coder, though")) - cli.ShowAppHelp(cmd) + cli.ShowRootCommandHelp(cmd) cli.ShowCommandHelp(ctx, cmd, "also-nope") cli.ShowSubcommandHelp(cmd) cli.ShowVersion(cmd) diff --git a/docs/v3/examples/help/generated-help-text.md b/docs/v3/examples/help/generated-help-text.md index 3f11b757a4..973a85756d 100644 --- a/docs/v3/examples/help/generated-help-text.md +++ b/docs/v3/examples/help/generated-help-text.md @@ -11,11 +11,10 @@ or subcommand, and break execution. #### Customization -All of the help text generation may be customized, and at multiple levels. The -templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and -`SubcommandHelpTemplate` which may be reassigned or augmented, and full override -is possible by assigning a compatible func to the `cli.HelpPrinter` variable, -e.g.: +All of the help text generation may be customized, and at multiple levels. The templates +are exposed as variables `RootCommandHelpTemplate`, `CommandHelpTemplate`, and +`SubcommandHelpTemplate` which may be reassigned or augmented, and full override is +possible by assigning a compatible func to the `cli.HelpPrinter` variable, e.g.: