Skip to content

Conversation

@thaJeztah
Copy link
Member

@thaJeztah thaJeztah commented Aug 15, 2021

fixes #1257

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 1148163
and 5a8d7d5 (#3158) 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

- What I did

- How I did it

- How to verify it

- Description for the changelog

- A picture of a cute animal (not mandatory but encouraged)

_docker_compose() {
local completionCommand="__completeNoDesc"
local resultArray=($COMPOSE_PLUGIN_PATH $completionCommand compose)
__completeNoDesc() {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to check if I updated this correctly. The intent is to:

  • change /usr/local/lib/docker/cli-plugins/docker-compose __completeNoDesc <command> to docker __completeNoDesc <command>

  • the same should now work for all plugins, so, we should be able to just use all plugins:

      docker info --format '{{range .ClientInfo.Plugins}}{{ .Name }} {{end}}'
      buildx compose scan
    

@thaJeztah thaJeztah force-pushed the add_completion branch 3 times, most recently from 1ab865b to 4ae8ae1 Compare August 15, 2021 20:02
@thaJeztah
Copy link
Member Author

@codecov-commenter
Copy link

codecov-commenter commented Aug 15, 2021

Codecov Report

Merging #3254 (1c332c7) into master (304a2dc) will decrease coverage by 0.09%.
The diff coverage is 6.06%.

@@            Coverage Diff             @@
##           master    #3254      +/-   ##
==========================================
- Coverage   57.99%   57.90%   -0.10%     
==========================================
  Files         302      302              
  Lines       21748    21774      +26     
==========================================
- Hits        12613    12608       -5     
- Misses       8212     8242      +30     
- Partials      923      924       +1     

@thaJeztah thaJeztah force-pushed the add_completion branch 2 times, most recently from 990d54e to f41bfc8 Compare August 16, 2021 09:29
COMPREPLY=( $(compgen -W "${result}" -- "$current") )
}

_docker_compose() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: _docker_compose _docker_buildx

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doh!

Still need to verify if this works as expected as well (didn't try yet TBH, as I ran out of steam 😅)

if [ -f "$COMPOSE_PLUGIN_PATH" ] ; then
known_plugin_commands+=("compose")
fi
PLUGINS_INSTALLED=$(docker info --format '{{range .ClientInfo.Plugins}}{{ .Name }} {{end}}')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: shouldn't we have a curated list here instead?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. Every plugin should have the posibility to hook into completion.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps; otoh, if the plugin is there, adding completion for it doesn't really harm I guess.
We should consider having a v0.0.2 version of the metadata that indicates that the plugin supports the __complete subcommands (although current code should handle it gracefully).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually; currently that won't work, because we have the dedicated _docker_<plugin name> functions 🤦

}
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")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: we mention powershell here but it's not in the completion map in completion.go. I guess it would fail then ? 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok my bad, powershell is fully generated by cobra 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it falls back to the auto-generated Cobra completion

COMPREPLY=( $(compgen -W "${result}" -- "$current") )
}

_docker_compose() {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be generic.
Assuming that a plugin contributes only one top level command, and that this command equals its name, we can check whether the command is part of $PLUGINS_INSTALLED right before calling

local completions_func=_docker_${command//-/_}
and if so, call a generic plugin completion instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks! Yes, I was assuming something like that was possible. That said, plugins do provide subcommands, in which case it should call (e.g. for docker buildx imagetools)

(looks like a "" is needed to force it to look up subcommands);

docker __complete buildx imagetools ""
create	Create a new image based on source images
inspect	Show details of image in the registry
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

Or, for flags (--);

docker __complete buildx imagetools --
--builder	Override the configured builder instance
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a big fan of a generic plugin-completion solution. That being said, I think care is needed here.

The $PLUGINS_INSTALLED list returns all cli plugins, not just the ones provided by docker. I don't think it would be right to call docker __completeNoDesc <anyPlugin> as those plugins may not support the __completeNoDesc command and may instead simply ignore it and execute whatever functionality they normally perform.

This was already hinted at by @thaJeztah earlier with the idea of "a v0.0.2 version of the metadata that indicates that the plugin supports the __complete subcommands" (#3254 (comment)), so sorry if I'm repeating things; I thought it was worth mentioning as it has been an important point in the discussion about how to support plugin completion for kubectl.

@albers
Copy link
Collaborator

albers commented Aug 16, 2021

I'd vote for a dedicated command for this functionality.

In my understanding, an option should only modify how a given command works, but not switch to conceptually different operation modes (disguised commands).

There are some well-known exceptions like --help and --version.
I don't feel --completion falls into this category. This is emphasized by the fact that the option needs a parameter in order to work.

Examples for similar commands that have dedicated completion commands:

  • vagrant autocomplete
  • kubectl completion bash
  • minikube completion bash

@thaJeztah
Copy link
Member Author

I'd vote for a dedicated command for this functionality.

I was a bit on the fence on this one, being that this (in most cases) will be a "set and forget" option (users would either use it once write to a file, or set it in their .profile or .bashrc). Making it a command would make it show up more prominently in the --help output (haven't tried, but probably it shows up under Management commands, but we can of course change that).

The kubectl completion uses the default Cobra functionality, which indeed is named completion (that said, we don't have to follow their naming, so we could name it anything)

@thaJeztah
Copy link
Member Author

Hmm... one thing to look into; it looks like the code doesn't match up;

When calling the plugin directly:

/usr/local/lib/docker/cli-plugins/docker-buildx __complete build
build	Start a build
:0
Completion ended with directive: ShellCompDirectiveDefault

When calling it through the cli:

./build/docker __complete buildx build
build	Start a build
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

Where :4 (ShellCompDirectiveNoFileComp) is documented as:

// ShellCompDirectiveNoFileComp indicates that the shell should not provide
// file completion even when no completion is provided.
// This currently does not work for zsh or bash < 4
ShellCompDirectiveNoFileComp

And :0 (ShellCompDirectiveDefault) is documented as:

// ShellCompDirectiveDefault indicates to let the shell perform its default
// behavior after completions have been provided.
// This one must be last to avoid messing up the iota count.
ShellCompDirectiveDefault ShellCompDirective = 0

From the above, I think it should return :0, otherwise it would not do further completions?

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 1148163
and 5a8d7d5 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 <github@gone.nl>
// 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 {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine, but FYI, the "__completeNoDesc" form is an alias of the same Cobra command (__complete). This means that in both cases cmd.Name() will be cobra.ShellCompRequestCmd, so you could simply check for that.

local completionCommand="__completeNoDesc"
local resultArray=($COMPOSE_PLUGIN_PATH $completionCommand compose)
__completeNoDesc() {
local resultArray=(docker __completeNoDesc $1)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of hard-coding docker you may want to use ${words[0]} here.
This will call the docker binary that the user is actually using. For example:

./build/docker buildx s[tab][tab]

would call ./build/docker __completeNoDesc buildx s instead of the docker __completeNoDesc buildx s, which I believe is what the user would want.

done
local result=$(eval "${resultArray[*]}" 2> /dev/null | grep -v '^:[0-9]*$')

COMPREPLY=( $(compgen -W "${result}" -- "$current") )

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using grep above is risky since this is a sourced script and therefore will take whatever grep alias may be in the user's environment. My grep alias prefixes the output with the line number (grep -n) which messes up completion.

I've had the same issue in my completion scripts 😄
You can use \grep, but it would be even better to use bash constructs to be more portable.
In Cobra, we use

# Remove the directive
result=${result%:*}

COMPREPLY=( $(compgen -W "${result}" -- "$current") )
}

_docker_compose() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a big fan of a generic plugin-completion solution. That being said, I think care is needed here.

The $PLUGINS_INSTALLED list returns all cli plugins, not just the ones provided by docker. I don't think it would be right to call docker __completeNoDesc <anyPlugin> as those plugins may not support the __completeNoDesc command and may instead simply ignore it and execute whatever functionality they normally perform.

This was already hinted at by @thaJeztah earlier with the idea of "a v0.0.2 version of the metadata that indicates that the plugin supports the __complete subcommands" (#3254 (comment)), so sorry if I'm repeating things; I thought it was worth mentioning as it has been an important point in the discussion about how to support plugin completion for kubectl.

@albers
Copy link
Collaborator

albers commented Feb 27, 2022

I don't think it would be right to call docker __completeNoDesc as those plugins may not support the __completeNoDesc command and may instead simply ignore it and execute whatever functionality they normally perform.

I think it should be sufficient to check the exit code of the __complete call and if it is not 0 then discard the plugin's output.

plugin with completion support

$ /usr/libexec/docker/cli-plugins/docker-scan __complete --c
--config	Location of client config files
--context	Name of the context to use to connect to the daemon (overrides DOCKER_HOST env var and default context set with "docker context use")
:4
Completion ended with directive: ShellCompDirectiveNoFileComp
$ echo $?
0

plugin without completion support

$ /usr/libexec/docker/cli-plugins/docker-app __complete --c
unknown flag: --c
See 'docker --help'.
[...]
$ echo $?
125

@marckhouzam
Copy link

I think it should be sufficient to check the exit code of the __complete call and if it is not 0 then discard the plugin's output.

We started with a similar point in the kubectl discussion but after iterating we realized it was problematic. Say a docker-cli plugin is a simple bash script. One could imagine a docker-prune plugin like:

#!/bin/bash

# handle docker plugin metadata first

docker system prune

Since the plugin ignores arguments, calling it with the __complete command would still cause the plugin to execute and do the pruning. It's for such cases that knowing in advance if completion is supported becomes important.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Docker completion scripts should be easier to install

5 participants