Skip to content

fix: show a map flag's key-value separator in help - #2519

Open
10kartik wants to merge 3 commits into
urfave:mainfrom
10kartik:map-separator-help
Open

10kartik wants to merge 3 commits into
urfave:mainfrom
10kartik:map-separator-help

Conversation

@10kartik

@10kartik 10kartik commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

  • bug

What this PR does / why we need it:

With MapFlagKeyValueSeparator set, help still shows map flags with =:

cmd := &cli.Command{
	Name:                     "app",
	MapFlagKeyValueSeparator: ":",
	Flags: []cli.Flag{
		&cli.StringMapFlag{Name: "labels", Value: map[string]string{"env": "prod"}},
	},
}
--labels string=string [ --labels string=string ]	(default: env="prod")

The flag only parses env:prod, though. --labels env=prod fails with item "env=prod" is missing separator ":", and the help printed after that error still says string=string.

There were two causes. TypeName and MapBase.ToString always used the default =. And --help, or the help after a usage error, is shown before the post-parse loop that passes each flag the command's parsing config, so the flag didn't know the separator yet. Now:

  • multiValueParsingConfig.mapKeyValueSeparator() returns the configured separator or =. MapBase.Set, MapBase.ToString and TypeName all use it.
  • GetValue passes the flag's parsing config to the value it formats the default with.
  • run passes each flag its parsing config right after PreParse, so help sees it. set already passes the same config before parsing a value, so parsing doesn't change.

With the change, help shows --labels string:string [ --labels string:string ] (default: env:"prod").

Which issue(s) this PR fixes:

None, found while checking how map flags render in help.

Testing

  • New TestCommandMapKeyValueFlagSeparatorHelp covers --help on the root, --help on a subcommand that inherits the separator, and the help printed after a usage error. It fails on main.
  • make generate vet check-binary-size and make v3diff pass, with no API change. go test -race ./... passes, as do the tests with urfave_cli_no_template, urfave_cli_no_docs and urfave_cli_no_suggest.

The change and the test were written with Claude Code (AI).

Release Notes

Help now shows map flags with the command's MapFlagKeyValueSeparator instead of always "=".

🤖 Generated with Claude Code

With MapFlagKeyValueSeparator set, help still showed map flags with "=",
in the type name and in the default value. The flag only parses the
configured separator, so following the help failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@10kartik
10kartik requested a review from a team as a code owner November 1, 2026 14:36

@yxshee yxshee left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran this locally. The new test fails on main in all three cases and passes here, and go test ./..., gofmt and go vet are clean.

I also tried a few other ways of getting help with MapFlagKeyValueSeparator: ":" set on the root. app --help, app help, app sub --help and app sub -h all show string:string now. app help sub doesn't, though: the subcommand's own map flag still prints as --labels string=string [ --labels string=string ] (default: env="prod"). That path goes through DefaultShowCommandHelp, which renders subCmd without run ever reaching its flags, so the new line in command_run.go doesn't cover it.

Setting the config there before printing fixed it for me, for help sub and for help sub deep:

for _, f := range subCmd.Flags {
	subCmd.setMultiValueParsingConfig(f)
}

Adding {"app", "help", "sub"} to the cases in TestCommandMapKeyValueFlagSeparatorHelp catches it.

`app help sub` prints the subcommand's help without running it, so its
map flags still showed the default `=` separator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@10kartik

10kartik commented Nov 6, 2026

Copy link
Copy Markdown
Contributor Author

good catch, thanks for testing it that thoroughly. fixed in c4c9886, help sub now sets the config before printing, and I added it to the test cases.

@Juneezee Juneezee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! The bug is real: every path in the description and test shows = on main and the separator here.

One path still shows =. A command's map flag shows up in its subcommands' help, and app help sub deep prints it with =, because help never runs sub and the new loop only covers deep's own flags:

cli/help.go

Lines 337 to 340 in c4c9886

// subCmd is not run, so set the config its map flags show in help
for _, f := range subCmd.Flags {
subCmd.setMultiValueParsingConfig(f)
}

cmd := &cli.Command{
	Name:                     "app",
	MapFlagKeyValueSeparator: ":",
	Commands: []*cli.Command{{
		Name:     "sub",
		Flags:    []cli.Flag{&cli.StringMapFlag{Name: "labels", Value: map[string]string{"env": "prod"}}},
		Commands: []*cli.Command{{Name: "deep"}},
	}},
}
$ app sub deep --help
...
GLOBAL OPTIONS:
   --labels string:string [ --labels string:string ]  (default: env:"prod")
$ app help sub deep
...
GLOBAL OPTIONS:
   --labels string=string [ --labels string=string ]  (default: env="prod")

Setting the config for every command once, in setupCommandGraph, fixes this and can replace both the new line in run and the new loop in DefaultShowCommandHelp:

cli/command_setup.go

Lines 176 to 186 in 025d953

func (cmd *Command) setupCommandGraph() {
tracef("setting up command graph (cmd=%[1]q)", cmd.Name)
_ = cmd.Walk(func(sub *Command) error {
for _, subCmd := range sub.Commands {
subCmd.parent = sub
subCmd.setupSubcommand()
}
return nil
})
}

	_ = cmd.Walk(func(sub *Command) error {
		// help can show a command's flags before it runs, or without running
		// it, and a map flag's help shows the key-value separator
		for _, f := range sub.allFlags() {
			sub.setMultiValueParsingConfig(f)
		}
		for _, subCmd := range sub.Commands {

Walk visits a command before its subcommands, and their parent is set during that visit, so the inherited separator resolves. PreParse replaces a flag's value but keeps multiValueConfig, which TypeName and GetValue read.

This test change catches it. It fails on this branch and passes with the change above:

 		{"app", "help", "sub"},
+		{"app", "help", "sub", "deep"},
 		{"app", "--labels", "env=prod"},
...
-				Commands:                 []*Command{{Name: "sub", Flags: []Flag{labels()}}},
+				Commands:                 []*Command{{Name: "sub", Flags: []Flag{labels()}, Commands: []*Command{{Name: "deep"}}}},

With both changes on top of current main, go test -race ./..., the three build tags, go vet and golangci-lint pass.

Please also update the description: it doesn't mention the help sub fix from c4c9886, and its third bullet describes the line in run.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants