From dc7255c65d353ad5781d72d650dbeeceb90d15db Mon Sep 17 00:00:00 2001 From: Alexandre Rodrigues Date: Fri, 13 Nov 2026 21:31:39 -0300 Subject: [PATCH] container: Complete local paths for docker cp Delegate local source and destination path completion to the shell, while suppressing local suggestions for container paths and extra arguments. Cover both command forms through Cobra completion requests. Related to: https://github.com/docker/cli/issues/3040 Signed-off-by: Alexandre Rodrigues --- cli/command/container/completion.go | 18 +++++++ cli/command/container/cp.go | 1 + cli/command/container/cp_test.go | 57 ++++++++++++++++++++++ docs/reference/commandline/container_cp.md | 3 ++ 4 files changed, 79 insertions(+) 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