Skip to content

Commit 33bf62c

Browse files
authoredJul 19, 2024··
Merge pull request #5261 from thaJeztah/27.1_backport_completion_enhancements
[27.0 backport] assorted fixes and enhancements for shell-completion
2 parents 7e972d2 + 66ec714 commit 33bf62c

File tree

18 files changed

+203
-45
lines changed

18 files changed

+203
-45
lines changed
 

‎Makefile

+10
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,16 @@ mod-outdated: ## check outdated dependencies
8686
authors: ## generate AUTHORS file from git history
8787
scripts/docs/generate-authors.sh
8888

89+
.PHONY: completion
90+
completion: binary
91+
completion: /etc/bash_completion.d/docker
92+
completion: ## generate and install the completion scripts
93+
94+
.PHONY: /etc/bash_completion.d/docker
95+
/etc/bash_completion.d/docker: ## generate and install the bash-completion script
96+
mkdir -p /etc/bash_completion.d
97+
docker completion bash > /etc/bash_completion.d/docker
98+
8999
.PHONY: manpages
90100
manpages: ## generate man pages from go source and markdown
91101
scripts/docs/generate-man.sh

‎cli/command/completion/functions.go

+36
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package completion
22

33
import (
44
"os"
5+
"strings"
56

67
"github.com/docker/cli/cli/command/formatter"
78
"github.com/docker/docker/api/types"
@@ -106,6 +107,41 @@ func NetworkNames(dockerCLI APIClientProvider) ValidArgsFn {
106107
}
107108
}
108109

110+
// EnvVarNames offers completion for environment-variable names. This
111+
// completion can be used for "--env" and "--build-arg" flags, which
112+
// allow obtaining the value of the given environment-variable if present
113+
// in the local environment, so we only should complete the names of the
114+
// environment variables, and not their value. This also prevents the
115+
// completion script from printing values of environment variables
116+
// containing sensitive values.
117+
//
118+
// For example;
119+
//
120+
// export MY_VAR=hello
121+
// docker run --rm --env MY_VAR alpine printenv MY_VAR
122+
// hello
123+
func EnvVarNames(_ *cobra.Command, _ []string, _ string) (names []string, _ cobra.ShellCompDirective) {
124+
envs := os.Environ()
125+
names = make([]string, 0, len(envs))
126+
for _, env := range envs {
127+
name, _, _ := strings.Cut(env, "=")
128+
names = append(names, name)
129+
}
130+
return names, cobra.ShellCompDirectiveNoFileComp
131+
}
132+
133+
// FromList offers completion for the given list of options.
134+
func FromList(options ...string) ValidArgsFn {
135+
return cobra.FixedCompletions(options, cobra.ShellCompDirectiveNoFileComp)
136+
}
137+
138+
// FileNames is a convenience function to use [cobra.ShellCompDirectiveDefault],
139+
// which indicates to let the shell perform its default behavior after
140+
// completions have been provided.
141+
func FileNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
142+
return nil, cobra.ShellCompDirectiveDefault
143+
}
144+
109145
// NoComplete is used for commands where there's no relevant completion
110146
func NoComplete(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
111147
return nil, cobra.ShellCompDirectiveNoFileComp

‎cli/command/container/completion.go

+94
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package container
2+
3+
import (
4+
"github.com/docker/cli/cli/command/completion"
5+
"github.com/docker/docker/api/types/container"
6+
"github.com/moby/sys/signal"
7+
"github.com/spf13/cobra"
8+
)
9+
10+
// allLinuxCapabilities is a list of all known Linux capabilities.
11+
//
12+
// This list was based on the containerd pkg/cap package;
13+
// https://github.com/containerd/containerd/blob/v1.7.19/pkg/cap/cap_linux.go#L133-L181
14+
//
15+
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
16+
var allLinuxCapabilities = []string{
17+
"ALL", // magic value for "all capabilities"
18+
19+
// caps35 is the caps of kernel 3.5 (37 entries)
20+
"CAP_CHOWN", // 2.2
21+
"CAP_DAC_OVERRIDE", // 2.2
22+
"CAP_DAC_READ_SEARCH", // 2.2
23+
"CAP_FOWNER", // 2.2
24+
"CAP_FSETID", // 2.2
25+
"CAP_KILL", // 2.2
26+
"CAP_SETGID", // 2.2
27+
"CAP_SETUID", // 2.2
28+
"CAP_SETPCAP", // 2.2
29+
"CAP_LINUX_IMMUTABLE", // 2.2
30+
"CAP_NET_BIND_SERVICE", // 2.2
31+
"CAP_NET_BROADCAST", // 2.2
32+
"CAP_NET_ADMIN", // 2.2
33+
"CAP_NET_RAW", // 2.2
34+
"CAP_IPC_LOCK", // 2.2
35+
"CAP_IPC_OWNER", // 2.2
36+
"CAP_SYS_MODULE", // 2.2
37+
"CAP_SYS_RAWIO", // 2.2
38+
"CAP_SYS_CHROOT", // 2.2
39+
"CAP_SYS_PTRACE", // 2.2
40+
"CAP_SYS_PACCT", // 2.2
41+
"CAP_SYS_ADMIN", // 2.2
42+
"CAP_SYS_BOOT", // 2.2
43+
"CAP_SYS_NICE", // 2.2
44+
"CAP_SYS_RESOURCE", // 2.2
45+
"CAP_SYS_TIME", // 2.2
46+
"CAP_SYS_TTY_CONFIG", // 2.2
47+
"CAP_MKNOD", // 2.4
48+
"CAP_LEASE", // 2.4
49+
"CAP_AUDIT_WRITE", // 2.6.11
50+
"CAP_AUDIT_CONTROL", // 2.6.11
51+
"CAP_SETFCAP", // 2.6.24
52+
"CAP_MAC_OVERRIDE", // 2.6.25
53+
"CAP_MAC_ADMIN", // 2.6.25
54+
"CAP_SYSLOG", // 2.6.37
55+
"CAP_WAKE_ALARM", // 3.0
56+
"CAP_BLOCK_SUSPEND", // 3.5
57+
58+
// caps316 is the caps of kernel 3.16 (38 entries)
59+
"CAP_AUDIT_READ",
60+
61+
// caps58 is the caps of kernel 5.8 (40 entries)
62+
"CAP_PERFMON",
63+
"CAP_BPF",
64+
65+
// caps59 is the caps of kernel 5.9 (41 entries)
66+
"CAP_CHECKPOINT_RESTORE",
67+
}
68+
69+
// restartPolicies is a list of all valid restart-policies..
70+
//
71+
// TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true")
72+
var restartPolicies = []string{
73+
string(container.RestartPolicyDisabled),
74+
string(container.RestartPolicyAlways),
75+
string(container.RestartPolicyOnFailure),
76+
string(container.RestartPolicyUnlessStopped),
77+
}
78+
79+
func completeLinuxCapabilityNames(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
80+
return completion.FromList(allLinuxCapabilities...)(cmd, args, toComplete)
81+
}
82+
83+
func completeRestartPolicies(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
84+
return completion.FromList(restartPolicies...)(cmd, args, toComplete)
85+
}
86+
87+
func completeSignals(cmd *cobra.Command, args []string, toComplete string) (names []string, _ cobra.ShellCompDirective) {
88+
// TODO(thaJeztah): do we want to provide the full list here, or a subset?
89+
signalNames := make([]string, 0, len(signal.SignalMap))
90+
for k := range signal.SignalMap {
91+
signalNames = append(signalNames, k)
92+
}
93+
return completion.FromList(signalNames...)(cmd, args, toComplete)
94+
}

‎cli/command/container/create.go

+10
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,16 @@ func NewCreateCommand(dockerCli command.Cli) *cobra.Command {
7777
command.AddPlatformFlag(flags, &options.platform)
7878
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
7979
copts = addFlags(flags)
80+
81+
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
82+
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
83+
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
84+
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
85+
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
86+
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
87+
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
88+
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
89+
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true))
8090
return cmd
8191
}
8292

‎cli/command/container/exec.go

+2-7
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66
"io"
7-
"os"
87

98
"github.com/docker/cli/cli"
109
"github.com/docker/cli/cli/command"
@@ -79,12 +78,8 @@ func NewExecCommand(dockerCli command.Cli) *cobra.Command {
7978
flags.StringVarP(&options.Workdir, "workdir", "w", "", "Working directory inside the container")
8079
flags.SetAnnotation("workdir", "version", []string{"1.35"})
8180

82-
_ = cmd.RegisterFlagCompletionFunc("env", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
83-
return os.Environ(), cobra.ShellCompDirectiveNoFileComp
84-
})
85-
_ = cmd.RegisterFlagCompletionFunc("env-file", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
86-
return nil, cobra.ShellCompDirectiveDefault // _filedir
87-
})
81+
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
82+
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
8883

8984
return cmd
9085
}

‎cli/command/container/kill.go

+4-1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ func NewKillCommand(dockerCli command.Cli) *cobra.Command {
3838

3939
flags := cmd.Flags()
4040
flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container")
41+
42+
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
43+
4144
return cmd
4245
}
4346

@@ -50,7 +53,7 @@ func runKill(ctx context.Context, dockerCli command.Cli, opts *killOptions) erro
5053
if err := <-errChan; err != nil {
5154
errs = append(errs, err.Error())
5255
} else {
53-
fmt.Fprintln(dockerCli.Out(), name)
56+
_, _ = fmt.Fprintln(dockerCli.Out(), name)
5457
}
5558
}
5659
if len(errs) > 0 {

‎cli/command/container/restart.go

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ func NewRestartCommand(dockerCli command.Cli) *cobra.Command {
4343
flags := cmd.Flags()
4444
flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container")
4545
flags.IntVarP(&opts.timeout, "time", "t", 0, "Seconds to wait before killing the container")
46+
47+
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
48+
4649
return cmd
4750
}
4851

‎cli/command/container/run.go

+9-17
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"context"
55
"fmt"
66
"io"
7-
"os"
87
"strings"
98
"syscall"
109

@@ -70,22 +69,15 @@ func NewRunCommand(dockerCli command.Cli) *cobra.Command {
7069
command.AddTrustVerificationFlags(flags, &options.untrusted, dockerCli.ContentTrustEnabled())
7170
copts = addFlags(flags)
7271

73-
cmd.RegisterFlagCompletionFunc(
74-
"env",
75-
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
76-
return os.Environ(), cobra.ShellCompDirectiveNoFileComp
77-
},
78-
)
79-
cmd.RegisterFlagCompletionFunc(
80-
"env-file",
81-
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
82-
return nil, cobra.ShellCompDirectiveDefault
83-
},
84-
)
85-
cmd.RegisterFlagCompletionFunc(
86-
"network",
87-
completion.NetworkNames(dockerCli),
88-
)
72+
_ = cmd.RegisterFlagCompletionFunc("cap-add", completeLinuxCapabilityNames)
73+
_ = cmd.RegisterFlagCompletionFunc("cap-drop", completeLinuxCapabilityNames)
74+
_ = cmd.RegisterFlagCompletionFunc("env", completion.EnvVarNames)
75+
_ = cmd.RegisterFlagCompletionFunc("env-file", completion.FileNames)
76+
_ = cmd.RegisterFlagCompletionFunc("network", completion.NetworkNames(dockerCli))
77+
_ = cmd.RegisterFlagCompletionFunc("pull", completion.FromList(PullImageAlways, PullImageMissing, PullImageNever))
78+
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
79+
_ = cmd.RegisterFlagCompletionFunc("stop-signal", completeSignals)
80+
_ = cmd.RegisterFlagCompletionFunc("volumes-from", completion.ContainerNames(dockerCli, true))
8981
return cmd
9082
}
9183

‎cli/command/container/stop.go

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ func NewStopCommand(dockerCli command.Cli) *cobra.Command {
4343
flags := cmd.Flags()
4444
flags.StringVarP(&opts.signal, "signal", "s", "", "Signal to send to the container")
4545
flags.IntVarP(&opts.timeout, "time", "t", 0, "Seconds to wait before killing the container")
46+
47+
_ = cmd.RegisterFlagCompletionFunc("signal", completeSignals)
48+
4649
return cmd
4750
}
4851

‎cli/command/container/update.go

+2
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ func NewUpdateCommand(dockerCli command.Cli) *cobra.Command {
8383
flags.Var(&options.cpus, "cpus", "Number of CPUs")
8484
flags.SetAnnotation("cpus", "version", []string{"1.29"})
8585

86+
_ = cmd.RegisterFlagCompletionFunc("restart", completeRestartPolicies)
87+
8688
return cmd
8789
}
8890

‎cli/command/image/history.go

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/docker/cli/cli"
77
"github.com/docker/cli/cli/command"
8+
"github.com/docker/cli/cli/command/completion"
89
"github.com/docker/cli/cli/command/formatter"
910
flagsHelper "github.com/docker/cli/cli/flags"
1011
"github.com/spf13/cobra"
@@ -31,6 +32,7 @@ func NewHistoryCommand(dockerCli command.Cli) *cobra.Command {
3132
opts.image = args[0]
3233
return runHistory(cmd.Context(), dockerCli, opts)
3334
},
35+
ValidArgsFunction: completion.ImageNames(dockerCli),
3436
Annotations: map[string]string{
3537
"aliases": "docker image history, docker history",
3638
},

‎cli/command/image/inspect.go

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88

99
"github.com/docker/cli/cli"
1010
"github.com/docker/cli/cli/command"
11+
"github.com/docker/cli/cli/command/completion"
1112
"github.com/docker/cli/cli/command/inspect"
1213
flagsHelper "github.com/docker/cli/cli/flags"
1314
"github.com/spf13/cobra"
@@ -30,6 +31,7 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
3031
opts.refs = args
3132
return runInspect(cmd.Context(), dockerCli, opts)
3233
},
34+
ValidArgsFunction: completion.ImageNames(dockerCli),
3335
}
3436

3537
flags := cmd.Flags()

‎cli/command/image/remove.go

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/docker/cli/cli"
99
"github.com/docker/cli/cli/command"
10+
"github.com/docker/cli/cli/command/completion"
1011
"github.com/docker/docker/api/types/image"
1112
"github.com/docker/docker/errdefs"
1213
"github.com/pkg/errors"
@@ -29,6 +30,7 @@ func NewRemoveCommand(dockerCli command.Cli) *cobra.Command {
2930
RunE: func(cmd *cobra.Command, args []string) error {
3031
return runRemove(cmd.Context(), dockerCli, opts, args)
3132
},
33+
ValidArgsFunction: completion.ImageNames(dockerCli),
3234
Annotations: map[string]string{
3335
"aliases": "docker image rm, docker image remove, docker rmi",
3436
},

‎cli/context/store/store.go

+3
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,9 @@ func (s *ContextStore) List() ([]Metadata, error) {
124124

125125
// Names return Metadata names for a Lister
126126
func Names(s Lister) ([]string, error) {
127+
if s == nil {
128+
return nil, errors.New("nil lister")
129+
}
127130
list, err := s.List()
128131
if err != nil {
129132
return nil, err

‎cli/context/store/store_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,9 @@ func TestCorruptMetadata(t *testing.T) {
260260
_, err = s.GetMetadata("source")
261261
assert.ErrorContains(t, err, fmt.Sprintf("parsing %s: unexpected end of JSON input", contextFile))
262262
}
263+
264+
func TestNames(t *testing.T) {
265+
names, err := Names(nil)
266+
assert.Check(t, is.Error(err, "nil lister"))
267+
assert.Check(t, is.Len(names, 0))
268+
}

‎cmd/docker/completions.go

+11-18
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,24 @@
11
package main
22

33
import (
4+
"github.com/docker/cli/cli/command/completion"
45
"github.com/docker/cli/cli/context/store"
56
"github.com/spf13/cobra"
67
)
78

8-
func registerCompletionFuncForGlobalFlags(contextStore store.Store, cmd *cobra.Command) error {
9-
err := cmd.RegisterFlagCompletionFunc(
10-
"context",
11-
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
12-
names, err := store.Names(contextStore)
13-
if err != nil {
14-
return nil, cobra.ShellCompDirectiveError
15-
}
16-
return names, cobra.ShellCompDirectiveNoFileComp
17-
},
18-
)
9+
type contextStoreProvider interface {
10+
ContextStore() store.Store
11+
}
12+
13+
func registerCompletionFuncForGlobalFlags(dockerCLI contextStoreProvider, cmd *cobra.Command) error {
14+
err := cmd.RegisterFlagCompletionFunc("context", func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
15+
names, _ := store.Names(dockerCLI.ContextStore())
16+
return names, cobra.ShellCompDirectiveNoFileComp
17+
})
1918
if err != nil {
2019
return err
2120
}
22-
err = cmd.RegisterFlagCompletionFunc(
23-
"log-level",
24-
func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
25-
values := []string{"debug", "info", "warn", "error", "fatal"}
26-
return values, cobra.ShellCompDirectiveNoFileComp
27-
},
28-
)
21+
err = cmd.RegisterFlagCompletionFunc("log-level", completion.FromList("debug", "info", "warn", "error", "fatal"))
2922
if err != nil {
3023
return err
3124
}

‎cmd/docker/docker.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand {
102102
cmd.SetErr(dockerCli.Err())
103103

104104
opts, helpCmd = cli.SetupRootCommand(cmd)
105-
_ = registerCompletionFuncForGlobalFlags(dockerCli.ContextStore(), cmd)
105+
_ = registerCompletionFuncForGlobalFlags(dockerCli, cmd)
106106
cmd.Flags().BoolP("version", "v", false, "Print version information and quit")
107107
setFlagErrorFunc(dockerCli, cmd)
108108

‎dockerfiles/Dockerfile.dev

+3-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
3535
FROM golang AS dev
3636
RUN apk add --no-cache \
3737
bash \
38+
bash-completion \
3839
build-base \
3940
ca-certificates \
4041
coreutils \
@@ -44,7 +45,8 @@ RUN apk add --no-cache \
4445
nano
4546

4647
RUN echo -e "\nYou are now in a development container. Run '\e\033[1mmake help\e\033[0m' to learn about\navailable make targets.\n" > /etc/motd \
47-
&& echo -e "cat /etc/motd\nPS1=\"\e[0;32m\u@docker-cli-dev\\$ \e[0m\"" >> /root/.bashrc
48+
&& echo -e "cat /etc/motd\nPS1=\"\e[0;32m\u@docker-cli-dev\\$ \e[0m\"" >> /root/.bashrc \
49+
&& echo -e "source /etc/bash/bash_completion.sh" >> /root/.bashrc
4850
CMD bash
4951
ENV DISABLE_WARN_OUTSIDE_CONTAINER=1
5052
ENV PATH=$PATH:/go/src/github.com/docker/cli/build

0 commit comments

Comments
 (0)
Please sign in to comment.