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.

https://github.com/containers/toolbox/pull/840

Co-authored-by: Ondřej Míchal <harrymichal@seznam.cz>
This commit is contained in:
Oliver Gutierrez 2021-07-16 14:00:23 +01:00 committed by Ondřej Míchal
parent 5c8ad7a7ec
commit d69ce6794b
10 changed files with 266 additions and 21 deletions

209
src/cmd/completion.go Normal file
View file

@ -0,0 +1,209 @@
package cmd
import (
"fmt"
"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":
if err := cmd.Root().GenBashCompletion(os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v", err)
}
case "zsh":
if err := cmd.Root().GenZshCompletion(os.Stdout); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v", err)
}
case "fish":
if err := cmd.Root().GenFishCompletion(os.Stdout, true); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v", err)
}
}
},
}
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
}
supportedDistros := utils.GetSupportedDistros()
return supportedDistros, 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
}

View file

@ -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() {
@ -91,6 +92,14 @@ func init() {
"Create a toolbox container for a different operating system release than the host")
createCmd.SetHelpFunc(createHelp)
if err := createCmd.RegisterFlagCompletionFunc("distro", completionDistroNames); err != nil {
logrus.Panicf("failed to register flag completion function: %v", err)
}
if err := createCmd.RegisterFlagCompletionFunc("image", completionImageNames); err != nil {
logrus.Panicf("failed to register flag completion function: %v", err)
}
rootCmd.AddCommand(createCmd)
}

View file

@ -23,6 +23,7 @@ import (
"strings"
"github.com/containers/toolbox/pkg/utils"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@ -35,9 +36,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() {
@ -61,6 +63,13 @@ func init() {
"",
"Enter a toolbox container for a different operating system release than the host")
if err := enterCmd.RegisterFlagCompletionFunc("container", completionContainerNames); err != nil {
logrus.Panicf("failed to register flag completion function: %v", err)
}
if err := enterCmd.RegisterFlagCompletionFunc("distro", completionDistroNames); err != nil {
logrus.Panicf("failed to register flag completion function: %v", err)
}
enterCmd.SetHelpFunc(enterHelp)
rootCmd.AddCommand(enterCmd)
}

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -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() {

View file

@ -95,6 +95,10 @@ func init() {
persistentFlags.CountVarP(&rootFlags.verbose, "verbose", "v", "Set log-level to 'debug'")
if err := rootCmd.RegisterFlagCompletionFunc("log-level", completionLogLevels); err != nil {
logrus.Panicf("failed to register flag completion function: %v", err)
}
rootCmd.SetHelpFunc(rootHelp)
usageTemplate := fmt.Sprintf("Run '%s --help' for usage.", executableBase)

View file

@ -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() {
@ -70,6 +71,14 @@ func init() {
"Run command inside a toolbox container for a different operating system release than the host")
runCmd.SetHelpFunc(runHelp)
if err := runCmd.RegisterFlagCompletionFunc("container", completionContainerNames); err != nil {
logrus.Panicf("failed to register flag completion function: %v", err)
}
if err := runCmd.RegisterFlagCompletionFunc("distro", completionDistroNames); err != nil {
logrus.Panicf("failed to register flag completion function: %v", err)
}
rootCmd.AddCommand(runCmd)
}

View file

@ -5,6 +5,7 @@ meson_go_fmt_program = find_program('meson_go_fmt.py')
sources = files(
'toolbox.go',
'cmd/completion.go',
'cmd/create.go',
'cmd/enter.go',
'cmd/help.go',