diff --git a/cli/cobra.go b/cli/cobra.go index 9ded6b6e1d57..1847fc52ddcb 100644 --- a/cli/cobra.go +++ b/cli/cobra.go @@ -9,6 +9,7 @@ import ( "github.com/docker/cli/cli/command" cliconfig "github.com/docker/cli/cli/config" cliflags "github.com/docker/cli/cli/flags" + "github.com/docker/cli/contrib/completion" "github.com/moby/term" "github.com/morikuni/aec" "github.com/pkg/errors" @@ -151,10 +152,50 @@ func (tcmd *TopLevelCommand) HandleGlobalFlags() (*cobra.Command, []string, erro } return nil, nil, cmd.FlagErrorFunc()(cmd, err) } - + if err := tcmd.HandleCompletionFlag(flags); err != nil { + return nil, nil, err + } return cmd, flags.Args(), nil } +// HandleCompletionFlag prints the completion script if the --completion flag +// is set, after which it exits the application. If the root-command does not +// have a --completion flag, flags.Parse() should already have produced an error, +// and we won't reach this function, so no need to check if the flag exists. +func (tcmd *TopLevelCommand) HandleCompletionFlag(flags *pflag.FlagSet) error { + f := flags.Lookup("completion") + if f == nil || !f.Changed { + return nil + } + shell := f.Value.String() + cmd := tcmd.cmd.Root() + if shell == "" { + // handle --completion= (with trailing '='), which isn't caught by the default Flag error handling + return FlagErrorFunc(cmd, fmt.Errorf("flag needs an argument: --"+f.Name)) + } + + // Use cobra's automatically generated completion script for languages + // for which we don't provide a script. + switch shell { + case "bash-cobra": + _ = cmd.GenBashCompletion(cmd.OutOrStdout()) + case "fish-cobra": + _ = cmd.GenFishCompletion(cmd.OutOrStdout(), true) + case "powershell": + _ = cmd.GenPowerShellCompletionWithDesc(cmd.OutOrStdout()) + case "zsh-cobra": + _ = cmd.GenZshCompletion(cmd.OutOrStdout()) + default: + s, err := completion.Get(shell) + if err != nil { + return err + } + _, _ = fmt.Fprint(cmd.OutOrStdout(), s) + } + os.Exit(0) + return nil +} + // Initialize finalises global option parsing and initializes the docker client. func (tcmd *TopLevelCommand) Initialize(ops ...command.InitializeOpt) error { tcmd.opts.Common.SetDefaultOptions(tcmd.flags) diff --git a/cmd/docker/docker.go b/cmd/docker/docker.go index 90945f0c478d..79798b1d48c3 100644 --- a/cmd/docker/docker.go +++ b/cmd/docker/docker.go @@ -11,7 +11,6 @@ import ( pluginmanager "github.com/docker/cli/cli-plugins/manager" "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/commands" - cliflags "github.com/docker/cli/cli/flags" "github.com/docker/cli/cli/version" "github.com/docker/docker/api/types/versions" "github.com/docker/docker/client" @@ -27,12 +26,6 @@ var allowedAliases = map[string]struct{}{ } func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand { - var ( - opts *cliflags.ClientOptions - flags *pflag.FlagSet - helpCmd *cobra.Command - ) - cmd := &cobra.Command{ Use: "docker [OPTIONS] COMMAND [ARG...]", Short: "A self-sufficient runtime for containers", @@ -44,15 +37,32 @@ func newDockerCommand(dockerCli *command.DockerCli) *cli.TopLevelCommand { return command.ShowHelp(dockerCli.Err())(cmd, args) } return fmt.Errorf("docker: '%s' is not a docker command.\nSee 'docker --help'", args[0]) - }, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Handle shell completion for plugin commands by forwarding the + // arguments to the plugin. Supporting completion is currently + // optional for plugins, so we ignore errors (if any). + if len(args) > 0 && cmd.Name() == cobra.ShellCompRequestCmd || cmd.Name() == cobra.ShellCompNoDescRequestCmd { + if _, _, err := cmd.Root().Find(args); err != nil { + // No such command. Try if we have a plugin that has this command + if err := tryPluginRun(dockerCli, cmd, args[0]); err == nil { + // We're done. Exit here to prevent the regular command + // from being executed. + os.Exit(0) + } + // Didn't find a plugin for this command either. Continue as + // usual, and have the default __complete / __completeNoDesc + // command handle the completion. + return nil + } + } return isSupported(cmd, dockerCli) }, Version: fmt.Sprintf("%s, build %s", version.Version, version.GitCommit), DisableFlagsInUseLine: true, } - opts, flags, helpCmd = cli.SetupRootCommand(cmd) + opts, flags, helpCmd := cli.SetupRootCommand(cmd) + flags.String("completion", "", "Print the shell completion script for the specified shell (bash, fish, powershell, or zsh) and quit") flags.BoolP("version", "v", false, "Print version information and quit") setFlagErrorFunc(dockerCli, cmd) diff --git a/contrib/completion/bash/docker b/contrib/completion/bash/docker index da5fc0e7789d..68b07156c3ce 100644 --- a/contrib/completion/bash/docker +++ b/contrib/completion/bash/docker @@ -5486,11 +5486,8 @@ _docker_wait() { _docker_container_wait } -COMPOSE_PLUGIN_PATH=$(docker info --format '{{json .ClientInfo.Plugins}}' | sed -n 's/.*"Path":"\([^"]\+docker-compose\)".*/\1/p') - -_docker_compose() { - local completionCommand="__completeNoDesc" - local resultArray=($COMPOSE_PLUGIN_PATH $completionCommand compose) +__completeNoDesc() { + local resultArray=(docker __completeNoDesc $1) for value in "${words[@]:2}"; do if [ -z "$value" ]; then resultArray+=( "''" ) @@ -5503,6 +5500,18 @@ _docker_compose() { COMPREPLY=( $(compgen -W "${result}" -- "$current") ) } +_docker_buildx() { + __completeNoDesc buildx +} + +_docker_compose() { + __completeNoDesc compose +} + +_docker_scan() { + __completeNoDesc scan +} + _docker() { local previous_extglob_setting=$(shopt -p extglob) shopt -s extglob @@ -5572,11 +5581,8 @@ _docker() { wait ) - local known_plugin_commands=() - - if [ -f "$COMPOSE_PLUGIN_PATH" ] ; then - known_plugin_commands+=("compose") - fi + PLUGINS_INSTALLED=$(docker info --format '{{range .ClientInfo.Plugins}}{{ .Name }} {{end}}') + local known_plugin_commands=($PLUGINS_INSTALLED) local experimental_server_commands=( checkpoint diff --git a/contrib/completion/completion.go b/contrib/completion/completion.go new file mode 100644 index 000000000000..1836fad9e408 --- /dev/null +++ b/contrib/completion/completion.go @@ -0,0 +1,32 @@ +package completion + +import ( + _ "embed" // needed to make embed work + "errors" +) + +var ( + //go:embed bash/docker + completionBash string + + //go:embed fish/docker.fish + completionFish string + + //go:embed zsh/_docker + completionZsh string + + completions = map[string]string{ + "bash": completionBash, + "fish": completionFish, + "zsh": completionZsh, + } +) + +// Get returns the completion script for the given shell (bash, fish, or zsh). +func Get(shell string) (string, error) { + cs, ok := completions[shell] + if !ok { + return "", errors.New("no completion available for: " + shell) + } + return cs, nil +} diff --git a/docs/reference/commandline/cli.md b/docs/reference/commandline/cli.md index f8ad89b13f52..effc6aeef9dd 100644 --- a/docs/reference/commandline/cli.md +++ b/docs/reference/commandline/cli.md @@ -32,6 +32,7 @@ Usage: docker [OPTIONS] COMMAND [ARG...] A self-sufficient runtime for containers. Options: + --completion string Print the shell completion script for the specified shell (bash, fish, powershell, or zsh) and quit --config string Location of client config files (default "/root/.docker") -c, --context string Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use") -D, --debug Enable debug mode diff --git a/man/docker.1.md b/man/docker.1.md index a3a19ed934b5..afa4ee1b936d 100644 --- a/man/docker.1.md +++ b/man/docker.1.md @@ -20,6 +20,10 @@ To see the man page for a command run **man docker **. **--help** Print usage statement +**--completion**="" + Print the shell completion script for the specified shell (bash, fish, + powershell, or zsh) and quit. + **--config**="" Specifies the location of the Docker client configuration files. The default is '~/.docker'.