From 965590b3785ee14a2065e999fa2a538f29bd59d3 Mon Sep 17 00:00:00 2001 From: Nick Sieger Date: Thu, 5 Nov 2026 09:52:46 -0600 Subject: [PATCH] fix: propagate leading --help to subcommands with a local help flag - --help before a subcommand is parsed as the root's persistent flag and stripped from args, which works for most subcommands since they share the same flag object with root - run/create register their own local help flag (no -h, to avoid a shorthand clash with --hostname), which shadows root's and never saw the value, so the container ran instead of printing usage - propagate the parsed value into a subcommand's local help flag when one shadows the root's, in HandleGlobalFlags Fixes #7473 Signed-off-by: Nick Sieger --- cli/cobra.go | 20 +++++++++++++++++++- cli/cobra_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) 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) + }) +}