mirror of https://github.com/docker/cli.git
feat: autocompletion installer
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
This commit is contained in:
parent
9861ce90fd
commit
f6018613db
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/docker/cli/cli/command/builder"
|
"github.com/docker/cli/cli/command/builder"
|
||||||
"github.com/docker/cli/cli/command/checkpoint"
|
"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/config"
|
||||||
"github.com/docker/cli/cli/command/container"
|
"github.com/docker/cli/cli/command/container"
|
||||||
"github.com/docker/cli/cli/command/context"
|
"github.com/docker/cli/cli/command/context"
|
||||||
|
@ -63,6 +64,9 @@ func AddCommands(cmd *cobra.Command, dockerCli command.Cli) {
|
||||||
stack.NewStackCommand(dockerCli),
|
stack.NewStackCommand(dockerCli),
|
||||||
swarm.NewSwarmCommand(dockerCli),
|
swarm.NewSwarmCommand(dockerCli),
|
||||||
|
|
||||||
|
// completion command
|
||||||
|
completion.NewCompletionCommand(dockerCli),
|
||||||
|
|
||||||
// legacy commands may be hidden
|
// legacy commands may be hidden
|
||||||
hide(container.NewAttachCommand(dockerCli)),
|
hide(container.NewAttachCommand(dockerCli)),
|
||||||
hide(container.NewCommitCommand(dockerCli)),
|
hide(container.NewCommitCommand(dockerCli)),
|
||||||
|
|
|
@ -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 <<EOT >> %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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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=<TAB>)
|
||||||
|
# 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
|
|
@ -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., <program> -n=<TAB>)
|
||||||
|
# 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'
|
|
@ -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=<TAB>)
|
||||||
|
# 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
|
Loading…
Reference in New Issue