Skip to content

Commit bbb6311

Browse files
authored
Merge pull request #6871 from thaJeztah/completions_no_dups
cli/command/completion: don't provide duplicate completions
2 parents 48afa03 + a48ff6b commit bbb6311

File tree

2 files changed

+68
-13
lines changed

2 files changed

+68
-13
lines changed

cli/command/completion/functions.go

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type APIClientProvider interface {
2323

2424
// ImageNames offers completion for images present within the local store
2525
func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
26-
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
26+
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
2727
if limit > 0 && len(args) >= limit {
2828
return nil, cobra.ShellCompDirectiveNoFileComp
2929
}
@@ -36,14 +36,14 @@ func ImageNames(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
3636
names = append(names, img.RepoTags...)
3737
}
3838
return names, cobra.ShellCompDirectiveNoFileComp
39-
}
39+
})
4040
}
4141

4242
// ImageNamesWithBase offers completion for images present within the local store,
4343
// including both full image names with tags and base image names (repository names only)
4444
// when multiple tags exist for the same base name
4545
func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.CompletionFunc {
46-
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
46+
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
4747
if limit > 0 && len(args) >= limit {
4848
return nil, cobra.ShellCompDirectiveNoFileComp
4949
}
@@ -69,14 +69,14 @@ func ImageNamesWithBase(dockerCLI APIClientProvider, limit int) cobra.Completion
6969
}
7070
}
7171
return names, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp
72-
}
72+
})
7373
}
7474

7575
// ContainerNames offers completion for container names and IDs
7676
// By default, only names are returned.
7777
// Set DOCKER_COMPLETION_SHOW_CONTAINER_IDS=yes to also complete IDs.
7878
func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(container.Summary) bool) cobra.CompletionFunc {
79-
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
79+
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
8080
res, err := dockerCLI.Client().ContainerList(cmd.Context(), client.ContainerListOptions{
8181
All: all,
8282
})
@@ -104,12 +104,12 @@ func ContainerNames(dockerCLI APIClientProvider, all bool, filters ...func(conta
104104
names = append(names, formatter.StripNamePrefix(ctr.Names)...)
105105
}
106106
return names, cobra.ShellCompDirectiveNoFileComp
107-
}
107+
})
108108
}
109109

110110
// VolumeNames offers completion for volumes
111111
func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
112-
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
112+
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
113113
res, err := dockerCLI.Client().VolumeList(cmd.Context(), client.VolumeListOptions{})
114114
if err != nil {
115115
return nil, cobra.ShellCompDirectiveError
@@ -119,12 +119,12 @@ func VolumeNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
119119
names = append(names, vol.Name)
120120
}
121121
return names, cobra.ShellCompDirectiveNoFileComp
122-
}
122+
})
123123
}
124124

125125
// NetworkNames offers completion for networks
126126
func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
127-
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
127+
return Unique(func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
128128
res, err := dockerCLI.Client().NetworkList(cmd.Context(), client.NetworkListOptions{})
129129
if err != nil {
130130
return nil, cobra.ShellCompDirectiveError
@@ -134,7 +134,7 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
134134
names = append(names, nw.Name)
135135
}
136136
return names, cobra.ShellCompDirectiveNoFileComp
137-
}
137+
})
138138
}
139139

140140
// EnvVarNames offers completion for environment-variable names. This
@@ -151,20 +151,20 @@ func NetworkNames(dockerCLI APIClientProvider) cobra.CompletionFunc {
151151
// docker run --rm --env MY_VAR alpine printenv MY_VAR
152152
// hello
153153
func EnvVarNames() cobra.CompletionFunc {
154-
return func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
154+
return Unique(func(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
155155
envs := os.Environ()
156156
names = make([]string, 0, len(envs))
157157
for _, env := range envs {
158158
name, _, _ := strings.Cut(env, "=")
159159
names = append(names, name)
160160
}
161161
return names, cobra.ShellCompDirectiveNoFileComp
162-
}
162+
})
163163
}
164164

165165
// FromList offers completion for the given list of options.
166166
func FromList(options ...string) cobra.CompletionFunc {
167-
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
167+
return Unique(cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp))
168168
}
169169

170170
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
@@ -218,3 +218,38 @@ func Platforms() cobra.CompletionFunc {
218218
return commonPlatforms, cobra.ShellCompDirectiveNoFileComp
219219
}
220220
}
221+
222+
// Unique wraps a completion func and removes completion results that are
223+
// already consumed (i.e., appear in "args").
224+
//
225+
// For example:
226+
//
227+
// # initial completion: args is empty, so all results are shown
228+
// command <tab>
229+
// one two three
230+
//
231+
// # "one" is already used so omitted
232+
// command one <tab>
233+
// two three
234+
func Unique(fn cobra.CompletionFunc) cobra.CompletionFunc {
235+
return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
236+
all, dir := fn(cmd, args, toComplete)
237+
if len(all) == 0 || len(args) == 0 {
238+
return all, dir
239+
}
240+
241+
alreadyCompleted := make(map[string]struct{}, len(args))
242+
for _, a := range args {
243+
alreadyCompleted[a] = struct{}{}
244+
}
245+
246+
out := make([]string, 0, len(all))
247+
for _, c := range all {
248+
if _, ok := alreadyCompleted[c]; !ok {
249+
out = append(out, c)
250+
}
251+
}
252+
253+
return out, dir
254+
}
255+
}

cli/command/completion/functions_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,3 +351,23 @@ func TestCompleteVolumeNames(t *testing.T) {
351351
})
352352
}
353353
}
354+
355+
func TestUnique(t *testing.T) {
356+
base := []string{"alpha", "beta", "gamma"}
357+
358+
comp := Unique(func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
359+
return base, cobra.ShellCompDirectiveNoFileComp
360+
})
361+
362+
values, directives := comp(&cobra.Command{}, []string{"beta"}, "")
363+
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
364+
assert.Check(t, is.DeepEqual(values, []string{"alpha", "gamma"}))
365+
366+
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
367+
368+
values, directives = comp(&cobra.Command{}, []string{"gamma"}, "")
369+
assert.Check(t, is.Equal(directives&cobra.ShellCompDirectiveNoFileComp, cobra.ShellCompDirectiveNoFileComp))
370+
assert.Check(t, is.DeepEqual(values, []string{"alpha", "beta"}))
371+
372+
assert.Check(t, is.DeepEqual(base, []string{"alpha", "beta", "gamma"}))
373+
}

0 commit comments

Comments
 (0)