From f6018613db442988b61239356788661f100b95b4 Mon Sep 17 00:00:00 2001 From: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> Date: Tue, 30 Apr 2024 15:16:37 +0200 Subject: [PATCH] feat: autocompletion installer Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com> --- cli/command/commands/commands.go | 4 + cli/command/completion/autocomplete.go | 239 ++++++++++++ cli/command/completion/autocomplete_test.go | 411 ++++++++++++++++++++ cli/command/completion/command.go | 46 +++ cli/command/completion/testdata/docker.bash | 338 ++++++++++++++++ cli/command/completion/testdata/docker.fish | 235 +++++++++++ cli/command/completion/testdata/docker.zsh | 212 ++++++++++ 7 files changed, 1485 insertions(+) create mode 100644 cli/command/completion/autocomplete.go create mode 100644 cli/command/completion/autocomplete_test.go create mode 100644 cli/command/completion/command.go create mode 100644 cli/command/completion/testdata/docker.bash create mode 100644 cli/command/completion/testdata/docker.fish create mode 100644 cli/command/completion/testdata/docker.zsh diff --git a/cli/command/commands/commands.go b/cli/command/commands/commands.go index 23a43568b5..7de64b889f 100644 --- a/cli/command/commands/commands.go +++ b/cli/command/commands/commands.go @@ -6,6 +6,7 @@ import ( "github.com/docker/cli/cli/command" "github.com/docker/cli/cli/command/builder" "github.com/docker/cli/cli/command/checkpoint" + "github.com/docker/cli/cli/command/completion" "github.com/docker/cli/cli/command/config" "github.com/docker/cli/cli/command/container" "github.com/docker/cli/cli/command/context" @@ -63,6 +64,9 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) { stack.NewStackCommand(dockerCli), swarm.NewSwarmCommand(dockerCli), + // completion command + completion.NewCompletionCommand(dockerCli), + // legacy commands may be hidden hide(container.NewAttachCommand(dockerCli)), hide(container.NewCommitCommand(dockerCli)), diff --git a/cli/command/completion/autocomplete.go b/cli/command/completion/autocomplete.go new file mode 100644 index 0000000000..5c90180bf8 --- /dev/null +++ b/cli/command/completion/autocomplete.go @@ -0,0 +1,239 @@ +package completion + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type shellCompletionSetup interface { + // Generate completions for the Docker CLI based on the provided shell. + DockerCompletion(ctx context.Context, shell supportedCompletionShell) ([]byte, error) + // Set up completions for the Docker CLI for the provided shell. + // + // For zsh completions, this function should also configure the user's + // .zshrc file to load the completions correctly. + // Please see https://zsh.sourceforge.io/Doc/Release/Completion-System.html + // for more information. + InstallCompletions(ctx context.Context, shell supportedCompletionShell) error + // Get the completion directory for the provided shell. + GetCompletionDir(shell supportedCompletionShell) string + // Get the manual instructions for the provided shell. + GetManualInstructions(shell supportedCompletionShell) string +} + +type supportedCompletionShell string + +const ( + bash supportedCompletionShell = "bash" + fish supportedCompletionShell = "fish" + zsh supportedCompletionShell = "zsh" + powershell supportedCompletionShell = "powershell" +) + +func (s supportedCompletionShell) FileName() string { + switch s { + case zsh: + return "_docker" + case bash: + return "docker" + case fish: + return "docker.fish" + } + return "" +} + +const ( + zshrc = ".zshrc" + zshCompletionDir = ".docker/completions" + fishCompletionDir = ".config/fish/completions" + bashCompletionDir = ".local/share/bash-completion/completions" +) + +// TODO: file permissions are difficult. +// Wondering if this should be 0644 or 0750 +// From stackoverflow most mention a sane default for the home directory +// is 0755/0751. +const filePerm = 0755 + +type common struct { + command func(ctx context.Context, name string, arg ...string) *exec.Cmd + homeDirectory string + dockerCliBinary string +} + +type unixShellSetup struct { + zshrc string + zshCompletionDir string + fishCompletionDir string + bashCompletionDir string + hasOhMyZsh bool + common +} + +func unixDefaultShell() (supportedCompletionShell, error) { + currentShell := os.Getenv("SHELL") + + if len(currentShell) == 0 { + return "", errors.New("SHELL environment variable not set") + } + + t := strings.Split(currentShell, "/") + + switch t[len(t)-1] { + case "bash": + return bash, nil + case "zsh": + return zsh, nil + case "fish": + return fish, nil + } + + return "", errors.New("unsupported shell") +} + +var _ shellCompletionSetup = &unixShellSetup{} + +func NewUnixShellSetup(homeDirectory string, dockerCliBinary string) shellCompletionSetup { + zshrcFile := filepath.Join(homeDirectory, zshrc) + // override the default directory if ZDOTDIR is set + // if this is set, we assume the user has set up their own zshrc + // and we should append to that file instead + if zshroot := os.Getenv("ZDOTDIR"); zshroot != "" { + zshrcFile = filepath.Join(zshroot, zshrc) + } + var hasOhMyZsh bool + zshCompletionDir := filepath.Join(homeDirectory, zshCompletionDir) + // overide the default zsh completions directory if oh-my-zsh is installed + if ohmyzsh := os.Getenv("ZSH"); ohmyzsh != "" { + // ensure that the oh-my-zsh completions directory exists + if _, err := os.Stat(ohmyzsh); err == nil { + zshCompletionDir = filepath.Join(ohmyzsh, "completions") + hasOhMyZsh = true + } + } + return &unixShellSetup{ + zshrc: zshrcFile, + zshCompletionDir: zshCompletionDir, + fishCompletionDir: filepath.Join(homeDirectory, fishCompletionDir), + bashCompletionDir: filepath.Join(homeDirectory, bashCompletionDir), + hasOhMyZsh: hasOhMyZsh, + common: common{ + homeDirectory: homeDirectory, + dockerCliBinary: dockerCliBinary, + command: exec.CommandContext, + }, + } +} + +func (u *unixShellSetup) DockerCompletion(ctx context.Context, shell supportedCompletionShell) ([]byte, error) { + dockerCmd := u.command(ctx, u.dockerCliBinary, "completion", string(shell)) + out, err := dockerCmd.Output() + if err != nil { + return nil, err + } + return out, nil +} + +func (u *unixShellSetup) GetCompletionDir(shell supportedCompletionShell) string { + switch shell { + case zsh: + return u.zshCompletionDir + case fish: + return u.fishCompletionDir + case bash: + return u.bashCompletionDir + } + return "" +} + +func (u *unixShellSetup) GetManualInstructions(shell supportedCompletionShell) string { + completionDir := u.GetCompletionDir(shell) + completionsFile := filepath.Join(completionDir, shell.FileName()) + + instructions := fmt.Sprintf(`mkdir -p %s && docker completion %s > %s`, completionDir, shell, completionsFile) + + if shell == zsh && !u.hasOhMyZsh { + instructions += "\n" + instructions += fmt.Sprintf("cat <> %s\n"+ + "# The following lines have been added by Docker to enable Docker CLI completions.\n"+ + "fpath=(%s $fpath)\n"+ + "autoload -Uz compinit\n"+ + "compinit\n"+ + "EOT\n"+ + "# End of Docker Completions", u.zshrc, completionsFile) + } + + return instructions +} + +func (u *unixShellSetup) InstallCompletions(ctx context.Context, shell supportedCompletionShell) error { + completionDir := u.GetCompletionDir(shell) + + if err := os.MkdirAll(completionDir, filePerm); err != nil { + return err + } + + completionFile := filepath.Join(completionDir, shell.FileName()) + + _ = os.Remove(completionFile) + + completions, err := u.DockerCompletion(ctx, shell) + if err != nil { + return err + } + + f, err := os.OpenFile(completionFile, os.O_CREATE|os.O_WRONLY, filePerm) + if err != nil { + return err + } + defer f.Close() + + if _, err = f.Write(completions); err != nil { + return err + } + + // only configure fpath for zsh if oh-my-zsh is not installed + if shell == zsh && !u.hasOhMyZsh { + + // This should error if it does not exist. + zshrcContent, err := os.ReadFile(u.zshrc) + if err != nil { + // TODO: what should we do here? The error message might not be too helpful. + return fmt.Errorf("could not open %s. Please ensure that your .zshrc file is set up correctly before continuing.", u.zshrc) + } + + fpath := fmt.Sprintf("fpath=(%s $fpath)", completionDir) + autoload := "autoload -Uz compinit" + compinit := "compinit" + + // if fpath is already in the .zshrc file, we don't need to add it again + if strings.Contains(string(zshrcContent), fpath) { + return nil + } + + // Only append to .zshrc when it exists. + f, err = os.OpenFile(u.zshrc, os.O_APPEND|os.O_WRONLY, os.ModeAppend) + if err != nil { + return err + } + defer f.Close() + + zshrcFpath := fmt.Sprintf("# The following lines have been added by Docker Desktop to enable Docker CLI completions.\n"+ + "%s\n"+ + "%s\n"+ + "%s\n"+ + "# End of Docker CLI completions\n", fpath, autoload, compinit) + + _, err = f.WriteString(zshrcFpath) + if err != nil { + return err + } + } + + return nil +} diff --git a/cli/command/completion/autocomplete_test.go b/cli/command/completion/autocomplete_test.go new file mode 100644 index 0000000000..18c7f42115 --- /dev/null +++ b/cli/command/completion/autocomplete_test.go @@ -0,0 +1,411 @@ +package completion + +import ( + "context" + "embed" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +type testFuncs string + +const ( + testDockerCompletions testFuncs = "TestDockerCompletions" +) + +//go:embed testdata +var fixtures embed.FS + +// fakeExecCommand is a helper function that hooks +// the current test binary into an os/exec cmd.Run() call +// allowing us to mock out third party dependencies called through os/exec. +// +// testBinary is the current test binary that is running, can be accessed through os.Args[0] +// funcName is the name of the function you want to run as a sub-process of the current test binary +// +// The call path is as follows: +// - Register the function you want to run through TestMain +// - Call the cmd.Run() function from the returned exec.Cmd +// - TestMain will execute the function as a sub-process of the current test binary +func fakeExecCommand(t *testing.T, testBinary string, funcName testFuncs) func(ctx context.Context, command string, args ...string) *exec.Cmd { + t.Helper() + return func(ctx context.Context, command string, args ...string) *exec.Cmd { + cmd := exec.Command(testBinary, append([]string{command}, args...)...) + cmd.Env = append(os.Environ(), "TEST_MAIN_FUNC="+string(funcName)) + return cmd + } +} + +// TestMain is setup here to act as a dispatcher +// for functions hooked into the test binary through +// fakeExecCommand. +func TestMain(m *testing.M) { + switch testFuncs(os.Getenv("TEST_MAIN_FUNC")) { + case testDockerCompletions: + FakeDockerCompletionsProcess() + default: + os.Exit(m.Run()) + } +} + +// this is a test function that will only be run when +// fakeExecCommand is hooked into a cmd.Run()/cmd.Output call +// with the funcName as "TestDockerCompletions" +// TestMain executes this function as a sub-process of the current +// test binary. +func FakeDockerCompletionsProcess() { + s := supportedCompletionShell(os.Args[3]) + if s == "" { + panic("shell not provided") + } + + completions, err := fixtures.ReadFile("testdata/docker." + string(s)) + if err != nil { + panic(err) + } + + _, err = fmt.Fprint(os.Stdout, string(completions)) + if err != nil { + panic(err) + } + + os.Exit(0) +} + +func TestDockerCompletion(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + ds := NewUnixShellSetup("", "docker").(*unixShellSetup) + ds.command = fakeExecCommand(t, os.Args[0], testDockerCompletions) + + t.Run("zsh completion", func(t *testing.T) { + completions, err := ds.DockerCompletion(ctx, zsh) + assert.NilError(t, err, "expected docker completions to not error, got %s", err) + assert.Check(t, len(completions) > 0, "expected docker completions to not be empty") + + expected, err := fixtures.ReadFile("testdata/docker.zsh") + assert.NilError(t, err) + + assert.Equal(t, string(expected), string(completions), "docker.zsh fixture did not match docker completion output") + }) + + t.Run("bash completion", func(t *testing.T) { + completions, err := ds.DockerCompletion(ctx, bash) + assert.NilError(t, err) + assert.Check(t, len(completions) > 0, "expected docker completions to not be empty") + + expected, err := fixtures.ReadFile("testdata/docker.bash") + assert.NilError(t, err) + + assert.Equal(t, string(expected), string(completions), "docker.bash fixtures did not match docker completion output") + }) + + t.Run("fish completion", func(t *testing.T) { + completions, err := ds.DockerCompletion(ctx, fish) + assert.NilError(t, err) + assert.Check(t, len(completions) > 0, "expected docker completions to not be empty") + + expected, err := fixtures.ReadFile("testdata/docker.fish") + assert.NilError(t, err) + + assert.Equal(t, string(expected), string(completions), "docker.fish fixtures did not match docker completion output") + }) +} + +func TestUnixDefaultShell(t *testing.T) { + for _, tc := range []struct { + desc string + path string + expected supportedCompletionShell + expectedErr string + }{ + { + desc: "bash", + path: "/bin/bash", + expected: bash, + expectedErr: "", + }, + { + desc: "zsh", + path: "/bin/zsh", + expected: zsh, + expectedErr: "", + }, + { + desc: "fish", + path: "/bin/fish", + expected: fish, + expectedErr: "", + }, + { + desc: "homebrew bash", + path: "/opt/homebrew/bin/bash", + expected: bash, + expectedErr: "", + }, + { + desc: "homebrew zsh", + path: "/opt/homebrew/bin/zsh", + expected: zsh, + expectedErr: "", + }, + { + desc: "homebrew fish", + path: "/opt/homebrew/bin/fish", + expected: fish, + expectedErr: "", + }, + { + desc: "unsupported shell", + path: "/bin/unsupported", + expected: "", + expectedErr: "unsupported shell", + }, + { + desc: "empty shell", + path: "", + expected: "", + expectedErr: "SHELL environment variable not set", + }, + } { + t.Run(tc.desc, func(t *testing.T) { + t.Setenv("SHELL", tc.path) + + s, err := unixDefaultShell() + if tc.expectedErr != "" { + assert.Check(t, is.ErrorContains(err, tc.expectedErr)) + } + assert.Equal(t, tc.expected, s) + }) + } +} + +func TestInstallCompletions(t *testing.T) { + + zshSetup := func(t *testing.T, ds *unixShellSetup) *os.File { + t.Helper() + zshrcFile, err := os.OpenFile(ds.zshrc, os.O_RDWR|os.O_CREATE, filePerm) + assert.NilError(t, err) + + t.Cleanup(func() { + zshrcFile.Close() + }) + + _, err = os.Stat(ds.zshrc) + assert.NilError(t, err, "expected zshrc file to exist") + return zshrcFile + } + + hasZshCompletions := func(t *testing.T, ds *unixShellSetup) { + t.Helper() + + zshrcContent, err := os.ReadFile(ds.zshrc) + assert.NilError(t, err) + assert.Check(t, is.Contains(string(zshrcContent), fmt.Sprintf("fpath=(%s $fpath)", ds.zshCompletionDir))) + + _, err = os.Stat(filepath.Join(ds.zshCompletionDir, zsh.FileName())) + assert.NilError(t, err, "expected zsh completions directory to exist") + + completions, err := os.ReadFile(filepath.Join(ds.zshCompletionDir, zsh.FileName())) + assert.NilError(t, err) + + zshFixture, err := fixtures.ReadFile("testdata/docker." + string(zsh)) + assert.NilError(t, err) + assert.Equal(t, string(zshFixture), string(completions)) + } + + setup := func(t *testing.T) *unixShellSetup { + t.Helper() + + tmphome := t.TempDir() + ds := NewUnixShellSetup(tmphome, "docker").(*unixShellSetup) + ds.command = fakeExecCommand(t, os.Args[0], testDockerCompletions) + return ds + } + + testcases := []struct { + shell supportedCompletionShell + desc string + setupFunc func(t *testing.T) *unixShellSetup + assertFunc func(t *testing.T, ds *unixShellSetup) + }{ + { + shell: zsh, + desc: "zsh completions", + setupFunc: func(t *testing.T) *unixShellSetup { + t.Helper() + ds := setup(t) + zshSetup(t, ds) + return ds + }, + assertFunc: hasZshCompletions, + }, + { + shell: zsh, + desc: "zsh completions with ZDOTDIR", + setupFunc: func(t *testing.T) *unixShellSetup { + t.Helper() + zdotdir := filepath.Join(t.TempDir(), "zdotdir") + assert.NilError(t, os.MkdirAll(zdotdir, filePerm)) + t.Setenv("ZDOTDIR", zdotdir) + + ds := setup(t) + zshSetup(t, ds) + return ds + }, + assertFunc: func(t *testing.T, ds *unixShellSetup) { + t.Helper() + hasZshCompletions(t, ds) + assert.Check(t, is.Contains(os.Getenv("ZDOTDIR"), "zdotdir")) + assert.Equal(t, ds.zshrc, filepath.Join(os.Getenv("ZDOTDIR"), ".zshrc")) + }, + }, + { + shell: zsh, + desc: "existing fpath in zshrc", + setupFunc: func(t *testing.T) *unixShellSetup { + t.Helper() + ds := setup(t) + zshrcFile := zshSetup(t, ds) + + _, err := fmt.Fprintf(zshrcFile, "fpath=(%s $fpath)", ds.zshCompletionDir) + assert.NilError(t, err) + + return ds + }, + assertFunc: func(t *testing.T, ds *unixShellSetup) { + t.Helper() + hasZshCompletions(t, ds) + zshrcFile, err := os.ReadFile(ds.zshrc) + assert.NilError(t, err) + assert.Equal(t, 1, strings.Count(string(zshrcFile), fmt.Sprintf("fpath=(%s $fpath)", ds.zshCompletionDir))) + }, + }, + { + shell: bash, + desc: "bash completions", + setupFunc: setup, + assertFunc: func(t *testing.T, ds *unixShellSetup) { + t.Helper() + _, err := os.Stat(filepath.Join(ds.bashCompletionDir, bash.FileName())) + assert.NilError(t, err) + + completions, err := os.ReadFile(filepath.Join(ds.bashCompletionDir, bash.FileName())) + assert.NilError(t, err) + + bashFixture, err := fixtures.ReadFile("testdata/docker." + string(bash)) + assert.NilError(t, err) + assert.Equal(t, string(bashFixture), string(completions)) + }, + }, + { + shell: fish, + desc: "fish completions", + setupFunc: setup, + assertFunc: func(t *testing.T, ds *unixShellSetup) { + t.Helper() + _, err := os.Stat(filepath.Join(ds.fishCompletionDir, fish.FileName())) + assert.NilError(t, err) + + completions, err := os.ReadFile(filepath.Join(ds.fishCompletionDir, fish.FileName())) + assert.NilError(t, err) + + fishFixture, err := fixtures.ReadFile("testdata/docker." + string(fish)) + assert.NilError(t, err) + assert.Equal(t, string(fishFixture), string(completions)) + }, + }, + { + shell: zsh, + desc: "zsh with oh-my-zsh", + setupFunc: func(t *testing.T) *unixShellSetup { + t.Helper() + tmphome := t.TempDir() + ohmyzsh := filepath.Join(tmphome, ".oh-my-zsh") + assert.NilError(t, os.MkdirAll(ohmyzsh, filePerm)) + t.Setenv("ZSH", ohmyzsh) + + ds := NewUnixShellSetup(tmphome, "docker").(*unixShellSetup) + ds.command = fakeExecCommand(t, os.Args[0], testDockerCompletions) + + zshSetup(t, ds) + return ds + }, + assertFunc: func(t *testing.T, ds *unixShellSetup) { + t.Helper() + assert.Equal(t, filepath.Join(ds.homeDirectory, ".oh-my-zsh/completions"), ds.zshCompletionDir) + _, err := os.Stat(filepath.Join(ds.zshCompletionDir, zsh.FileName())) + assert.NilError(t, err) + + completions, err := os.ReadFile(filepath.Join(ds.zshCompletionDir, zsh.FileName())) + assert.NilError(t, err) + + zshFixture, err := fixtures.ReadFile("testdata/docker." + string(zsh)) + assert.NilError(t, err) + assert.Equal(t, string(zshFixture), string(completions)) + }, + }, + { + shell: zsh, + desc: "should fallback to zsh when oh-my-zsh directory does not exist", + setupFunc: func(t *testing.T) *unixShellSetup { + t.Helper() + tmphome := t.TempDir() + t.Setenv("ZSH", filepath.Join(tmphome, ".oh-my-zsh")) + + ds := NewUnixShellSetup(tmphome, "docker").(*unixShellSetup) + ds.command = fakeExecCommand(t, os.Args[0], testDockerCompletions) + + zshSetup(t, ds) + return ds + }, + assertFunc: func(t *testing.T, ds *unixShellSetup) { + t.Helper() + assert.Check(t, !strings.Contains(ds.zshCompletionDir, ".oh-my-zsh")) + hasZshCompletions(t, ds) + }, + }, + } + + for _, tc := range testcases { + t.Run(tc.desc, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + ds := tc.setupFunc(t) + assert.NilError(t, ds.InstallCompletions(ctx, tc.shell)) + tc.assertFunc(t, ds) + }) + } +} + +func TestGetCompletionDir(t *testing.T) { + + t.Run("standard shells", func(t *testing.T) { + tmphome := t.TempDir() + ds := NewUnixShellSetup(tmphome, "docker") + + assert.Equal(t, filepath.Join(tmphome, zshCompletionDir), ds.GetCompletionDir(zsh)) + assert.Equal(t, filepath.Join(tmphome, fishCompletionDir), ds.GetCompletionDir(fish)) + assert.Equal(t, filepath.Join(tmphome, bashCompletionDir), ds.GetCompletionDir(bash)) + assert.Equal(t, "", ds.GetCompletionDir(supportedCompletionShell("unsupported"))) + }) + + t.Run("oh-my-zsh", func(t *testing.T) { + tmphome := t.TempDir() + ohMyZshTmpDir := filepath.Join(tmphome, ".oh-my-zsh") + assert.NilError(t, os.MkdirAll(ohMyZshTmpDir, filePerm)) + t.Setenv("ZSH", ohMyZshTmpDir) + ds := NewUnixShellSetup(tmphome, "docker") + assert.Equal(t, filepath.Join(ohMyZshTmpDir, "completions"), ds.GetCompletionDir(zsh)) + }) + +} diff --git a/cli/command/completion/command.go b/cli/command/completion/command.go new file mode 100644 index 0000000000..7e6b2481f9 --- /dev/null +++ b/cli/command/completion/command.go @@ -0,0 +1,46 @@ +package completion + +import ( + "fmt" + "os" + + "github.com/docker/cli/cli" + "github.com/docker/cli/cli/command" + "github.com/spf13/cobra" +) + +func NewCompletionCommand(dockerCli command.Cli) *cobra.Command { + cmd := &cobra.Command{ + Use: "completion [bash|zsh|fish|powershell|install]", + Short: "Output shell completion code for the specified shell (bash or zsh)", + Args: cli.RequiresMaxArgs(1), + ValidArgs: []string{"bash", "zsh", "fish", "powershell", "install"}, + DisableFlagParsing: false, + RunE: func(cmd *cobra.Command, args []string) error { + shellSetup := NewUnixShellSetup("", "docker") + + if cmd.Flag("manual").Changed { + _, _ = fmt.Fprint(dockerCli.Out(), shellSetup.GetManualInstructions(supportedCompletionShell(args[0]))) + return nil + } + + switch args[0] { + case "install": + return shellSetup.InstallCompletions(cmd.Context(), supportedCompletionShell(os.Getenv("SHELL"))) + case "bash": + + return cmd.GenBashCompletionV2(dockerCli.Out(), true) + case "zsh": + return cmd.GenZshCompletion(dockerCli.Out()) + case "fish": + return cmd.GenFishCompletion(dockerCli.Out(), true) + default: + return command.ShowHelp(dockerCli.Err())(cmd, args) + } + }, + } + + cmd.PersistentFlags().Bool("manual", false, "Display instructions for installing autocompletion") + + return cmd +} diff --git a/cli/command/completion/testdata/docker.bash b/cli/command/completion/testdata/docker.bash new file mode 100644 index 0000000000..facf5249f7 --- /dev/null +++ b/cli/command/completion/testdata/docker.bash @@ -0,0 +1,338 @@ +# bash completion V2 for docker -*- shell-script -*- + +__docker_debug() +{ + if [[ -n ${BASH_COMP_DEBUG_FILE-} ]]; then + echo "$*" >> "${BASH_COMP_DEBUG_FILE}" + fi +} + +# Macs have bash3 for which the bash-completion package doesn't include +# _init_completion. This is a minimal version of that function. +__docker_init_completion() +{ + COMPREPLY=() + _get_comp_words_by_ref "$@" cur prev words cword +} + +# This function calls the docker program to obtain the completion +# results and the directive. It fills the 'out' and 'directive' vars. +__docker_get_completion_results() { + local requestComp lastParam lastChar args + + # Prepare the command to request completions for the program. + # Calling ${words[0]} instead of directly docker allows handling aliases + args=("${words[@]:1}") + requestComp="${words[0]} __completeNoDesc ${args[*]}" + + lastParam=${words[$((${#words[@]}-1))]} + lastChar=${lastParam:$((${#lastParam}-1)):1} + __docker_debug "lastParam ${lastParam}, lastChar ${lastChar}" + + if [[ -z ${cur} && ${lastChar} != = ]]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go method. + __docker_debug "Adding extra empty parameter" + requestComp="${requestComp} ''" + fi + + # When completing a flag with an = (e.g., docker -n=) + # bash focuses on the part after the =, so we need to remove + # the flag part from $cur + if [[ ${cur} == -*=* ]]; then + cur="${cur#*=}" + fi + + __docker_debug "Calling ${requestComp}" + # Use eval to handle any environment variables and such + out=$(eval "${requestComp}" 2>/dev/null) + + # Extract the directive integer at the very end of the output following a colon (:) + directive=${out##*:} + # Remove the directive + out=${out%:*} + if [[ ${directive} == "${out}" ]]; then + # There is not directive specified + directive=0 + fi + __docker_debug "The completion directive is: ${directive}" + __docker_debug "The completions are: ${out}" +} + +__docker_process_completion_results() { + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + local shellCompDirectiveKeepOrder=32 + + if (((directive & shellCompDirectiveError) != 0)); then + # Error code. No completion. + __docker_debug "Received error from custom completion go code" + return + else + if (((directive & shellCompDirectiveNoSpace) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + __docker_debug "Activating no space" + compopt -o nospace + else + __docker_debug "No space directive not supported in this version of bash" + fi + fi + if (((directive & shellCompDirectiveKeepOrder) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + # no sort isn't supported for bash less than < 4.4 + if [[ ${BASH_VERSINFO[0]} -lt 4 || ( ${BASH_VERSINFO[0]} -eq 4 && ${BASH_VERSINFO[1]} -lt 4 ) ]]; then + __docker_debug "No sort directive not supported in this version of bash" + else + __docker_debug "Activating keep order" + compopt -o nosort + fi + else + __docker_debug "No sort directive not supported in this version of bash" + fi + fi + if (((directive & shellCompDirectiveNoFileComp) != 0)); then + if [[ $(type -t compopt) == builtin ]]; then + __docker_debug "Activating no file completion" + compopt +o default + else + __docker_debug "No file completion directive not supported in this version of bash" + fi + fi + fi + + # Separate activeHelp from normal completions + local completions=() + local activeHelp=() + __docker_extract_activeHelp + + if (((directive & shellCompDirectiveFilterFileExt) != 0)); then + # File extension filtering + local fullFilter filter filteringCmd + + # Do not use quotes around the $completions variable or else newline + # characters will be kept. + for filter in ${completions[*]}; do + fullFilter+="$filter|" + done + + filteringCmd="_filedir $fullFilter" + __docker_debug "File filtering command: $filteringCmd" + $filteringCmd + elif (((directive & shellCompDirectiveFilterDirs) != 0)); then + # File completion for directories only + + local subdir + subdir=${completions[0]} + if [[ -n $subdir ]]; then + __docker_debug "Listing directories in $subdir" + pushd "$subdir" >/dev/null 2>&1 && _filedir -d && popd >/dev/null 2>&1 || return + else + __docker_debug "Listing directories in ." + _filedir -d + fi + else + __docker_handle_completion_types + fi + + __docker_handle_special_char "$cur" : + __docker_handle_special_char "$cur" = + + # Print the activeHelp statements before we finish + if ((${#activeHelp[*]} != 0)); then + printf "\n"; + printf "%s\n" "${activeHelp[@]}" + printf "\n" + + # The prompt format is only available from bash 4.4. + # We test if it is available before using it. + if (x=${PS1@P}) 2> /dev/null; then + printf "%s" "${PS1@P}${COMP_LINE[@]}" + else + # Can't print the prompt. Just print the + # text the user had typed, it is workable enough. + printf "%s" "${COMP_LINE[@]}" + fi + fi +} + +# Separate activeHelp lines from real completions. +# Fills the $activeHelp and $completions arrays. +__docker_extract_activeHelp() { + local activeHelpMarker="_activeHelp_ " + local endIndex=${#activeHelpMarker} + + while IFS='' read -r comp; do + if [[ ${comp:0:endIndex} == $activeHelpMarker ]]; then + comp=${comp:endIndex} + __docker_debug "ActiveHelp found: $comp" + if [[ -n $comp ]]; then + activeHelp+=("$comp") + fi + else + # Not an activeHelp line but a normal completion + completions+=("$comp") + fi + done <<<"${out}" +} + +__docker_handle_completion_types() { + __docker_debug "__docker_handle_completion_types: COMP_TYPE is $COMP_TYPE" + + case $COMP_TYPE in + 37|42) + # Type: menu-complete/menu-complete-backward and insert-completions + # If the user requested inserting one completion at a time, or all + # completions at once on the command-line we must remove the descriptions. + # https://github.com/spf13/cobra/issues/1508 + local tab=$'\t' comp + while IFS='' read -r comp; do + [[ -z $comp ]] && continue + # Strip any description + comp=${comp%%$tab*} + # Only consider the completions that match + if [[ $comp == "$cur"* ]]; then + COMPREPLY+=("$comp") + fi + done < <(printf "%s\n" "${completions[@]}") + ;; + + *) + # Type: complete (normal completion) + __docker_handle_standard_completion_case + ;; + esac +} + +__docker_handle_standard_completion_case() { + local tab=$'\t' comp + + # Short circuit to optimize if we don't have descriptions + if [[ "${completions[*]}" != *$tab* ]]; then + IFS=$'\n' read -ra COMPREPLY -d '' < <(compgen -W "${completions[*]}" -- "$cur") + return 0 + fi + + local longest=0 + local compline + # Look for the longest completion so that we can format things nicely + while IFS='' read -r compline; do + [[ -z $compline ]] && continue + # Strip any description before checking the length + comp=${compline%%$tab*} + # Only consider the completions that match + [[ $comp == "$cur"* ]] || continue + COMPREPLY+=("$compline") + if ((${#comp}>longest)); then + longest=${#comp} + fi + done < <(printf "%s\n" "${completions[@]}") + + # If there is a single completion left, remove the description text + if ((${#COMPREPLY[*]} == 1)); then + __docker_debug "COMPREPLY[0]: ${COMPREPLY[0]}" + comp="${COMPREPLY[0]%%$tab*}" + __docker_debug "Removed description from single completion, which is now: ${comp}" + COMPREPLY[0]=$comp + else # Format the descriptions + __docker_format_comp_descriptions $longest + fi +} + +__docker_handle_special_char() +{ + local comp="$1" + local char=$2 + if [[ "$comp" == *${char}* && "$COMP_WORDBREAKS" == *${char}* ]]; then + local word=${comp%"${comp##*${char}}"} + local idx=${#COMPREPLY[*]} + while ((--idx >= 0)); do + COMPREPLY[idx]=${COMPREPLY[idx]#"$word"} + done + fi +} + +__docker_format_comp_descriptions() +{ + local tab=$'\t' + local comp desc maxdesclength + local longest=$1 + + local i ci + for ci in ${!COMPREPLY[*]}; do + comp=${COMPREPLY[ci]} + # Properly format the description string which follows a tab character if there is one + if [[ "$comp" == *$tab* ]]; then + __docker_debug "Original comp: $comp" + desc=${comp#*$tab} + comp=${comp%%$tab*} + + # $COLUMNS stores the current shell width. + # Remove an extra 4 because we add 2 spaces and 2 parentheses. + maxdesclength=$(( COLUMNS - longest - 4 )) + + # Make sure we can fit a description of at least 8 characters + # if we are to align the descriptions. + if ((maxdesclength > 8)); then + # Add the proper number of spaces to align the descriptions + for ((i = ${#comp} ; i < longest ; i++)); do + comp+=" " + done + else + # Don't pad the descriptions so we can fit more text after the completion + maxdesclength=$(( COLUMNS - ${#comp} - 4 )) + fi + + # If there is enough space for any description text, + # truncate the descriptions that are too long for the shell width + if ((maxdesclength > 0)); then + if ((${#desc} > maxdesclength)); then + desc=${desc:0:$(( maxdesclength - 1 ))} + desc+="…" + fi + comp+=" ($desc)" + fi + COMPREPLY[ci]=$comp + __docker_debug "Final comp: $comp" + fi + done +} + +__start_docker() +{ + local cur prev words cword split + + COMPREPLY=() + + # Call _init_completion from the bash-completion package + # to prepare the arguments properly + if declare -F _init_completion >/dev/null 2>&1; then + _init_completion -n =: || return + else + __docker_init_completion -n =: || return + fi + + __docker_debug + __docker_debug "========= starting completion logic ==========" + __docker_debug "cur is ${cur}, words[*] is ${words[*]}, #words[@] is ${#words[@]}, cword is $cword" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $cword location, so we need + # to truncate the command-line ($words) up to the $cword location. + words=("${words[@]:0:$cword+1}") + __docker_debug "Truncated words[*]: ${words[*]}," + + local out directive + __docker_get_completion_results + __docker_process_completion_results +} + +if [[ $(type -t compopt) = "builtin" ]]; then + complete -o default -F __start_docker docker +else + complete -o default -o nospace -F __start_docker docker +fi + +# ex: ts=4 sw=4 et filetype=sh diff --git a/cli/command/completion/testdata/docker.fish b/cli/command/completion/testdata/docker.fish new file mode 100644 index 0000000000..840c731424 --- /dev/null +++ b/cli/command/completion/testdata/docker.fish @@ -0,0 +1,235 @@ +# fish completion for docker -*- shell-script -*- + +function __docker_debug + set -l file "$BASH_COMP_DEBUG_FILE" + if test -n "$file" + echo "$argv" >> $file + end +end + +function __docker_perform_completion + __docker_debug "Starting __docker_perform_completion" + + # Extract all args except the last one + set -l args (commandline -opc) + # Extract the last arg and escape it in case it is a space + set -l lastArg (string escape -- (commandline -ct)) + + __docker_debug "args: $args" + __docker_debug "last arg: $lastArg" + + # Disable ActiveHelp which is not supported for fish shell + set -l requestComp "DOCKER_ACTIVE_HELP=0 $args[1] __completeNoDesc $args[2..-1] $lastArg" + + __docker_debug "Calling $requestComp" + set -l results (eval $requestComp 2> /dev/null) + + # Some programs may output extra empty lines after the directive. + # Let's ignore them or else it will break completion. + # Ref: https://github.com/spf13/cobra/issues/1279 + for line in $results[-1..1] + if test (string trim -- $line) = "" + # Found an empty line, remove it + set results $results[1..-2] + else + # Found non-empty line, we have our proper output + break + end + end + + set -l comps $results[1..-2] + set -l directiveLine $results[-1] + + # For Fish, when completing a flag with an = (e.g., -n=) + # completions must be prefixed with the flag + set -l flagPrefix (string match -r -- '-.*=' "$lastArg") + + __docker_debug "Comps: $comps" + __docker_debug "DirectiveLine: $directiveLine" + __docker_debug "flagPrefix: $flagPrefix" + + for comp in $comps + printf "%s%s\n" "$flagPrefix" "$comp" + end + + printf "%s\n" "$directiveLine" +end + +# this function limits calls to __docker_perform_completion, by caching the result behind $__docker_perform_completion_once_result +function __docker_perform_completion_once + __docker_debug "Starting __docker_perform_completion_once" + + if test -n "$__docker_perform_completion_once_result" + __docker_debug "Seems like a valid result already exists, skipping __docker_perform_completion" + return 0 + end + + set --global __docker_perform_completion_once_result (__docker_perform_completion) + if test -z "$__docker_perform_completion_once_result" + __docker_debug "No completions, probably due to a failure" + return 1 + end + + __docker_debug "Performed completions and set __docker_perform_completion_once_result" + return 0 +end + +# this function is used to clear the $__docker_perform_completion_once_result variable after completions are run +function __docker_clear_perform_completion_once_result + __docker_debug "" + __docker_debug "========= clearing previously set __docker_perform_completion_once_result variable ==========" + set --erase __docker_perform_completion_once_result + __docker_debug "Successfully erased the variable __docker_perform_completion_once_result" +end + +function __docker_requires_order_preservation + __docker_debug "" + __docker_debug "========= checking if order preservation is required ==========" + + __docker_perform_completion_once + if test -z "$__docker_perform_completion_once_result" + __docker_debug "Error determining if order preservation is required" + return 1 + end + + set -l directive (string sub --start 2 $__docker_perform_completion_once_result[-1]) + __docker_debug "Directive is: $directive" + + set -l shellCompDirectiveKeepOrder 32 + set -l keeporder (math (math --scale 0 $directive / $shellCompDirectiveKeepOrder) % 2) + __docker_debug "Keeporder is: $keeporder" + + if test $keeporder -ne 0 + __docker_debug "This does require order preservation" + return 0 + end + + __docker_debug "This doesn't require order preservation" + return 1 +end + + +# This function does two things: +# - Obtain the completions and store them in the global __docker_comp_results +# - Return false if file completion should be performed +function __docker_prepare_completions + __docker_debug "" + __docker_debug "========= starting completion logic ==========" + + # Start fresh + set --erase __docker_comp_results + + __docker_perform_completion_once + __docker_debug "Completion results: $__docker_perform_completion_once_result" + + if test -z "$__docker_perform_completion_once_result" + __docker_debug "No completion, probably due to a failure" + # Might as well do file completion, in case it helps + return 1 + end + + set -l directive (string sub --start 2 $__docker_perform_completion_once_result[-1]) + set --global __docker_comp_results $__docker_perform_completion_once_result[1..-2] + + __docker_debug "Completions are: $__docker_comp_results" + __docker_debug "Directive is: $directive" + + set -l shellCompDirectiveError 1 + set -l shellCompDirectiveNoSpace 2 + set -l shellCompDirectiveNoFileComp 4 + set -l shellCompDirectiveFilterFileExt 8 + set -l shellCompDirectiveFilterDirs 16 + + if test -z "$directive" + set directive 0 + end + + set -l compErr (math (math --scale 0 $directive / $shellCompDirectiveError) % 2) + if test $compErr -eq 1 + __docker_debug "Received error directive: aborting." + # Might as well do file completion, in case it helps + return 1 + end + + set -l filefilter (math (math --scale 0 $directive / $shellCompDirectiveFilterFileExt) % 2) + set -l dirfilter (math (math --scale 0 $directive / $shellCompDirectiveFilterDirs) % 2) + if test $filefilter -eq 1; or test $dirfilter -eq 1 + __docker_debug "File extension filtering or directory filtering not supported" + # Do full file completion instead + return 1 + end + + set -l nospace (math (math --scale 0 $directive / $shellCompDirectiveNoSpace) % 2) + set -l nofiles (math (math --scale 0 $directive / $shellCompDirectiveNoFileComp) % 2) + + __docker_debug "nospace: $nospace, nofiles: $nofiles" + + # If we want to prevent a space, or if file completion is NOT disabled, + # we need to count the number of valid completions. + # To do so, we will filter on prefix as the completions we have received + # may not already be filtered so as to allow fish to match on different + # criteria than the prefix. + if test $nospace -ne 0; or test $nofiles -eq 0 + set -l prefix (commandline -t | string escape --style=regex) + __docker_debug "prefix: $prefix" + + set -l completions (string match -r -- "^$prefix.*" $__docker_comp_results) + set --global __docker_comp_results $completions + __docker_debug "Filtered completions are: $__docker_comp_results" + + # Important not to quote the variable for count to work + set -l numComps (count $__docker_comp_results) + __docker_debug "numComps: $numComps" + + if test $numComps -eq 1; and test $nospace -ne 0 + # We must first split on \t to get rid of the descriptions to be + # able to check what the actual completion will be. + # We don't need descriptions anyway since there is only a single + # real completion which the shell will expand immediately. + set -l split (string split --max 1 \t $__docker_comp_results[1]) + + # Fish won't add a space if the completion ends with any + # of the following characters: @=/:., + set -l lastChar (string sub -s -1 -- $split) + if not string match -r -q "[@=/:.,]" -- "$lastChar" + # In other cases, to support the "nospace" directive we trick the shell + # by outputting an extra, longer completion. + __docker_debug "Adding second completion to perform nospace directive" + set --global __docker_comp_results $split[1] $split[1]. + __docker_debug "Completions are now: $__docker_comp_results" + end + end + + if test $numComps -eq 0; and test $nofiles -eq 0 + # To be consistent with bash and zsh, we only trigger file + # completion when there are no other completions + __docker_debug "Requesting file completion" + return 1 + end + end + + return 0 +end + +# Since Fish completions are only loaded once the user triggers them, we trigger them ourselves +# so we can properly delete any completions provided by another script. +# Only do this if the program can be found, or else fish may print some errors; besides, +# the existing completions will only be loaded if the program can be found. +if type -q "docker" + # The space after the program name is essential to trigger completion for the program + # and not completion of the program name itself. + # Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish. + complete --do-complete "docker " > /dev/null 2>&1 +end + +# Remove any pre-existing completions for the program since we will be handling all of them. +complete -c docker -e + +# this will get called after the two calls below and clear the $__docker_perform_completion_once_result global +complete -c docker -n '__docker_clear_perform_completion_once_result' +# The call to __docker_prepare_completions will setup __docker_comp_results +# which provides the program's completion choices. +# If this doesn't require order preservation, we don't use the -k flag +complete -c docker -n 'not __docker_requires_order_preservation && __docker_prepare_completions' -f -a '$__docker_comp_results' +# otherwise we use the -k flag +complete -k -c docker -n '__docker_requires_order_preservation && __docker_prepare_completions' -f -a '$__docker_comp_results' diff --git a/cli/command/completion/testdata/docker.zsh b/cli/command/completion/testdata/docker.zsh new file mode 100644 index 0000000000..2089017079 --- /dev/null +++ b/cli/command/completion/testdata/docker.zsh @@ -0,0 +1,212 @@ +#compdef docker +compdef _docker docker + +# zsh completion for docker -*- shell-script -*- + +__docker_debug() +{ + local file="$BASH_COMP_DEBUG_FILE" + if [[ -n ${file} ]]; then + echo "$*" >> "${file}" + fi +} + +_docker() +{ + local shellCompDirectiveError=1 + local shellCompDirectiveNoSpace=2 + local shellCompDirectiveNoFileComp=4 + local shellCompDirectiveFilterFileExt=8 + local shellCompDirectiveFilterDirs=16 + local shellCompDirectiveKeepOrder=32 + + local lastParam lastChar flagPrefix requestComp out directive comp lastComp noSpace keepOrder + local -a completions + + __docker_debug "\n========= starting completion logic ==========" + __docker_debug "CURRENT: ${CURRENT}, words[*]: ${words[*]}" + + # The user could have moved the cursor backwards on the command-line. + # We need to trigger completion from the $CURRENT location, so we need + # to truncate the command-line ($words) up to the $CURRENT location. + # (We cannot use $CURSOR as its value does not work when a command is an alias.) + words=("${=words[1,CURRENT]}") + __docker_debug "Truncated words[*]: ${words[*]}," + + lastParam=${words[-1]} + lastChar=${lastParam[-1]} + __docker_debug "lastParam: ${lastParam}, lastChar: ${lastChar}" + + # For zsh, when completing a flag with an = (e.g., docker -n=) + # completions must be prefixed with the flag + setopt local_options BASH_REMATCH + if [[ "${lastParam}" =~ '-.*=' ]]; then + # We are dealing with a flag with an = + flagPrefix="-P ${BASH_REMATCH}" + fi + + # Prepare the command to obtain completions + requestComp="${words[1]} __completeNoDesc ${words[2,-1]}" + if [ "${lastChar}" = "" ]; then + # If the last parameter is complete (there is a space following it) + # We add an extra empty parameter so we can indicate this to the go completion code. + __docker_debug "Adding extra empty parameter" + requestComp="${requestComp} \"\"" + fi + + __docker_debug "About to call: eval ${requestComp}" + + # Use eval to handle any environment variables and such + out=$(eval ${requestComp} 2>/dev/null) + __docker_debug "completion output: ${out}" + + # Extract the directive integer following a : from the last line + local lastLine + while IFS='\n' read -r line; do + lastLine=${line} + done < <(printf "%s\n" "${out[@]}") + __docker_debug "last line: ${lastLine}" + + if [ "${lastLine[1]}" = : ]; then + directive=${lastLine[2,-1]} + # Remove the directive including the : and the newline + local suffix + (( suffix=${#lastLine}+2)) + out=${out[1,-$suffix]} + else + # There is no directive specified. Leave $out as is. + __docker_debug "No directive found. Setting do default" + directive=0 + fi + + __docker_debug "directive: ${directive}" + __docker_debug "completions: ${out}" + __docker_debug "flagPrefix: ${flagPrefix}" + + if [ $((directive & shellCompDirectiveError)) -ne 0 ]; then + __docker_debug "Completion received error. Ignoring completions." + return + fi + + local activeHelpMarker="_activeHelp_ " + local endIndex=${#activeHelpMarker} + local startIndex=$((${#activeHelpMarker}+1)) + local hasActiveHelp=0 + while IFS='\n' read -r comp; do + # Check if this is an activeHelp statement (i.e., prefixed with $activeHelpMarker) + if [ "${comp[1,$endIndex]}" = "$activeHelpMarker" ];then + __docker_debug "ActiveHelp found: $comp" + comp="${comp[$startIndex,-1]}" + if [ -n "$comp" ]; then + compadd -x "${comp}" + __docker_debug "ActiveHelp will need delimiter" + hasActiveHelp=1 + fi + + continue + fi + + if [ -n "$comp" ]; then + # If requested, completions are returned with a description. + # The description is preceded by a TAB character. + # For zsh's _describe, we need to use a : instead of a TAB. + # We first need to escape any : as part of the completion itself. + comp=${comp//:/\\:} + + local tab="$(printf '\t')" + comp=${comp//$tab/:} + + __docker_debug "Adding completion: ${comp}" + completions+=${comp} + lastComp=$comp + fi + done < <(printf "%s\n" "${out[@]}") + + # Add a delimiter after the activeHelp statements, but only if: + # - there are completions following the activeHelp statements, or + # - file completion will be performed (so there will be choices after the activeHelp) + if [ $hasActiveHelp -eq 1 ]; then + if [ ${#completions} -ne 0 ] || [ $((directive & shellCompDirectiveNoFileComp)) -eq 0 ]; then + __docker_debug "Adding activeHelp delimiter" + compadd -x "--" + hasActiveHelp=0 + fi + fi + + if [ $((directive & shellCompDirectiveNoSpace)) -ne 0 ]; then + __docker_debug "Activating nospace." + noSpace="-S ''" + fi + + if [ $((directive & shellCompDirectiveKeepOrder)) -ne 0 ]; then + __docker_debug "Activating keep order." + keepOrder="-V" + fi + + if [ $((directive & shellCompDirectiveFilterFileExt)) -ne 0 ]; then + # File extension filtering + local filteringCmd + filteringCmd='_files' + for filter in ${completions[@]}; do + if [ ${filter[1]} != '*' ]; then + # zsh requires a glob pattern to do file filtering + filter="\*.$filter" + fi + filteringCmd+=" -g $filter" + done + filteringCmd+=" ${flagPrefix}" + + __docker_debug "File filtering command: $filteringCmd" + _arguments '*:filename:'"$filteringCmd" + elif [ $((directive & shellCompDirectiveFilterDirs)) -ne 0 ]; then + # File completion for directories only + local subdir + subdir="${completions[1]}" + if [ -n "$subdir" ]; then + __docker_debug "Listing directories in $subdir" + pushd "${subdir}" >/dev/null 2>&1 + else + __docker_debug "Listing directories in ." + fi + + local result + _arguments '*:dirname:_files -/'" ${flagPrefix}" + result=$? + if [ -n "$subdir" ]; then + popd >/dev/null 2>&1 + fi + return $result + else + __docker_debug "Calling _describe" + if eval _describe $keepOrder "completions" completions $flagPrefix $noSpace; then + __docker_debug "_describe found some completions" + + # Return the success of having called _describe + return 0 + else + __docker_debug "_describe did not find completions." + __docker_debug "Checking if we should do file completion." + if [ $((directive & shellCompDirectiveNoFileComp)) -ne 0 ]; then + __docker_debug "deactivating file completion" + + # We must return an error code here to let zsh know that there were no + # completions found by _describe; this is what will trigger other + # matching algorithms to attempt to find completions. + # For example zsh can match letters in the middle of words. + return 1 + else + # Perform file completion + __docker_debug "Activating file completion" + + # We must return the result of this command, so it must be the + # last command, or else we must store its result to return it. + _arguments '*:filename:_files'" ${flagPrefix}" + fi + fi + fi +} + +# don't run the completion function when being source-ed or eval-ed +if [ "$funcstack[1]" = "_docker" ]; then + _docker +fi