mirror of https://github.com/docker/cli.git
Compare commits
9 Commits
b917b70b97
...
c8a2bc832b
Author | SHA1 | Date |
---|---|---|
Alano Terblanche | c8a2bc832b | |
Sebastiaan van Stijn | a5fb752ecf | |
Laura Brehm | 4e64c59d64 | |
Jonathan A. Sternberg | 3472bbc28a | |
Laura Brehm | 649e564ee0 | |
Sebastiaan van Stijn | e1213edcc6 | |
Jonathan A. Sternberg | b1956f5073 | |
Alano Terblanche | a123a86a5e | |
Alano Terblanche | b761fe9d76 |
|
@ -67,7 +67,7 @@ jobs:
|
|||
name: Update Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: 1.22.7
|
||||
-
|
||||
name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
|
|
|
@ -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,311 @@
|
|||
//go:build unix
|
||||
|
||||
package completion
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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 unixShellSetup struct {
|
||||
zshrc string
|
||||
zshCompletionDir string
|
||||
fishCompletionDir string
|
||||
bashCompletionDir string
|
||||
hasOhMyZsh bool
|
||||
common
|
||||
}
|
||||
|
||||
var (
|
||||
ErrCompletionNotInstalled = errors.New("completion not installed")
|
||||
ErrCompletionOutdated = errors.New("completion file is outdated")
|
||||
ErrZshrcCouldNotWrite = errors.New("could not write to .zshrc file since it may not exist or have the necessary permissions")
|
||||
ErrCompletionDirectoryCreate = errors.New("could not create the completions directory")
|
||||
ErrCompletionFileWrite = errors.New("could not write to the completions file")
|
||||
ErrCompletionGenerated = errors.New("could not generate completions")
|
||||
ErrZshFpathNotFound = errors.New("completions file not found in the FPATH environment variable")
|
||||
)
|
||||
|
||||
var _ ShellCompletionSetup = &unixShellSetup{}
|
||||
|
||||
func hasCompletionInFpath(zshrc, completionDir string) (bool, error) {
|
||||
// check the FPATH environment variable first which contains a string of directories
|
||||
if fpathEnv := os.Getenv("FPATH"); fpathEnv != "" && strings.Contains(fpathEnv, completionDir) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat(zshrc); err != nil {
|
||||
return false, fmt.Errorf("unable to edit %s since it does not exist. Setup your zsh completions manually or create the .zshrc file inside of your home directory and try again", zshrc)
|
||||
}
|
||||
|
||||
// This should error if it does not exist.
|
||||
zshrcContent, err := os.ReadFile(zshrc)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to edit %s. Make sure that your .zshrc file is set up correctly before continuing", zshrc)
|
||||
}
|
||||
|
||||
fpath := fmt.Sprintf("fpath=(%s $fpath)", completionDir)
|
||||
if strings.Contains(string(zshrcContent), fpath) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func NewShellCompletionSetup(homeDirectory string, generateCompletions generateCompletions, opts ...NewShellCompletionOptsFunc) (ShellCompletionSetup, error) {
|
||||
return newUnixShellSetup(homeDirectory, generateCompletions, opts...)
|
||||
}
|
||||
|
||||
func newUnixShellSetup(homeDirectory string, generateCompletions generateCompletions, opts ...NewShellCompletionOptsFunc) (*unixShellSetup, error) {
|
||||
shell, shellRawString, err := shellFromEnv()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
c := &common{
|
||||
homeDirectory: homeDirectory,
|
||||
command: generateCompletions,
|
||||
currentShell: shell,
|
||||
currentShellRawString: shellRawString,
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(c)
|
||||
}
|
||||
|
||||
u := &unixShellSetup{
|
||||
zshrc: zshrcFile,
|
||||
zshCompletionDir: zshCompletionDir,
|
||||
fishCompletionDir: filepath.Join(homeDirectory, fishCompletionDir),
|
||||
bashCompletionDir: filepath.Join(homeDirectory, bashCompletionDir),
|
||||
hasOhMyZsh: hasOhMyZsh,
|
||||
common: *c,
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (u *unixShellSetup) GetCompletionScript(ctx context.Context) ([]byte, error) {
|
||||
var err error
|
||||
var buff bytes.Buffer
|
||||
|
||||
switch u.currentShell {
|
||||
case zsh:
|
||||
err = u.command.GenZshCompletion(&buff)
|
||||
case bash:
|
||||
err = u.command.GenBashCompletionV2(&buff, true)
|
||||
case fish:
|
||||
err = u.command.GenFishCompletion(&buff, true)
|
||||
default:
|
||||
return nil, ErrShellUnsupported
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buff.Bytes(), nil
|
||||
}
|
||||
|
||||
func (u *unixShellSetup) GetCompletionDir(ctx context.Context) string {
|
||||
switch u.currentShell {
|
||||
case zsh:
|
||||
return u.zshCompletionDir
|
||||
case fish:
|
||||
return u.fishCompletionDir
|
||||
case bash:
|
||||
return u.bashCompletionDir
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (u *unixShellSetup) GetManualInstructions(ctx context.Context) string {
|
||||
completionDir := u.GetCompletionDir(ctx)
|
||||
completionsFile := filepath.Join(completionDir, u.currentShell.FileName())
|
||||
|
||||
instructions := fmt.Sprintf("\tmkdir -p %s\n\tdocker completion %s > %s", completionDir, u.currentShell, completionsFile)
|
||||
|
||||
if u.currentShell == 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) error {
|
||||
completionDir := u.GetCompletionDir(ctx)
|
||||
|
||||
if err := os.MkdirAll(completionDir, filePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
completionFile := filepath.Join(completionDir, u.currentShell.FileName())
|
||||
|
||||
_ = os.Remove(completionFile)
|
||||
|
||||
completions, err := u.GetCompletionScript(ctx)
|
||||
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 u.currentShell == 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
|
||||
}
|
||||
|
||||
func (u *unixShellSetup) GetShell() supportedCompletionShell {
|
||||
return u.currentShell
|
||||
}
|
||||
|
||||
func (u *unixShellSetup) InstallStatus(ctx context.Context) (*ShellCompletionInstallStatus, error) {
|
||||
installStatus := &ShellCompletionInstallStatus{
|
||||
Shell: u.currentShellRawString,
|
||||
Status: StatusNotInstalled,
|
||||
}
|
||||
|
||||
ok, err := u.currentShell.Supported()
|
||||
if !ok {
|
||||
installStatus.Status = StatusUnsupported
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
installStatus.Reason = err.Error()
|
||||
return installStatus, nil
|
||||
}
|
||||
|
||||
completionDir := u.GetCompletionDir(ctx)
|
||||
completionFile := filepath.Join(completionDir, u.currentShell.FileName())
|
||||
installStatus.CompletionPath = completionFile
|
||||
|
||||
if _, err := os.Stat(completionFile); err != nil {
|
||||
installStatus.Reason = ErrCompletionNotInstalled.Error()
|
||||
return installStatus, nil
|
||||
}
|
||||
|
||||
completionContent, err := os.ReadFile(completionFile)
|
||||
if err != nil {
|
||||
return installStatus, fmt.Errorf("could not open existing completion file: %s", err.Error())
|
||||
}
|
||||
|
||||
completionGenerated, err := u.GetCompletionScript(ctx)
|
||||
if err != nil {
|
||||
return installStatus, fmt.Errorf("could not generate cli completions: %s", err)
|
||||
}
|
||||
|
||||
if !strings.EqualFold(string(completionContent), string(completionGenerated)) {
|
||||
installStatus.Status = StatusOutdated
|
||||
installStatus.Reason = ErrCompletionOutdated.Error()
|
||||
return installStatus, nil
|
||||
}
|
||||
|
||||
if u.currentShell == zsh && !u.hasOhMyZsh {
|
||||
hasFpath, err := hasCompletionInFpath(u.zshrc, completionDir)
|
||||
if err != nil || !hasFpath {
|
||||
installStatus.Reason = ErrZshFpathNotFound.Error()
|
||||
return installStatus, nil
|
||||
}
|
||||
f, err := os.Stat(u.zshrc)
|
||||
if err != nil {
|
||||
installStatus.Reason = ErrZshrcCouldNotWrite.Error()
|
||||
return installStatus, nil
|
||||
}
|
||||
if f.Mode().Perm() < 0o600 {
|
||||
installStatus.Reason = ErrZshrcCouldNotWrite.Error()
|
||||
return installStatus, nil
|
||||
}
|
||||
}
|
||||
|
||||
installStatus.Status = StatusInstalled
|
||||
installStatus.Reason = fmt.Sprintf("Shell completion already installed for %s.", u.currentShell)
|
||||
|
||||
return installStatus, nil
|
||||
}
|
|
@ -0,0 +1,435 @@
|
|||
//go:build unix
|
||||
|
||||
package completion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gotest.tools/v3/assert"
|
||||
is "gotest.tools/v3/assert/cmp"
|
||||
)
|
||||
|
||||
//go:embed testdata
|
||||
var fixtures embed.FS
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
type fakeGenerate struct{}
|
||||
|
||||
var _ generateCompletions = (*fakeGenerate)(nil)
|
||||
|
||||
func (f *fakeGenerate) GenBashCompletionV2(w io.Writer, includeDesc bool) error {
|
||||
completions, err := fixtures.ReadFile("testdata/docker.bash")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(completions)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *fakeGenerate) GenZshCompletion(w io.Writer) error {
|
||||
completions, err := fixtures.ReadFile("testdata/docker.zsh")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(completions)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *fakeGenerate) GenFishCompletion(w io.Writer, includeDesc bool) error {
|
||||
completions, err := fixtures.ReadFile("testdata/docker.fish")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = w.Write(completions)
|
||||
return err
|
||||
}
|
||||
|
||||
func newFakeGenerate() generateCompletions {
|
||||
return &fakeGenerate{}
|
||||
}
|
||||
|
||||
func TestDockerCompletion(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("zsh completion", func(t *testing.T) {
|
||||
t.Setenv("SHELL", "/bin/zsh")
|
||||
ds, err := NewShellCompletionSetup("", newFakeGenerate())
|
||||
assert.NilError(t, err)
|
||||
|
||||
completions, err := ds.GetCompletionScript(ctx)
|
||||
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) {
|
||||
t.Setenv("SHELL", "/bin/bash")
|
||||
ds, err := NewShellCompletionSetup("", newFakeGenerate())
|
||||
assert.NilError(t, err)
|
||||
|
||||
completions, err := ds.GetCompletionScript(ctx)
|
||||
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) {
|
||||
t.Setenv("SHELL", "/bin/fish")
|
||||
ds, err := NewShellCompletionSetup("", newFakeGenerate())
|
||||
assert.NilError(t, err)
|
||||
|
||||
completions, err := ds.GetCompletionScript(ctx)
|
||||
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, shellRaw, err := shellFromEnv()
|
||||
if tc.expectedErr != "" {
|
||||
assert.Check(t, is.ErrorContains(err, tc.expectedErr))
|
||||
} else {
|
||||
assert.Equal(t, tc.expected, s)
|
||||
assert.Equal(t, string(tc.expected), shellRaw)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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, err := NewShellCompletionSetup(tmphome, newFakeGenerate())
|
||||
assert.NilError(t, err)
|
||||
return ds.(*unixShellSetup)
|
||||
}
|
||||
|
||||
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, err := newUnixShellSetup(tmphome, newFakeGenerate())
|
||||
assert.NilError(t, err)
|
||||
|
||||
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, err := newUnixShellSetup(tmphome, newFakeGenerate())
|
||||
assert.NilError(t, err)
|
||||
|
||||
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)
|
||||
|
||||
t.Setenv("SHELL", string(tc.shell))
|
||||
ds := tc.setupFunc(t)
|
||||
assert.NilError(t, ds.InstallCompletions(ctx))
|
||||
tc.assertFunc(t, ds)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCompletionDir(t *testing.T) {
|
||||
t.Run("standard shells", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
tmphome := t.TempDir()
|
||||
|
||||
for _, tc := range []struct {
|
||||
shell string
|
||||
expected string
|
||||
}{
|
||||
{"/bin/bash", filepath.Join(tmphome, bashCompletionDir)},
|
||||
{"/bin/zsh", filepath.Join(tmphome, zshCompletionDir)},
|
||||
{"/bin/fish", filepath.Join(tmphome, fishCompletionDir)},
|
||||
} {
|
||||
t.Setenv("SHELL", tc.shell)
|
||||
ds, err := NewShellCompletionSetup(tmphome, newFakeGenerate())
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, tc.expected, ds.GetCompletionDir(ctx))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("oh-my-zsh", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
tmphome := t.TempDir()
|
||||
ohMyZshTmpDir := filepath.Join(tmphome, ".oh-my-zsh")
|
||||
assert.NilError(t, os.MkdirAll(ohMyZshTmpDir, filePerm))
|
||||
t.Setenv("SHELL", "/bin/zsh")
|
||||
t.Setenv("ZSH", ohMyZshTmpDir)
|
||||
ds, err := NewShellCompletionSetup(tmphome, newFakeGenerate())
|
||||
assert.NilError(t, err)
|
||||
assert.Equal(t, filepath.Join(ohMyZshTmpDir, "completions"), ds.GetCompletionDir(ctx))
|
||||
})
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
package completion
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ShellCompletionSetup interface {
|
||||
// Generate completions for the Docker CLI based on the provided shell.
|
||||
GetCompletionScript(ctx context.Context) ([]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) error
|
||||
// Check if the shell completion is already installed.
|
||||
InstallStatus(ctx context.Context) (*ShellCompletionInstallStatus, error)
|
||||
// Get the completion directory for the provided shell.
|
||||
GetCompletionDir(ctx context.Context) string
|
||||
// Get the manual instructions for the provided shell.
|
||||
GetManualInstructions(ctx context.Context) string
|
||||
// Get he current supported shell
|
||||
GetShell() supportedCompletionShell
|
||||
}
|
||||
|
||||
type completionStatus string
|
||||
|
||||
const (
|
||||
StatusInstalled completionStatus = "INSTALLED"
|
||||
StatusNotInstalled completionStatus = "NOT_INSTALLED"
|
||||
StatusUnsupported completionStatus = "UNSUPPORTED"
|
||||
StatusOutdated completionStatus = "OUTDATED"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrShellEnvNotSet = errors.New("SHELL environment variable not set")
|
||||
ErrShellUnsupported = errors.New("unsupported shell")
|
||||
)
|
||||
|
||||
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 ""
|
||||
}
|
||||
|
||||
func (s supportedCompletionShell) Supported() (bool, error) {
|
||||
switch s {
|
||||
case zsh, bash, fish:
|
||||
return true, nil
|
||||
}
|
||||
return false, ErrShellUnsupported
|
||||
}
|
||||
|
||||
type ShellCompletionInstallStatus struct {
|
||||
Status completionStatus
|
||||
Shell string
|
||||
CompletionPath string
|
||||
Reason string
|
||||
}
|
||||
|
||||
type common struct {
|
||||
homeDirectory string
|
||||
command generateCompletions
|
||||
currentShell supportedCompletionShell
|
||||
currentShellRawString string
|
||||
}
|
||||
|
||||
type generateCompletions interface {
|
||||
GenBashCompletionV2(w io.Writer, includeDesc bool) error
|
||||
GenZshCompletion(w io.Writer) error
|
||||
GenFishCompletion(w io.Writer, includeDesc bool) error
|
||||
}
|
||||
|
||||
// shellFromEnv returns the shell type and its name.
|
||||
func shellFromEnv() (supportedCompletionShell, string, error) {
|
||||
currentShell := os.Getenv("SHELL")
|
||||
|
||||
if len(currentShell) == 0 {
|
||||
return "", "", ErrShellEnvNotSet
|
||||
}
|
||||
|
||||
t := strings.Split(currentShell, "/")
|
||||
shellName := t[len(t)-1]
|
||||
|
||||
if ok, err := supportedCompletionShell(shellName).Supported(); !ok {
|
||||
return "", shellName, err
|
||||
}
|
||||
|
||||
return supportedCompletionShell(shellName), shellName, nil
|
||||
}
|
||||
|
||||
type NewShellCompletionOptsFunc func(*common)
|
||||
|
||||
func WithShellOverride(shell string) NewShellCompletionOptsFunc {
|
||||
return func(u *common) {
|
||||
u.currentShell = supportedCompletionShell(shell)
|
||||
u.currentShellRawString = shell
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package completion
|
||||
|
||||
import "context"
|
||||
|
||||
func NewShellCompletionSetup(homeDirectory string, rootCobraCmd generateCompletions, opts ...NewShellCompletionOptsFunc) (ShellCompletionSetup, error) {
|
||||
return newWindowsShellSetup(homeDirectory, rootCobraCmd, opts...)
|
||||
}
|
||||
|
||||
type windowsShellSetup struct {
|
||||
common
|
||||
powershellCompletionDir string
|
||||
}
|
||||
|
||||
var _ ShellCompletionSetup = (*windowsShellSetup)(nil)
|
||||
|
||||
func newWindowsShellSetup(homeDirectory string, rootCobraCmd generateCompletions, opts ...NewShellCompletionOptsFunc) (*windowsShellSetup, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (w *windowsShellSetup) GetCompletionScript(ctx context.Context) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (w *windowsShellSetup) GetCompletionDir(ctx context.Context) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *windowsShellSetup) InstallCompletions(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *windowsShellSetup) GetShell() supportedCompletionShell {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *windowsShellSetup) GetManualInstructions(ctx context.Context) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (w *windowsShellSetup) InstallStatus(ctx context.Context) (*ShellCompletionInstallStatus, error) {
|
||||
return nil, nil
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
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 {
|
||||
|
||||
switch args[0] {
|
||||
case "install":
|
||||
|
||||
userHome, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := []NewShellCompletionOptsFunc{}
|
||||
if cmd.Flag("shell").Changed {
|
||||
opts = append(opts, WithShellOverride(cmd.Flag("shell").Value.String()))
|
||||
}
|
||||
|
||||
shellSetup, err := NewShellCompletionSetup(userHome, cmd.Root(), opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmd.Flag("manual").Changed {
|
||||
_, _ = fmt.Fprint(dockerCli.Out(), shellSetup.GetManualInstructions(cmd.Context()))
|
||||
return nil
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("\nDetected shell [%s]\n\nThe automatic installer will do the following:\n\n%s\n\nAre you sure you want to continue?", shellSetup.GetShell(), shellSetup.GetManualInstructions(cmd.Context()))
|
||||
ok, err := command.PromptForConfirmation(cmd.Context(), dockerCli.In(), dockerCli.Out(), msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return shellSetup.InstallCompletions(cmd.Context())
|
||||
case "bash":
|
||||
|
||||
return cmd.Root().GenBashCompletionV2(dockerCli.Out(), true)
|
||||
case "zsh":
|
||||
return cmd.Root().GenZshCompletion(dockerCli.Out())
|
||||
case "fish":
|
||||
return cmd.Root().GenFishCompletion(dockerCli.Out(), true)
|
||||
default:
|
||||
return command.ShowHelp(dockerCli.Err())(cmd, args)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().Bool("manual", false, "Display instructions for installing autocompletion")
|
||||
cmd.PersistentFlags().String("shell", "", "Shell type for autocompletion (bash, zsh, fish, powershell)")
|
||||
|
||||
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
|
|
@ -180,7 +180,7 @@ func toWslPath(s string) string {
|
|||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("mnt/%s%s", drive, p)
|
||||
return fmt.Sprintf("mnt/%s%s", strings.ToLower(drive), p)
|
||||
}
|
||||
|
||||
func parseUNCPath(s string) (drive, p string, ok bool) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/url"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
@ -9,21 +10,48 @@ import (
|
|||
)
|
||||
|
||||
func TestWslSocketPath(t *testing.T) {
|
||||
u, err := url.Parse("unix:////./c:/my/file/path")
|
||||
assert.NilError(t, err)
|
||||
|
||||
// Ensure host is empty.
|
||||
assert.Equal(t, u.Host, "")
|
||||
|
||||
// Use a filesystem where the WSL path exists.
|
||||
fs := fstest.MapFS{
|
||||
"mnt/c/my/file/path": {},
|
||||
testCases := []struct {
|
||||
doc string
|
||||
fs fs.FS
|
||||
url string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
doc: "filesystem where WSL path does not exist",
|
||||
fs: fstest.MapFS{
|
||||
"my/file/path": {},
|
||||
},
|
||||
url: "unix:////./c:/my/file/path",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
doc: "filesystem where WSL path exists",
|
||||
fs: fstest.MapFS{
|
||||
"mnt/c/my/file/path": {},
|
||||
},
|
||||
url: "unix:////./c:/my/file/path",
|
||||
expected: "/mnt/c/my/file/path",
|
||||
},
|
||||
{
|
||||
doc: "filesystem where WSL path exists uppercase URL",
|
||||
fs: fstest.MapFS{
|
||||
"mnt/c/my/file/path": {},
|
||||
},
|
||||
url: "unix:////./C:/my/file/path",
|
||||
expected: "/mnt/c/my/file/path",
|
||||
},
|
||||
}
|
||||
assert.Equal(t, wslSocketPath(u.Path, fs), "/mnt/c/my/file/path")
|
||||
|
||||
// Use a filesystem where the WSL path doesn't exist.
|
||||
fs = fstest.MapFS{
|
||||
"my/file/path": {},
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.doc, func(t *testing.T) {
|
||||
u, err := url.Parse(tc.url)
|
||||
assert.NilError(t, err)
|
||||
// Ensure host is empty.
|
||||
assert.Equal(t, u.Host, "")
|
||||
|
||||
result := wslSocketPath(u.Path, tc.fs)
|
||||
|
||||
assert.Equal(t, result, tc.expected)
|
||||
})
|
||||
}
|
||||
assert.Equal(t, wslSocketPath(u.Path, fs), "")
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"github.com/docker/cli/cli/version"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
@ -94,7 +95,9 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue)
|
|||
metric.WithAttributes(cmdStatusAttrs...),
|
||||
)
|
||||
if mp, ok := mp.(MeterProvider); ok {
|
||||
mp.ForceFlush(ctx)
|
||||
if err := mp.ForceFlush(ctx); err != nil {
|
||||
otel.Handle(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -358,7 +358,9 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
|||
|
||||
mp := dockerCli.MeterProvider()
|
||||
if mp, ok := mp.(command.MeterProvider); ok {
|
||||
defer mp.Shutdown(ctx)
|
||||
if err := mp.Shutdown(ctx); err != nil {
|
||||
otel.Handle(err)
|
||||
}
|
||||
} else {
|
||||
fmt.Fprint(dockerCli.Err(), "Warning: Unexpected OTEL error, metrics may not be flushed")
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue