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',