Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions cmd/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,13 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
}
}

// run and create register a local --help so -h can stay --hostname.
// That flag shadows the persistent one, which HandleGlobalFlags already
// consumed, so `docker --help run <image>` would start the container.
if ccmd := shadowedHelpCommand(cmd, args); ccmd != nil {
return ccmd.Help()
}

// This is a fallback for the case where the command does not exit
// based on context cancellation.
go forceExitAfter3TerminationSignals(ctx, dockerCli)
Expand All @@ -613,6 +620,30 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
return err
}

// shadowedHelpCommand returns the subcommand whose help should be printed
// when a global --help was parsed before the command name. Only commands
// that define their own --help flag need this; everyone else still inherits
// the persistent flag and cobra shows help on its own.
func shadowedHelpCommand(root *cobra.Command, args []string) *cobra.Command {
if root == nil || len(args) == 0 {
return nil
}
help, err := root.PersistentFlags().GetBool("help")
if err != nil || !help {
return nil
}
ccmd, _, err := root.Find(args)
if err != nil || ccmd == nil || ccmd == root {
return nil
}
local := ccmd.Flags().Lookup("help")
persistent := root.PersistentFlags().Lookup("help")
if local == nil || persistent == nil || local == persistent {
return nil
}
return ccmd
}

// hasCompletionArg returns true if a cobra completion arg request is found.
func hasCompletionArg(args []string) bool {
for _, arg := range args {
Expand Down
62 changes: 62 additions & 0 deletions cmd/docker/docker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,68 @@ func TestVersion(t *testing.T) {
assert.Check(t, is.Contains(b.String(), "Docker version"))
}

func TestShadowedHelpCommand(t *testing.T) {
parse := func(args ...string) (*cobra.Command, []string) {
t.Helper()
cli, err := command.NewDockerCli(command.WithBaseContext(context.TODO()))
assert.NilError(t, err)
tcmd := newDockerCommand(cli)
tcmd.SetArgs(args)
cmd, rest, err := tcmd.HandleGlobalFlags()
assert.NilError(t, err)
return cmd, rest
}

cmd, args := parse("--help", "run", "hello-world")
got := shadowedHelpCommand(cmd, args)
assert.Assert(t, got != nil)
assert.Check(t, is.Equal(got.Name(), "run"))

cmd, args = parse("--help", "create", "hello-world")
got = shadowedHelpCommand(cmd, args)
assert.Assert(t, got != nil)
assert.Check(t, is.Equal(got.Name(), "create"))

cmd, args = parse("--help", "container", "run", "hello-world")
got = shadowedHelpCommand(cmd, args)
assert.Assert(t, got != nil)
assert.Check(t, is.Equal(got.Name(), "run"))

// --help after the command name is still cobra's job.
cmd, args = parse("run", "--help")
assert.Assert(t, shadowedHelpCommand(cmd, args) == nil)

// inherited --help already works; don't steal it.
cmd, args = parse("--help", "images")
assert.Assert(t, shadowedHelpCommand(cmd, args) == nil)

// --help after the image is a container arg.
cmd, args = parse("run", "hello-world", "--help")
assert.Assert(t, shadowedHelpCommand(cmd, args) == nil)
}

func TestLeadingHelpDoesNotRunContainer(t *testing.T) {
var b bytes.Buffer
ctx := context.TODO()
cli, err := command.NewDockerCli(
command.WithBaseContext(ctx),
command.WithCombinedStreams(&b),
)
assert.NilError(t, err)

orig := os.Args
t.Cleanup(func() { os.Args = orig })

os.Args = []string{"docker", "--help", "run", "hello-world"}
assert.NilError(t, runDocker(ctx, cli))
assert.Check(t, is.Contains(b.String(), "Create and run a new container"))

b.Reset()
os.Args = []string{"docker", "--help", "create", "hello-world"}
assert.NilError(t, runDocker(ctx, cli))
assert.Check(t, is.Contains(b.String(), "Create a new container"))
}

func TestUserTerminatedError(t *testing.T) {
ctx, cancel := context.WithTimeoutCause(context.Background(), time.Second*1, errors.New("test timeout"))
t.Cleanup(cancel)
Expand Down
Loading