From df57ff720132cead0da9292e2e73b95ff8fe38c7 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 20 Mar 2026 09:10:47 +0100 Subject: [PATCH 1/2] cli/command/completion: ContainerNames: skip legacy link names Inline StripNamePrefix and skip legacy links for completion. Legacy links can be removed from a container, but only when using `docker [container] rm --link `. When linking containers through legacy links, a container can get multiple names; its own name, and a name for each link it's providing: # create two containers with links between them docker run -d --name one nginx:alpine docker run -d --name two --link one:link1 --link one:link2 --link one:link3 nginx:alpine # container "one" now has multiple names docker ps --no-trunc --format '{{.Names}}' two one,two/link1,two/link2,two/link3 # running `docker rm --link` with a link-name removes a link: docker rm --link two/link3 docker ps --no-trunc --format '{{.Names}}' two one,two/link1,two/link2 # but without `--link`, it resolves the linked container and removes it: docker rm -fv two/link2 two/link2 docker ps --no-trunc --format '{{.Names}}' two Legacy links are deprecated, and this can be confusing, so let's not provide completion for secondary names. Signed-off-by: Sebastiaan van Stijn --- cli/command/completion/functions.go | 9 +++++++-- cli/command/completion/functions_test.go | 10 +++++----- 2 files changed, 12 insertions(+), 7 deletions(-) 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"}}, }, From 9c117d3c5d28c115366a7dcd848eab1920702cdf Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Fri, 20 Mar 2026 17:04:29 +0100 Subject: [PATCH 2/2] cli/command/container: add shell completion for docker rm --link When linking containers through legacy links, a container can get multiple names; its own name, and a name for each link it's providing: # create two containers with links between them docker run -d --name one nginx:alpine docker run -d --name two --link one:link1 --link one:link2 --link one:link3 nginx:alpine docker rm --link two/link1 two/link2 two/link3 Signed-off-by: Sebastiaan van Stijn --- cli/command/container/completion.go | 29 +++++++++++++++++ cli/command/container/completion_test.go | 41 ++++++++++++++++++++++++ cli/command/container/rm.go | 15 +++++++-- 3 files changed, 82 insertions(+), 3 deletions(-) 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, }