Skip to content

Commit b3e4bde

Browse files
olivergsHarryMichal
andcommitted
cmd: Add shell completion command & generate completion
Cobra (the CLI library) has an advanced support for generating shell completion. It support Bash, Zsh, Fish and PowerShell. This offering covers the majority of use cases with some exceptions, of course. The generated completion scripts have one behavioral difference when compared to the existing solution: flags (--xxx) are not shown by default. User needs to type '-' first to get the completion. containers#840 Co-authored-by: Ondřej Míchal <[email protected]>
1 parent 8bcb56a commit b3e4bde

File tree

11 files changed

+257
-21
lines changed

11 files changed

+257
-21
lines changed

src/cmd/completion.go

+206
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"strings"
6+
7+
"github.com/containers/toolbox/pkg/utils"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var completionCmd = &cobra.Command{
12+
Use: "completion [bash|zsh|fish|powershell]",
13+
Short: "Generate completion script",
14+
Long: `To load completions:
15+
16+
Bash:
17+
18+
$ source <(toolbox completion bash)
19+
20+
# To load completions for each session, execute once:
21+
# Linux:
22+
$ toolbox completion bash > /etc/bash_completion.d/toolbox
23+
# macOS:
24+
$ toolbox completion bash > /usr/local/etc/bash_completion.d/toolbox
25+
26+
Zsh:
27+
28+
# If shell completion is not already enabled in your environment,
29+
# you will need to enable it. You can execute the following once:
30+
31+
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
32+
33+
# To load completions for each session, execute once:
34+
$ toolbox completion zsh > "${fpath[1]}/_toolbox"
35+
36+
# You will need to start a new shell for this setup to take effect.
37+
38+
fish:
39+
40+
$ toolbox completion fish | source
41+
42+
# To load completions for each session, execute once:
43+
$ toolbox completion fish > ~/.config/fish/completions/toolbox.fish
44+
45+
`,
46+
Hidden: true,
47+
DisableFlagsInUseLine: true,
48+
ValidArgs: []string{"bash", "zsh", "fish"},
49+
Args: cobra.ExactValidArgs(1),
50+
Run: func(cmd *cobra.Command, args []string) {
51+
switch args[0] {
52+
case "bash":
53+
cmd.Root().GenBashCompletion(os.Stdout)
54+
case "zsh":
55+
cmd.Root().GenZshCompletion(os.Stdout)
56+
case "fish":
57+
cmd.Root().GenFishCompletion(os.Stdout, true)
58+
}
59+
},
60+
}
61+
62+
func init() {
63+
rootCmd.AddCommand(completionCmd)
64+
}
65+
66+
func completionEmpty(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
67+
return nil, cobra.ShellCompDirectiveNoFileComp
68+
}
69+
70+
func completionCommands(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
71+
commandNames := []string{}
72+
commands := cmd.Root().Commands()
73+
for _, command := range commands {
74+
if strings.Contains(command.Name(), "complet") {
75+
continue
76+
}
77+
commandNames = append(commandNames, command.Name())
78+
}
79+
80+
return commandNames, cobra.ShellCompDirectiveNoFileComp
81+
}
82+
83+
func completionContainerNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
84+
containerNames := []string{}
85+
if containers, err := getContainers(); err == nil {
86+
for _, container := range containers {
87+
containerNames = append(containerNames, container.Names[0])
88+
}
89+
}
90+
91+
if len(containerNames) == 0 {
92+
return nil, cobra.ShellCompDirectiveNoFileComp
93+
}
94+
95+
return containerNames, cobra.ShellCompDirectiveNoFileComp
96+
}
97+
98+
func completionContainerNamesFiltered(cmd *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
99+
if cmd.Name() == "enter" && len(args) >= 1 {
100+
return nil, cobra.ShellCompDirectiveNoFileComp
101+
}
102+
103+
containerNames := []string{}
104+
if containers, err := getContainers(); err == nil {
105+
for _, container := range containers {
106+
skip := false
107+
for _, arg := range args {
108+
if container.Names[0] == arg {
109+
skip = true
110+
break
111+
}
112+
}
113+
114+
if skip {
115+
continue
116+
}
117+
118+
containerNames = append(containerNames, container.Names[0])
119+
}
120+
}
121+
122+
if len(containerNames) == 0 {
123+
return nil, cobra.ShellCompDirectiveNoFileComp
124+
}
125+
126+
return containerNames, cobra.ShellCompDirectiveNoFileComp
127+
128+
}
129+
130+
func completionDistroNames(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
131+
imageFlag := cmd.Flag("image")
132+
if imageFlag != nil && imageFlag.Changed {
133+
return nil, cobra.ShellCompDirectiveNoFileComp
134+
}
135+
136+
distros := []string{}
137+
supportedDistros := utils.GetSupportedDistros()
138+
for key := range supportedDistros {
139+
distros = append(distros, key)
140+
}
141+
142+
return distros, cobra.ShellCompDirectiveNoFileComp
143+
}
144+
145+
func completionImageNames(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
146+
distroFlag := cmd.Flag("distro")
147+
if distroFlag != nil && distroFlag.Changed {
148+
return nil, cobra.ShellCompDirectiveNoFileComp
149+
}
150+
151+
imageNames := []string{}
152+
if images, err := getImages(); err == nil {
153+
for _, image := range images {
154+
if len(image.Names) > 0 {
155+
imageNames = append(imageNames, image.Names[0])
156+
} else {
157+
imageNames = append(imageNames, image.ID)
158+
}
159+
}
160+
}
161+
162+
if len(imageNames) == 0 {
163+
return nil, cobra.ShellCompDirectiveNoFileComp
164+
}
165+
166+
return imageNames, cobra.ShellCompDirectiveNoFileComp
167+
}
168+
169+
func completionImageNamesFiltered(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
170+
imageNames := []string{}
171+
if images, err := getImages(); err == nil {
172+
for _, image := range images {
173+
skip := false
174+
var imageName string
175+
176+
if len(image.Names) > 0 {
177+
imageName = image.Names[0]
178+
} else {
179+
imageName = image.ID
180+
}
181+
182+
for _, arg := range args {
183+
if arg == imageName {
184+
skip = true
185+
break
186+
}
187+
}
188+
189+
if skip {
190+
continue
191+
}
192+
193+
imageNames = append(imageNames, imageName)
194+
}
195+
}
196+
197+
if len(imageNames) == 0 {
198+
return nil, cobra.ShellCompDirectiveNoFileComp
199+
}
200+
201+
return imageNames, cobra.ShellCompDirectiveNoFileComp
202+
}
203+
204+
func completionLogLevels(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
205+
return []string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, cobra.ShellCompDirectiveNoFileComp
206+
}

src/cmd/create.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,10 @@ var (
5858
)
5959

6060
var createCmd = &cobra.Command{
61-
Use: "create",
62-
Short: "Create a new toolbox container",
63-
RunE: create,
61+
Use: "create",
62+
Short: "Create a new toolbox container",
63+
RunE: create,
64+
ValidArgsFunction: completionEmpty,
6465
}
6566

6667
func init() {
@@ -91,6 +92,10 @@ func init() {
9192
"Create a toolbox container for a different operating system release than the host")
9293

9394
createCmd.SetHelpFunc(createHelp)
95+
96+
createCmd.RegisterFlagCompletionFunc("distro", completionDistroNames)
97+
createCmd.RegisterFlagCompletionFunc("image", completionImageNames)
98+
9499
rootCmd.AddCommand(createCmd)
95100
}
96101

src/cmd/enter.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ var (
3535
)
3636

3737
var enterCmd = &cobra.Command{
38-
Use: "enter",
39-
Short: "Enter a toolbox container for interactive use",
40-
RunE: enter,
38+
Use: "enter",
39+
Short: "Enter a toolbox container for interactive use",
40+
RunE: enter,
41+
ValidArgsFunction: completionContainerNamesFiltered,
4142
}
4243

4344
func init() {
@@ -61,6 +62,9 @@ func init() {
6162
"",
6263
"Enter a toolbox container for a different operating system release than the host")
6364

65+
enterCmd.RegisterFlagCompletionFunc("container", completionContainerNames)
66+
enterCmd.RegisterFlagCompletionFunc("distro", completionDistroNames)
67+
6468
enterCmd.SetHelpFunc(enterHelp)
6569
rootCmd.AddCommand(enterCmd)
6670
}

src/cmd/help.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ import (
2626
)
2727

2828
var helpCmd = &cobra.Command{
29-
Use: "help",
30-
Short: "Display help information about Toolbox",
31-
RunE: help,
29+
Use: "help",
30+
Short: "Display help information about Toolbox",
31+
RunE: help,
32+
ValidArgsFunction: completionCommands,
3233
}
3334

3435
func init() {

src/cmd/list.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,10 @@ var (
6060
)
6161

6262
var listCmd = &cobra.Command{
63-
Use: "list",
64-
Short: "List existing toolbox containers and images",
65-
RunE: list,
63+
Use: "list",
64+
Short: "List existing toolbox containers and images",
65+
RunE: list,
66+
ValidArgsFunction: completionEmpty,
6667
}
6768

6869
func init() {

src/cmd/rm.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ var (
3535
)
3636

3737
var rmCmd = &cobra.Command{
38-
Use: "rm",
39-
Short: "Remove one or more toolbox containers",
40-
RunE: rm,
38+
Use: "rm",
39+
Short: "Remove one or more toolbox containers",
40+
RunE: rm,
41+
ValidArgsFunction: completionContainerNamesFiltered,
4142
}
4243

4344
func init() {

src/cmd/rmi.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ var (
3535
)
3636

3737
var rmiCmd = &cobra.Command{
38-
Use: "rmi",
39-
Short: "Remove one or more toolbox images",
40-
RunE: rmi,
38+
Use: "rmi",
39+
Short: "Remove one or more toolbox images",
40+
RunE: rmi,
41+
ValidArgsFunction: completionImageNamesFiltered,
4142
}
4243

4344
func init() {

src/cmd/root.go

+2
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ func init() {
9595

9696
persistentFlags.CountVarP(&rootFlags.verbose, "verbose", "v", "Set log-level to 'debug'")
9797

98+
rootCmd.RegisterFlagCompletionFunc("log-level", completionLogLevels)
99+
98100
rootCmd.SetHelpFunc(rootHelp)
99101

100102
usageTemplate := fmt.Sprintf(`Run '%s --help' for usage.`, executableBase)

src/cmd/run.go

+8-3
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ var (
4242
)
4343

4444
var runCmd = &cobra.Command{
45-
Use: "run",
46-
Short: "Run a command in an existing toolbox container",
47-
RunE: run,
45+
Use: "run",
46+
Short: "Run a command in an existing toolbox container",
47+
RunE: run,
48+
ValidArgsFunction: completionEmpty,
4849
}
4950

5051
func init() {
@@ -70,6 +71,10 @@ func init() {
7071
"Run command inside a toolbox container for a different operating system release than the host")
7172

7273
runCmd.SetHelpFunc(runHelp)
74+
75+
runCmd.RegisterFlagCompletionFunc("container", completionContainerNames)
76+
runCmd.RegisterFlagCompletionFunc("distro", completionDistroNames)
77+
7378
rootCmd.AddCommand(runCmd)
7479
}
7580

src/meson.build

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ go_build_wrapper_program = find_program('go-build-wrapper')
33

44
sources = files(
55
'toolbox.go',
6+
'cmd/completion.go',
67
'cmd/create.go',
78
'cmd/enter.go',
89
'cmd/help.go',

src/pkg/utils/utils.go

+9
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,15 @@ func GetRuntimeDirectory(targetUser *user.User) (string, error) {
446446
return toolboxRuntimeDirectory, nil
447447
}
448448

449+
// GetSupportedDistros returns the names of all supported distributions
450+
//
451+
// The form of the names is as found in VARIANT_ID in os-release[0]
452+
//
453+
// [0] https://www.freedesktop.org/software/systemd/man/os-release.html
454+
func GetSupportedDistros() map[string]Distro {
455+
return supportedDistros
456+
}
457+
449458
// HumanDuration accepts a Unix time value and converts it into a human readable
450459
// string.
451460
//

0 commit comments

Comments
 (0)