diff --git a/cli/command/container/completion.go b/cli/command/container/completion.go index 97d32728fa84..5b1816d2997f 100644 --- a/cli/command/container/completion.go +++ b/cli/command/container/completion.go @@ -18,6 +18,24 @@ import ( // allCaps is the magic value for "all capabilities". const allCaps = "ALL" +// completeCopy delegates local path completion to the shell. Container paths +// cannot be completed from the local filesystem. +func completeCopy(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) >= 2 || toComplete == "-" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + if ctr, _ := splitCpArg(toComplete); ctr != "" { + return nil, cobra.ShellCompDirectiveNoFileComp + } + if len(args) == 1 { + if ctr, _ := splitCpArg(args[0]); ctr == "" { + // A local source (including stdin) requires a container destination. + return nil, cobra.ShellCompDirectiveNoFileComp + } + } + return nil, cobra.ShellCompDirectiveDefault +} + // allLinuxCapabilities is a list of all known Linux capabilities. // // TODO(thaJeztah): add descriptions, and enable descriptions for our completion scripts (cobra.CompletionOptions.DisableDescriptions is currently set to "true") diff --git a/cli/command/container/cp.go b/cli/command/container/cp.go index 52fc06ab3e21..aa59d19a24ac 100644 --- a/cli/command/container/cp.go +++ b/cli/command/container/cp.go @@ -154,6 +154,7 @@ container source to stdout.`, Annotations: map[string]string{ "aliases": "docker container cp, docker cp", }, + ValidArgsFunction: completeCopy, DisableFlagsInUseLine: true, } diff --git a/cli/command/container/cp_test.go b/cli/command/container/cp_test.go index d2ce73b0d30d..2d28d9b85470 100644 --- a/cli/command/container/cp_test.go +++ b/cli/command/container/cp_test.go @@ -1,10 +1,13 @@ package container import ( + "bytes" "context" "errors" + "fmt" "io" "os" + "path/filepath" "runtime" "strings" "testing" @@ -14,11 +17,65 @@ import ( "github.com/moby/go-archive/compression" "github.com/moby/moby/api/types/container" "github.com/moby/moby/client" + "github.com/spf13/cobra" "gotest.tools/v3/assert" is "gotest.tools/v3/assert/cmp" "gotest.tools/v3/fs" ) +func TestCopyCompletion(t *testing.T) { + absolutePath := filepath.Join(t.TempDir(), "file") + if runtime.GOOS != "windows" { + absolutePath += ":name" + } + tests := []struct { + name string + args []string + directive cobra.ShellCompDirective + }{ + {name: "empty source", args: []string{""}}, + {name: "relative source", args: []string{"./"}}, + {name: "source in home directory", args: []string{"~/"}}, + {name: "absolute source with colon", args: []string{absolutePath}}, + {name: "relative source with colon", args: []string{"./file:name"}}, + {name: "container source", args: []string{"example:/tmp/"}, directive: cobra.ShellCompDirectiveNoFileComp}, + {name: "empty container path", args: []string{"example:"}, directive: cobra.ShellCompDirectiveNoFileComp}, + {name: "stdin source", args: []string{"--", "-"}, directive: cobra.ShellCompDirectiveNoFileComp}, + {name: "empty local destination", args: []string{"example:/tmp/file", ""}}, + {name: "relative destination", args: []string{"example:/tmp/file", "./"}}, + {name: "destination in home directory", args: []string{"example:/tmp/file", "~/"}}, + {name: "absolute destination with colon", args: []string{"example:/tmp/file", absolutePath}}, + {name: "container destination", args: []string{"./file", "example:/tmp/"}, directive: cobra.ShellCompDirectiveNoFileComp}, + {name: "destination after local source", args: []string{"./file", ""}, directive: cobra.ShellCompDirectiveNoFileComp}, + {name: "destination after stdin", args: []string{"-", ""}, directive: cobra.ShellCompDirectiveNoFileComp}, + {name: "stdout destination", args: []string{"--", "example:/tmp/file", "-"}, directive: cobra.ShellCompDirectiveNoFileComp}, + {name: "extra argument", args: []string{"example:/tmp/file", "./file", ""}, directive: cobra.ShellCompDirectiveNoFileComp}, + } + for _, commandPath := range [][]string{{"cp"}, {"container", "cp"}} { + t.Run(strings.Join(commandPath, " "), func(t *testing.T) { + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + root := &cobra.Command{Use: "docker"} + root.CompletionOptions.SetDefaultShellCompDirective(cobra.ShellCompDirectiveNoFileComp) + parent := root + if len(commandPath) == 2 { + parent = &cobra.Command{Use: "container"} + root.AddCommand(parent) + } + parent.AddCommand(newCopyCommand(test.NewFakeCli(nil))) + var out bytes.Buffer + root.SetOut(&out) + root.SetErr(io.Discard) + args := append([]string{cobra.ShellCompNoDescRequestCmd}, commandPath...) + root.SetArgs(append(args, tc.args...)) + assert.NilError(t, root.Execute()) + assert.Equal(t, out.String(), fmt.Sprintf(":%d\n", tc.directive)) + }) + } + }) + } +} + func TestRunCopyWithInvalidArguments(t *testing.T) { testcases := []struct { doc string diff --git a/docs/reference/commandline/container_cp.md b/docs/reference/commandline/container_cp.md index 8d8958bb37b7..c71cf8e3e4b8 100644 --- a/docs/reference/commandline/container_cp.md +++ b/docs/reference/commandline/container_cp.md @@ -40,6 +40,9 @@ be an absolute or relative value. The command interprets a local machine's relative paths as relative to the current working directory where `docker cp` is run. +When shell completion is enabled, `docker cp` completes local file and directory +paths. Completion of paths inside containers is not supported. + The `cp` command behaves like the Unix `cp -a` command in that directories are copied recursively with permissions preserved if possible. Ownership is set to the user and primary group at the destination. For example, files copied to a