Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cli/command/container/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
1 change: 1 addition & 0 deletions cli/command/container/cp.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ container source to stdout.`,
Annotations: map[string]string{
"aliases": "docker container cp, docker cp",
},
ValidArgsFunction: completeCopy,
DisableFlagsInUseLine: true,
}

Expand Down
57 changes: 57 additions & 0 deletions cli/command/container/cp_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package container

import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions docs/reference/commandline/container_cp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading