From d69ce6794b08972592b8528613e0dcbcbda6d5f8 Mon Sep 17 00:00:00 2001 From: Oliver Gutierrez Date: Fri, 16 Jul 2021 14:00:23 +0100 Subject: [PATCH] cmd: Add shell completion command & generate completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/cmd/completion.go | 209 ++++++++++++++++++++++++++++++++++++++++++ src/cmd/create.go | 15 ++- src/cmd/enter.go | 15 ++- src/cmd/help.go | 7 +- src/cmd/list.go | 7 +- src/cmd/rm.go | 7 +- src/cmd/rmi.go | 7 +- src/cmd/root.go | 4 + src/cmd/run.go | 15 ++- src/meson.build | 1 + 10 files changed, 266 insertions(+), 21 deletions(-) create mode 100644 src/cmd/completion.go diff --git a/src/cmd/completion.go b/src/cmd/completion.go new file mode 100644 index 0000000..66c4969 --- /dev/null +++ b/src/cmd/completion.go @@ -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 +} diff --git a/src/cmd/create.go b/src/cmd/create.go index 3c1b06c..d819e37 100644 --- a/src/cmd/create.go +++ b/src/cmd/create.go @@ -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) } diff --git a/src/cmd/enter.go b/src/cmd/enter.go index 779f000..b7b73cb 100644 --- a/src/cmd/enter.go +++ b/src/cmd/enter.go @@ -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) } diff --git a/src/cmd/help.go b/src/cmd/help.go index eba53a9..48155c2 100644 --- a/src/cmd/help.go +++ b/src/cmd/help.go @@ -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() { diff --git a/src/cmd/list.go b/src/cmd/list.go index 948bfa6..0f7127b 100644 --- a/src/cmd/list.go +++ b/src/cmd/list.go @@ -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() { diff --git a/src/cmd/rm.go b/src/cmd/rm.go index 6de5eb2..7a6df59 100644 --- a/src/cmd/rm.go +++ b/src/cmd/rm.go @@ -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() { diff --git a/src/cmd/rmi.go b/src/cmd/rmi.go index d47a75e..48cecfa 100644 --- a/src/cmd/rmi.go +++ b/src/cmd/rmi.go @@ -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() { diff --git a/src/cmd/root.go b/src/cmd/root.go index ad0753b..4d8ebc9 100644 --- a/src/cmd/root.go +++ b/src/cmd/root.go @@ -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) diff --git a/src/cmd/run.go b/src/cmd/run.go index bc07013..c7ebf79 100644 --- a/src/cmd/run.go +++ b/src/cmd/run.go @@ -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) } diff --git a/src/meson.build b/src/meson.build index 68698df..30b771a 100644 --- a/src/meson.build +++ b/src/meson.build @@ -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',