Skip to content

Commit 0e75283

Browse files
authored
Merge pull request #6016 from thaJeztah/context_completion
context: add shell-completion for context-names
2 parents 659b026 + 6fd72c6 commit 0e75283

File tree

8 files changed

+147
-13
lines changed

8 files changed

+147
-13
lines changed

cli/command/context/completion.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
2+
//go:build go1.22
3+
4+
package context
5+
6+
import (
7+
"slices"
8+
9+
"github.com/docker/cli/cli/context/store"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
type contextProvider interface {
14+
ContextStore() store.Store
15+
CurrentContext() string
16+
}
17+
18+
// completeContextNames implements shell completion for context-names.
19+
//
20+
// FIXME(thaJeztah): export, and remove duplicate of this function in cmd/docker.
21+
func completeContextNames(dockerCLI contextProvider, limit int, withFileComp bool) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
22+
return func(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
23+
if limit > 0 && len(args) >= limit {
24+
if withFileComp {
25+
// Provide file/path completion after context name (for "docker context export")
26+
return nil, cobra.ShellCompDirectiveDefault
27+
}
28+
return nil, cobra.ShellCompDirectiveNoFileComp
29+
}
30+
31+
// TODO(thaJeztah): implement function similar to [store.Names] to (also) include descriptions.
32+
names, _ := store.Names(dockerCLI.ContextStore())
33+
out := make([]string, 0, len(names))
34+
for _, name := range names {
35+
if slices.Contains(args, name) {
36+
// Already completed
37+
continue
38+
}
39+
if name == dockerCLI.CurrentContext() {
40+
name += "\tcurrent"
41+
}
42+
out = append(out, name)
43+
}
44+
return out, cobra.ShellCompDirectiveNoFileComp
45+
}
46+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package context
2+
3+
import (
4+
"testing"
5+
6+
"github.com/docker/cli/cli/context/store"
7+
"github.com/spf13/cobra"
8+
"gotest.tools/v3/assert"
9+
is "gotest.tools/v3/assert/cmp"
10+
)
11+
12+
type fakeContextProvider struct {
13+
contextStore store.Store
14+
}
15+
16+
func (c *fakeContextProvider) ContextStore() store.Store {
17+
return c.contextStore
18+
}
19+
20+
func (*fakeContextProvider) CurrentContext() string {
21+
return "default"
22+
}
23+
24+
type fakeContextStore struct {
25+
store.Store
26+
names []string
27+
}
28+
29+
func (f fakeContextStore) List() (c []store.Metadata, _ error) {
30+
for _, name := range f.names {
31+
c = append(c, store.Metadata{Name: name})
32+
}
33+
return c, nil
34+
}
35+
36+
func TestCompleteContextNames(t *testing.T) {
37+
allNames := []string{"context-b", "context-c", "context-a"}
38+
cli := &fakeContextProvider{
39+
contextStore: fakeContextStore{
40+
names: allNames,
41+
},
42+
}
43+
44+
t.Run("with limit", func(t *testing.T) {
45+
compFunc := completeContextNames(cli, 1, false)
46+
values, directives := compFunc(nil, nil, "")
47+
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
48+
assert.Check(t, is.DeepEqual(values, allNames))
49+
50+
values, directives = compFunc(nil, []string{"context-c"}, "")
51+
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
52+
assert.Check(t, is.Len(values, 0))
53+
})
54+
55+
t.Run("with limit and file completion", func(t *testing.T) {
56+
compFunc := completeContextNames(cli, 1, true)
57+
values, directives := compFunc(nil, nil, "")
58+
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
59+
assert.Check(t, is.DeepEqual(values, allNames))
60+
61+
values, directives = compFunc(nil, []string{"context-c"}, "")
62+
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveDefault), "should provide filenames completion after limit")
63+
assert.Check(t, is.Len(values, 0))
64+
})
65+
66+
t.Run("without limits", func(t *testing.T) {
67+
compFunc := completeContextNames(cli, -1, false)
68+
values, directives := compFunc(nil, []string{"context-c"}, "")
69+
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
70+
assert.Check(t, is.DeepEqual(values, []string{"context-b", "context-a"}), "should not contain already completed")
71+
72+
values, directives = compFunc(nil, []string{"context-c", "context-a"}, "")
73+
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp))
74+
assert.Check(t, is.DeepEqual(values, []string{"context-b"}), "should not contain already completed")
75+
76+
values, directives = compFunc(nil, []string{"context-c", "context-a", "context-b"}, "")
77+
assert.Check(t, is.Equal(directives, cobra.ShellCompDirectiveNoFileComp), "should provide filenames completion after limit")
78+
assert.Check(t, is.Len(values, 0))
79+
})
80+
}

cli/command/context/export.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type ExportOptions struct {
1818
Dest string
1919
}
2020

21-
func newExportCommand(dockerCli command.Cli) *cobra.Command {
21+
func newExportCommand(dockerCLI command.Cli) *cobra.Command {
2222
return &cobra.Command{
2323
Use: "export [OPTIONS] CONTEXT [FILE|-]",
2424
Short: "Export a context to a tar archive FILE or a tar stream on STDOUT.",
@@ -32,8 +32,9 @@ func newExportCommand(dockerCli command.Cli) *cobra.Command {
3232
} else {
3333
opts.Dest = opts.ContextName + ".dockercontext"
3434
}
35-
return RunExport(dockerCli, opts)
35+
return RunExport(dockerCLI, opts)
3636
},
37+
ValidArgsFunction: completeContextNames(dockerCLI, 1, true),
3738
}
3839
}
3940

cli/command/context/import.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77

88
"github.com/docker/cli/cli"
99
"github.com/docker/cli/cli/command"
10+
"github.com/docker/cli/cli/command/completion"
1011
"github.com/docker/cli/cli/context/store"
1112
"github.com/spf13/cobra"
1213
)
@@ -19,6 +20,8 @@ func newImportCommand(dockerCli command.Cli) *cobra.Command {
1920
RunE: func(cmd *cobra.Command, args []string) error {
2021
return RunImport(dockerCli, args[0], args[1])
2122
},
23+
// TODO(thaJeztah): this should also include "-"
24+
ValidArgsFunction: completion.FileNames,
2225
}
2326
return cmd
2427
}

cli/command/context/inspect.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ type inspectOptions struct {
1919
}
2020

2121
// newInspectCommand creates a new cobra.Command for `docker context inspect`
22-
func newInspectCommand(dockerCli command.Cli) *cobra.Command {
22+
func newInspectCommand(dockerCLI command.Cli) *cobra.Command {
2323
var opts inspectOptions
2424

2525
cmd := &cobra.Command{
@@ -28,13 +28,14 @@ func newInspectCommand(dockerCli command.Cli) *cobra.Command {
2828
RunE: func(cmd *cobra.Command, args []string) error {
2929
opts.refs = args
3030
if len(opts.refs) == 0 {
31-
if dockerCli.CurrentContext() == "" {
31+
if dockerCLI.CurrentContext() == "" {
3232
return errors.New("no context specified")
3333
}
34-
opts.refs = []string{dockerCli.CurrentContext()}
34+
opts.refs = []string{dockerCLI.CurrentContext()}
3535
}
36-
return runInspect(dockerCli, opts)
36+
return runInspect(dockerCLI, opts)
3737
},
38+
ValidArgsFunction: completeContextNames(dockerCLI, -1, false),
3839
}
3940

4041
flags := cmd.Flags()

cli/command/context/remove.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,17 @@ type RemoveOptions struct {
1616
Force bool
1717
}
1818

19-
func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
19+
func newRemoveCommand(dockerCLI command.Cli) *cobra.Command {
2020
var opts RemoveOptions
2121
cmd := &cobra.Command{
2222
Use: "rm CONTEXT [CONTEXT...]",
2323
Aliases: []string{"remove"},
2424
Short: "Remove one or more contexts",
2525
Args: cli.RequiresMinArgs(1),
2626
RunE: func(cmd *cobra.Command, args []string) error {
27-
return RunRemove(dockerCli, opts, args)
27+
return RunRemove(dockerCLI, opts, args)
2828
},
29+
ValidArgsFunction: completeContextNames(dockerCLI, -1, false),
2930
}
3031
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Force the removal of a context in use")
3132
return cmd

cli/command/context/update.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,18 @@ func longUpdateDescription() string {
3333
return buf.String()
3434
}
3535

36-
func newUpdateCommand(dockerCli command.Cli) *cobra.Command {
36+
func newUpdateCommand(dockerCLI command.Cli) *cobra.Command {
3737
opts := &UpdateOptions{}
3838
cmd := &cobra.Command{
3939
Use: "update [OPTIONS] CONTEXT",
4040
Short: "Update a context",
4141
Args: cli.ExactArgs(1),
4242
RunE: func(cmd *cobra.Command, args []string) error {
4343
opts.Name = args[0]
44-
return RunUpdate(dockerCli, opts)
44+
return RunUpdate(dockerCLI, opts)
4545
},
46-
Long: longUpdateDescription(),
46+
Long: longUpdateDescription(),
47+
ValidArgsFunction: completeContextNames(dockerCLI, 1, false),
4748
}
4849
flags := cmd.Flags()
4950
flags.StringVar(&opts.Description, "description", "", "Description of the context")

cli/command/context/use.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,16 @@ import (
1010
"github.com/spf13/cobra"
1111
)
1212

13-
func newUseCommand(dockerCli command.Cli) *cobra.Command {
13+
func newUseCommand(dockerCLI command.Cli) *cobra.Command {
1414
cmd := &cobra.Command{
1515
Use: "use CONTEXT",
1616
Short: "Set the current docker context",
1717
Args: cobra.ExactArgs(1),
1818
RunE: func(cmd *cobra.Command, args []string) error {
1919
name := args[0]
20-
return RunUse(dockerCli, name)
20+
return RunUse(dockerCLI, name)
2121
},
22+
ValidArgsFunction: completeContextNames(dockerCLI, 1, false),
2223
}
2324
return cmd
2425
}

0 commit comments

Comments
 (0)