Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion cli/cobra.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
28 changes: 19 additions & 9 deletions cmd/docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -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 {

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.

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")
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

flags.BoolP("version", "v", false, "Print version information and quit")

setFlagErrorFunc(dockerCli, cmd)
Expand Down
26 changes: 16 additions & 10 deletions contrib/completion/bash/docker
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
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
    

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.

for value in "${words[@]:2}"; do
if [ -z "$value" ]; then
resultArray+=( "''" )
Expand All @@ -5503,6 +5500,18 @@ _docker_compose() {
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%:*}

}

_docker_buildx() {
__completeNoDesc buildx
}

_docker_compose() {
__completeNoDesc compose
}

_docker_scan() {
__completeNoDesc scan
}

_docker() {
local previous_extglob_setting=$(shopt -p extglob)
shopt -s extglob
Expand Down Expand Up @@ -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}}')
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 🤦

local known_plugin_commands=($PLUGINS_INSTALLED)

local experimental_server_commands=(
checkpoint
Expand Down
32 changes: 32 additions & 0 deletions contrib/completion/completion.go
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions docs/reference/commandline/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions man/docker.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ To see the man page for a command run **man docker <command>**.
**--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'.

Expand Down