Skip to content

Commit

Permalink
cmd: Add shell completion command & generate completion
Browse files Browse the repository at this point in the history
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 <harrymichal@seznam.cz>
  • Loading branch information
olivergs and HarryMichal committed Nov 21, 2021
1 parent 8bcb56a commit b3e4bde
Show file tree
Hide file tree
Showing 11 changed files with 257 additions and 21 deletions.
206 changes: 206 additions & 0 deletions src/cmd/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
package cmd

import (
"os"
"strings"

"github.com/containers/toolbox/pkg/utils"
"github.com/spf13/cobra"
)

var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: `To load completions:
Bash:
$ source <(toolbox completion bash)
# To load completions for each session, execute once:
# Linux:
$ toolbox completion bash > /etc/bash_completion.d/toolbox
# macOS:
$ toolbox completion bash > /usr/local/etc/bash_completion.d/toolbox
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ toolbox completion zsh > "${fpath[1]}/_toolbox"
# You will need to start a new shell for this setup to take effect.
fish:
$ toolbox completion fish | source
# To load completions for each session, execute once:
$ toolbox completion fish > ~/.config/fish/completions/toolbox.fish
`,
Hidden: true,
DisableFlagsInUseLine: true,
ValidArgs: []string{"bash", "zsh", "fish"},
Args: cobra.ExactValidArgs(1),
Run: func(cmd *cobra.Command, args []string) {
switch args[0] {
case "bash":
cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
cmd.Root().GenFishCompletion(os.Stdout, true)
}
},
}

func init() {
rootCmd.AddCommand(completionCmd)
}

func completionEmpty(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return nil, cobra.ShellCompDirectiveNoFileComp
}

func completionCommands(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
commandNames := []string{}
commands := cmd.Root().Commands()
for _, command := range commands {
if strings.Contains(command.Name(), "complet") {
continue
}
commandNames = append(commandNames, command.Name())
}

return commandNames, cobra.ShellCompDirectiveNoFileComp
}

func completionContainerNames(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
containerNames := []string{}
if containers, err := getContainers(); err == nil {
for _, container := range containers {
containerNames = append(containerNames, container.Names[0])
}
}

if len(containerNames) == 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

return containerNames, cobra.ShellCompDirectiveNoFileComp
}

func completionContainerNamesFiltered(cmd *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
if cmd.Name() == "enter" && len(args) >= 1 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

containerNames := []string{}
if containers, err := getContainers(); err == nil {
for _, container := range containers {
skip := false
for _, arg := range args {
if container.Names[0] == arg {
skip = true
break
}
}

if skip {
continue
}

containerNames = append(containerNames, container.Names[0])
}
}

if len(containerNames) == 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

return containerNames, cobra.ShellCompDirectiveNoFileComp

}

func completionDistroNames(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
imageFlag := cmd.Flag("image")
if imageFlag != nil && imageFlag.Changed {
return nil, cobra.ShellCompDirectiveNoFileComp
}

distros := []string{}
supportedDistros := utils.GetSupportedDistros()
for key := range supportedDistros {
distros = append(distros, key)
}

return distros, cobra.ShellCompDirectiveNoFileComp
}

func completionImageNames(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
distroFlag := cmd.Flag("distro")
if distroFlag != nil && distroFlag.Changed {
return nil, cobra.ShellCompDirectiveNoFileComp
}

imageNames := []string{}
if images, err := getImages(); err == nil {
for _, image := range images {
if len(image.Names) > 0 {
imageNames = append(imageNames, image.Names[0])
} else {
imageNames = append(imageNames, image.ID)
}
}
}

if len(imageNames) == 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

return imageNames, cobra.ShellCompDirectiveNoFileComp
}

func completionImageNamesFiltered(_ *cobra.Command, args []string, _ string) ([]string, cobra.ShellCompDirective) {
imageNames := []string{}
if images, err := getImages(); err == nil {
for _, image := range images {
skip := false
var imageName string

if len(image.Names) > 0 {
imageName = image.Names[0]
} else {
imageName = image.ID
}

for _, arg := range args {
if arg == imageName {
skip = true
break
}
}

if skip {
continue
}

imageNames = append(imageNames, imageName)
}
}

if len(imageNames) == 0 {
return nil, cobra.ShellCompDirectiveNoFileComp
}

return imageNames, cobra.ShellCompDirectiveNoFileComp
}

func completionLogLevels(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
return []string{"trace", "debug", "info", "warn", "error", "fatal", "panic"}, cobra.ShellCompDirectiveNoFileComp
}
11 changes: 8 additions & 3 deletions src/cmd/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,10 @@ var (
)

var createCmd = &cobra.Command{
Use: "create",
Short: "Create a new toolbox container",
RunE: create,
Use: "create",
Short: "Create a new toolbox container",
RunE: create,
ValidArgsFunction: completionEmpty,
}

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

createCmd.SetHelpFunc(createHelp)

createCmd.RegisterFlagCompletionFunc("distro", completionDistroNames)
createCmd.RegisterFlagCompletionFunc("image", completionImageNames)

rootCmd.AddCommand(createCmd)
}

Expand Down
10 changes: 7 additions & 3 deletions src/cmd/enter.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ var (
)

var enterCmd = &cobra.Command{
Use: "enter",
Short: "Enter a toolbox container for interactive use",
RunE: enter,
Use: "enter",
Short: "Enter a toolbox container for interactive use",
RunE: enter,
ValidArgsFunction: completionContainerNamesFiltered,
}

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

enterCmd.RegisterFlagCompletionFunc("container", completionContainerNames)
enterCmd.RegisterFlagCompletionFunc("distro", completionDistroNames)

enterCmd.SetHelpFunc(enterHelp)
rootCmd.AddCommand(enterCmd)
}
Expand Down
7 changes: 4 additions & 3 deletions src/cmd/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import (
)

var helpCmd = &cobra.Command{
Use: "help",
Short: "Display help information about Toolbox",
RunE: help,
Use: "help",
Short: "Display help information about Toolbox",
RunE: help,
ValidArgsFunction: completionCommands,
}

func init() {
Expand Down
7 changes: 4 additions & 3 deletions src/cmd/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,10 @@ var (
)

var listCmd = &cobra.Command{
Use: "list",
Short: "List existing toolbox containers and images",
RunE: list,
Use: "list",
Short: "List existing toolbox containers and images",
RunE: list,
ValidArgsFunction: completionEmpty,
}

func init() {
Expand Down
7 changes: 4 additions & 3 deletions src/cmd/rm.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ var (
)

var rmCmd = &cobra.Command{
Use: "rm",
Short: "Remove one or more toolbox containers",
RunE: rm,
Use: "rm",
Short: "Remove one or more toolbox containers",
RunE: rm,
ValidArgsFunction: completionContainerNamesFiltered,
}

func init() {
Expand Down
7 changes: 4 additions & 3 deletions src/cmd/rmi.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ var (
)

var rmiCmd = &cobra.Command{
Use: "rmi",
Short: "Remove one or more toolbox images",
RunE: rmi,
Use: "rmi",
Short: "Remove one or more toolbox images",
RunE: rmi,
ValidArgsFunction: completionImageNamesFiltered,
}

func init() {
Expand Down
2 changes: 2 additions & 0 deletions src/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ func init() {

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

rootCmd.RegisterFlagCompletionFunc("log-level", completionLogLevels)

rootCmd.SetHelpFunc(rootHelp)

usageTemplate := fmt.Sprintf(`Run '%s --help' for usage.`, executableBase)
Expand Down
11 changes: 8 additions & 3 deletions src/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ var (
)

var runCmd = &cobra.Command{
Use: "run",
Short: "Run a command in an existing toolbox container",
RunE: run,
Use: "run",
Short: "Run a command in an existing toolbox container",
RunE: run,
ValidArgsFunction: completionEmpty,
}

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

runCmd.SetHelpFunc(runHelp)

runCmd.RegisterFlagCompletionFunc("container", completionContainerNames)
runCmd.RegisterFlagCompletionFunc("distro", completionDistroNames)

rootCmd.AddCommand(runCmd)
}

Expand Down
1 change: 1 addition & 0 deletions src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ go_build_wrapper_program = find_program('go-build-wrapper')

sources = files(
'toolbox.go',
'cmd/completion.go',
'cmd/create.go',
'cmd/enter.go',
'cmd/help.go',
Expand Down
9 changes: 9 additions & 0 deletions src/pkg/utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,15 @@ func GetRuntimeDirectory(targetUser *user.User) (string, error) {
return toolboxRuntimeDirectory, nil
}

// GetSupportedDistros returns the names of all supported distributions
//
// The form of the names is as found in VARIANT_ID in os-release[0]
//
// [0] https://www.freedesktop.org/software/systemd/man/os-release.html
func GetSupportedDistros() map[string]Distro {
return supportedDistros
}

// HumanDuration accepts a Unix time value and converts it into a human readable
// string.
//
Expand Down

0 comments on commit b3e4bde

Please sign in to comment.