diff --git a/cli/command/completion/functions.go b/cli/command/completion/functions.go index f461d5befcdc..12e01de08f35 100644 --- a/cli/command/completion/functions.go +++ b/cli/command/completion/functions.go @@ -5,7 +5,6 @@ import ( "strings" "github.com/distribution/reference" - "github.com/docker/cli/cli/command/formatter" "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" "github.com/spf13/cobra" @@ -101,7 +100,13 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta if showContainerIDs { names = append(names, ctr.ID) } - names = append(names, formatter.StripNamePrefix(ctr.Names)...) + for _, n := range ctr.Names { + // Skip legacy link names: "/linked-container/link-name" + if len(n) <= 1 || strings.IndexByte(n[1:], '/') != -1 { + continue + } + names = append(names, strings.TrimPrefix(n, "/")) + } } return names, cobra.ShellCompDirectiveNoFileComp } diff --git a/cli/command/completion/functions_test.go b/cli/command/completion/functions_test.go index 27b52cc16bdd..86f10ef950a6 100644 --- a/cli/command/completion/functions_test.go +++ b/cli/command/completion/functions_test.go @@ -84,7 +84,7 @@ func TestCompleteContainerNames(t *testing.T) { {ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}}, {ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}}, }, - expOut: []string{"container-c", "container-c/link-b", "container-b", "container-a"}, + expOut: []string{"container-c", "container-b", "container-a"}, expOpts: client.ContainerListOptions{All: true}, expDirective: cobra.ShellCompDirectiveNoFileComp, }, @@ -97,7 +97,7 @@ func TestCompleteContainerNames(t *testing.T) { {ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}}, {ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}}, }, - expOut: []string{"id-c", "container-c", "container-c/link-b", "id-b", "container-b", "id-a", "container-a"}, + expOut: []string{"id-c", "container-c", "id-b", "container-b", "id-a", "container-a"}, expOpts: client.ContainerListOptions{All: true}, expDirective: cobra.ShellCompDirectiveNoFileComp, }, @@ -107,7 +107,7 @@ func TestCompleteContainerNames(t *testing.T) { containers: []container.Summary{ {ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}}, }, - expOut: []string{"container-c", "container-c/link-b"}, + expOut: []string{"container-c"}, expDirective: cobra.ShellCompDirectiveNoFileComp, }, { @@ -117,7 +117,7 @@ func TestCompleteContainerNames(t *testing.T) { func(ctr container.Summary) bool { return ctr.State == container.StateCreated }, }, containers: []container.Summary{ - {ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}}, + {ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}}, {ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}}, {ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}}, }, @@ -133,7 +133,7 @@ func TestCompleteContainerNames(t *testing.T) { func(ctr container.Summary) bool { return ctr.State == container.StateCreated }, }, containers: []container.Summary{ - {ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b"}}, + {ID: "id-c", State: container.StateRunning, Names: []string{"/container-c"}}, {ID: "id-b", State: container.StateCreated, Names: []string{"/container-b"}}, {ID: "id-a", State: container.StateCreated, Names: []string{"/container-a"}}, }, diff --git a/cli/command/container/completion.go b/cli/command/container/completion.go index 73ae61fa1b20..93b9b64433bf 100644 --- a/cli/command/container/completion.go +++ b/cli/command/container/completion.go @@ -182,6 +182,35 @@ func completeLink(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { } } +// completeLinks implements shell completion for the `--link` option of `rm --link`. +// +// It contacts the API to get names of legacy links on containers. +// In case of an error, an empty list is returned. +func completeLinks(dockerCLI completion.APIClientProvider) cobra.CompletionFunc { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{ + All: true, + }) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, ctr := range res.Items { + if len(ctr.Names) <= 1 { + // Container has no links names. + continue + } + for _, n := range ctr.Names { + // Skip legacy link names: "/linked-container/link-name" + if len(n) > 1 && strings.IndexByte(n[1:], '/') != -1 { + names = append(names, strings.TrimPrefix(n, "/")) + } + } + } + return names, cobra.ShellCompDirectiveNoFileComp + } +} + // completeLogDriver implements shell completion for the `--log-driver` option of `run` and `create`. // The log drivers are collected from a call to the Info endpoint with a fallback to a hard-coded list // of the build-in log drivers. diff --git a/cli/command/container/completion_test.go b/cli/command/container/completion_test.go index 21b20211d232..649e4701e890 100644 --- a/cli/command/container/completion_test.go +++ b/cli/command/container/completion_test.go @@ -135,3 +135,44 @@ func TestCompleteSignals(t *testing.T) { assert.Check(t, len(values) > 1) assert.Check(t, is.Len(values, len(signal.SignalMap))) } + +func TestCompleteLinks(t *testing.T) { + tests := []struct { + doc string + showAll, showIDs bool + filters []func(container.Summary) bool + containers []container.Summary + expOut []string + expDirective cobra.ShellCompDirective + }{ + { + doc: "no results", + expDirective: cobra.ShellCompDirectiveNoFileComp, + }, + { + doc: "all containers", + showAll: true, + containers: []container.Summary{ + {ID: "id-c", State: container.StateRunning, Names: []string{"/container-c", "/container-c/link-b", "/container-c/link-c"}}, + {ID: "id-b", State: container.StateCreated, Names: []string{"/container-b", "/container-b/link-a"}}, + {ID: "id-a", State: container.StateExited, Names: []string{"/container-a"}}, + }, + expOut: []string{"container-c/link-b", "container-c/link-c", "container-b/link-a"}, + expDirective: cobra.ShellCompDirectiveNoFileComp, + }, + } + + for _, tc := range tests { + t.Run(tc.doc, func(t *testing.T) { + comp := completeLinks(test.NewFakeCli(&fakeClient{ + containerListFunc: func(client.ContainerListOptions) (client.ContainerListResult, error) { + return client.ContainerListResult{Items: tc.containers}, nil + }, + })) + + containers, directives := comp(&cobra.Command{}, nil, "") + assert.Check(t, is.Equal(directives&tc.expDirective, tc.expDirective)) + assert.Check(t, is.DeepEqual(containers, tc.expOut)) + }) + } +} diff --git a/cli/command/container/rm.go b/cli/command/container/rm.go index 8251f2a9dd70..c38256a468b4 100644 --- a/cli/command/container/rm.go +++ b/cli/command/container/rm.go @@ -27,6 +27,11 @@ type rmOptions struct { func newRmCommand(dockerCLI command.Cli) *cobra.Command { var opts rmOptions + completeLinkNames := completeLinks(dockerCLI) + completeNames := completion.ContainerNames(dockerCLI, true, func(ctr container.Summary) bool { + return opts.force || ctr.State == container.StateExited || ctr.State == container.StateCreated + }) + cmd := &cobra.Command{ Use: "rm [OPTIONS] CONTAINER [CONTAINER...]", Short: "Remove one or more containers", @@ -38,9 +43,13 @@ func newRmCommand(dockerCLI command.Cli) *cobra.Command { Annotations: map[string]string{ "aliases": "docker container rm, docker container remove, docker rm", }, - ValidArgsFunction: completion.ContainerNames(dockerCLI, true, func(ctr container.Summary) bool { - return opts.force || ctr.State == container.StateExited || ctr.State == container.StateCreated - }), + ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if opts.rmLink { + // "--link" (remove link) is set; provide link names instead of container (primary) names. + return completeLinkNames(cmd, args, toComplete) + } + return completeNames(cmd, args, toComplete) + }, DisableFlagsInUseLine: true, }