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/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)),
|
||||
|
|
|
@ -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