Compare commits

...

9 Commits

Author SHA1 Message Date
Alano Terblanche c8a2bc832b
Merge a123a86a5e into a5fb752ecf 2024-09-18 15:34:58 +01:00
Sebastiaan van Stijn a5fb752ecf
Merge pull request #5445 from jsternberg/lowercase-windows-drive
command: change drive to lowercase for wsl path
2024-09-18 12:15:45 +02:00
Laura Brehm 4e64c59d64
Merge pull request #5446 from thaJeztah/codeql_updates
gha: update codeql workflow to go1.22.7
2024-09-18 11:04:32 +01:00
Jonathan A. Sternberg 3472bbc28a
command: change drive to lowercase for wsl path
On Windows, the drive casing doesn't matter outside of WSL. For WSL, the
drives are lowercase. When we're producing a WSL path, lowercase the
drive letter.

Co-authored-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
Co-authored-by: Laura Brehm <laurabrehm@hey.com>

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
Signed-off-by: Laura Brehm <laurabrehm@hey.com>
2024-09-18 10:59:08 +01:00
Laura Brehm 649e564ee0
Merge pull request #5444 from jsternberg/handle-otel-errors
telemetry: pass otel errors to the otel handler for shutdown and force flush
2024-09-18 10:39:55 +01:00
Sebastiaan van Stijn e1213edcc6
gha: update codeql workflow to go1.22.7
commit d7d56599ca updated this
repository to go1.22, but the codeql action didn't specify a
patch version, and was missed.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2024-09-17 21:39:56 +02:00
Jonathan A. Sternberg b1956f5073
telemetry: pass otel errors to the otel handler for shutdown and force flush
Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
2024-09-17 10:47:04 -05:00
Alano Terblanche a123a86a5e
feat: improve completion installer
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2024-06-13 14:59:32 +02:00
Alano Terblanche b761fe9d76
feat: autocompletion installer
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
2024-04-30 15:20:46 +02:00
14 changed files with 1818 additions and 18 deletions

View File

@ -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

View File

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

View File

@ -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
}

View File

@ -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))
})
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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) {

View File

@ -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), "")
}

View File

@ -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)
}
}
}
}

View File

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