From 1c332c7ba93ad08dcf817e251854f3f6cdbdc481 Mon Sep 17 00:00:00 2001 From: Sebastiaan van Stijn Date: Sun, 15 Aug 2021 20:51:29 +0200 Subject: [PATCH] Add top-level --completion flag, and integrate completion for plugins This patch adds a new top-level `--completion` flag, which allows the user to print the completion script for their shell of choice. We currently maintain hand-written completion scripts for bash, fish, and zsh, of which the bash script is in a good state, but the fish and zsh scripts are less well maintained. We should review the quality of the fish and zsh scripts, and decide wether or not we want to use the hand-written versions for those, or to replace them with the auto-generated completion provided by Cobra. This patch embeds the hand-written completion scripts using Go 1.16's "embed" functionality, but also adds options to try/test/compare the Cobra completion scripts. Currently, this patch allows for the following: docker --completion=bash outputs the hand-written bash completion script docker --completion=bash-cobra outputs cobra's generated completion script docker --completion=fish outputs the hand-written fish completion script docker --completion=fish-cobra outputs cobra's generated completion script docker --completion=powershell outputs cobra's generated completion script docker --completion=zsh outputs the hand-written bash completion script docker --completion=zsh-cobra outputs cobra's generated completion script While the hand-written bash completion script has definitions for commands and options provided by the main CLI, it does not provide completions for commands and options provided by plug-ins. Commits 1148163c3ec90fc78b8f385e065984989b018e8b and 5a8d7d506cbbe7cd0df3ed3903cb27a90a51e142 added support for completions for the compose cli plug-in, but do so by directly calling the plug-in binary. Cobra's completion scripts use two hidden subcommands that provide dynamic generating of completion scripts: `__complete` and `__completeNoDesc`. This patch adds support for both these subcommands, and implements code to extend these commands to be plug-in aware: if either of those commands request completion for an unknown command, the cli will look if a plug-in is install that provides the command, and will call the `__complete` or `__completeNoDesc` command on the plug-in. With that, we are able to automatically provide completion scripts for all plug- ins. For example: docker __complete compose r restart Restart containers rm Removes stopped service containers run Run a one-off command on a service. :4 Completion ended with directive: ShellCompDirectiveNoFileComp docker __complete scan -- --accept-license Accept using a third party scanning provider --dependency-tree Show dependency tree with scan results --exclude-base Exclude base image from vulnerability scanning (requires --file) --file Dockerfile associated with image, provides more detailed results --file= Dockerfile associated with image, provides more detailed results --group-issues Aggregate duplicated vulnerabilities and group them to a single one (requires --json) --json Output results in JSON format --login Authenticate to the scan provider using an optional token (with --token), or web base token if empty --reject-license Reject using a third party scanning provider --severity Only report vulnerabilities of provided level or higher (low|medium|high) --severity= Only report vulnerabilities of provided level or higher (low|medium|high) --token Authentication token to login to the third party scanning provider --token= Authentication token to login to the third party scanning provider --version Display version of the scan plugin :0 Completion ended with directive: ShellCompDirectiveDefault docker __completeNoDesc scan -- --accept-license --dependency-tree --exclude-base --file --file= --group-issues --json --login --reject-license --severity --severity= --token --token= --version :0 Completion ended with directive: ShellCompDirectiveDefault Signed-off-by: Sebastiaan van Stijn --- cli/cobra.go | 43 ++++++++++++++++++++++++++++++- cmd/docker/docker.go | 28 +++++++++++++------- contrib/completion/bash/docker | 26 ++++++++++++------- contrib/completion/completion.go | 32 +++++++++++++++++++++++ docs/reference/commandline/cli.md | 1 + man/docker.1.md | 4 +++ 6 files changed, 114 insertions(+), 20 deletions(-) create mode 100644 contrib/completion/completion.go 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'.