diff --git a/cli/cobra.go b/cli/cobra.go index bfcb1bdcc8..9ef121f810 100644 --- a/cli/cobra.go +++ b/cli/cobra.go @@ -160,7 +160,25 @@ func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, erro return nil, nil, cmd.FlagErrorFunc()(cmd, err) } - return cmd, flags.Args(), nil + args := flags.Args() + + // A leading "--help"/"-h" is consumed above as the root's persistent + // help flag and stripped from args, so the target subcommand never sees + // it. That's fine for most subcommands, which inherit the same help + // flag instance, but "run" and "create" register their own local + // "help" flag (to avoid a shorthand clash with "-h"/"--hostname"), + // which shadows the persistent one and is left false. Propagate the + // value so "docker --help run " shows help like "docker run + // --help" does, instead of running the container. + if help, _ := flags.GetBool("help"); help { + if sub, _, err := cmd.Find(args); err == nil && sub != cmd { + if sub.Flags().Lookup("help") != nil { + _ = sub.Flags().Set("help", "true") + } + } + } + + return cmd, args, nil } // Initialize finalises global option parsing and initializes the docker client. diff --git a/cli/cobra_test.go b/cli/cobra_test.go index eb87c75d41..5b0b32b8a5 100644 --- a/cli/cobra_test.go +++ b/cli/cobra_test.go @@ -108,3 +108,45 @@ func TestDecoratedName(t *testing.T) { topLevelCommand.Annotations = map[string]string{metadata.CommandAnnotationPlugin: "true"} assert.Equal(t, decoratedName(topLevelCommand), "pluginTopLevelCommand*") } + +// TestHandleGlobalFlagsHelpPropagation covers https://github.com/docker/cli/issues/7473: +// a leading "--help" is consumed as the root's persistent help flag and +// stripped from args, so a subcommand that registers its own local "help" +// flag (as "run" and "create" do, to avoid a shorthand clash with "-h") never +// sees it and runs instead of showing help. +func TestHandleGlobalFlagsHelpPropagation(t *testing.T) { + newRootCmd := func() (root, sub *cobra.Command) { + root = &cobra.Command{Use: "docker"} + root.PersistentFlags().BoolP("help", "h", false, "Print usage") + + sub = &cobra.Command{Use: "run", RunE: func(cmd *cobra.Command, args []string) error { return nil }} + sub.Flags().Bool("help", false, "Print usage") + root.AddCommand(sub) + return root, sub + } + + t.Run("propagates to the shadowing local help flag", func(t *testing.T) { + root, sub := newRootCmd() + tcmd := &TopLevelCommand{cmd: root, args: []string{"--help", "run", "hello-world"}} + + _, args, err := tcmd.HandleGlobalFlags() + assert.NilError(t, err) + assert.DeepEqual(t, args, []string{"run", "hello-world"}) + + help, err := sub.Flags().GetBool("help") + assert.NilError(t, err) + assert.Equal(t, help, true) + }) + + t.Run("leaves the local help flag alone without --help", func(t *testing.T) { + root, sub := newRootCmd() + tcmd := &TopLevelCommand{cmd: root, args: []string{"run", "hello-world"}} + + _, _, err := tcmd.HandleGlobalFlags() + assert.NilError(t, err) + + help, err := sub.Flags().GetBool("help") + assert.NilError(t, err) + assert.Equal(t, help, false) + }) +}