From cf4967de4d4e4abeb183217dfee130d8ec2e02f5 Mon Sep 17 00:00:00 2001 From: Paul Holzinger Date: Sat, 14 Nov 2020 16:50:02 +0100 Subject: Improve the shell completion api One main advantage of the new shell completion logic is that we can easly parse flags and adjust based on the given flags the suggestions. For example some commands accept the `--latest` flag only if no arguments are given. This commit implements this logic in a simple maintainable way since it reuses the already existing `Args` function in the cmd struct. I also refactored the `getXXX` function to match based on the namei/id which could speed up the shell completion with many containers, images, etc... I also added the degraded status to the valid pod status filters which was implemented in #8081. Signed-off-by: Paul Holzinger --- cmd/podman/common/completion.go | 245 ++++++++++++++++++++++++++++++---------- cmd/podman/containers/start.go | 9 +- libpod/filters/pods.go | 2 +- 3 files changed, 194 insertions(+), 62 deletions(-) diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 4d66b4e2b..00123f9e6 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -2,6 +2,7 @@ package common import ( "bufio" + "fmt" "os" "strings" @@ -22,62 +23,97 @@ var ( LogLevels = []string{"debug", "info", "warn", "error", "fatal", "panic"} ) -func getContainers(status string, toComplete string) ([]string, cobra.ShellCompDirective) { +func getContainers(toComplete string, statuses ...string) ([]string, cobra.ShellCompDirective) { suggestions := []string{} listOpts := entities.ContainerListOptions{ Filters: make(map[string][]string), } listOpts.All = true + listOpts.Pod = true - if status != "all" { - listOpts.Filters = map[string][]string{"status": {status}} - } + // TODO: The api doesn't handle several different statuses correct see: + // https://github.com/containers/podman/issues/8344 + // Instead of looping over the statuses we should be able to set + // listOpts.Filters["status"] = statuses - containers, err := registry.ContainerEngine().ContainerList(registry.GetContext(), listOpts) - if err != nil { - cobra.CompErrorln(err.Error()) - return nil, cobra.ShellCompDirectiveError + var containers []entities.ListContainer + var err error + if len(statuses) == 0 { + containers, err = registry.ContainerEngine().ContainerList(registry.GetContext(), listOpts) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveError + } + } else { + for _, s := range statuses { + listOpts.Filters["status"] = []string{s} + res, err := registry.ContainerEngine().ContainerList(registry.GetContext(), listOpts) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveError + } + containers = append(containers, res...) + } } - for _, container := range containers { + for _, c := range containers { // include ids in suggestions if more then 2 chars are typed - if len(toComplete) > 1 { - suggestions = append(suggestions, container.ID[0:12]) + if len(toComplete) > 1 && strings.HasPrefix(c.ID, toComplete) { + suggestions = append(suggestions, c.ID[0:12]+"\t"+c.PodName) } // include name in suggestions - suggestions = append(suggestions, container.Names...) + if strings.HasPrefix(c.Names[0], toComplete) { + suggestions = append(suggestions, c.Names[0]+"\t"+c.PodName) + } } return suggestions, cobra.ShellCompDirectiveNoFileComp } -func getPods(status string, toComplete string) ([]string, cobra.ShellCompDirective) { +func getPods(toComplete string, statuses ...string) ([]string, cobra.ShellCompDirective) { suggestions := []string{} listOpts := entities.PodPSOptions{ Filters: make(map[string][]string), } - if status != "all" { - listOpts.Filters = map[string][]string{"status": {status}} - } + // TODO: The api doesn't handle several different statuses correct see: + // https://github.com/containers/podman/issues/8344 + // Instead of looping over the statuses we should be able to set + // listOpts.Filters["status"] = statuses - pods, err := registry.ContainerEngine().PodPs(registry.GetContext(), listOpts) - if err != nil { - cobra.CompErrorln(err.Error()) - return nil, cobra.ShellCompDirectiveError + var pods []*entities.ListPodsReport + var err error + if len(statuses) == 0 { + pods, err = registry.ContainerEngine().PodPs(registry.GetContext(), listOpts) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveError + } + } else { + for _, s := range statuses { + listOpts.Filters["status"] = []string{s} + res, err := registry.ContainerEngine().PodPs(registry.GetContext(), listOpts) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveError + } + pods = append(pods, res...) + } } for _, pod := range pods { // include ids in suggestions if more then 2 chars are typed - if len(toComplete) > 1 { + if len(toComplete) > 1 && strings.HasPrefix(pod.Id, toComplete) { suggestions = append(suggestions, pod.Id[0:12]) } // include name in suggestions - suggestions = append(suggestions, pod.Name) + if strings.HasPrefix(pod.Name, toComplete) { + suggestions = append(suggestions, pod.Name) + } } return suggestions, cobra.ShellCompDirectiveNoFileComp } -func getVolumes() ([]string, cobra.ShellCompDirective) { +func getVolumes(toComplete string) ([]string, cobra.ShellCompDirective) { suggestions := []string{} lsOpts := entities.VolumeListOptions{} @@ -87,8 +123,10 @@ func getVolumes() ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveError } - for _, volume := range volumes { - suggestions = append(suggestions, volume.Name) + for _, v := range volumes { + if strings.HasPrefix(v.Name, toComplete) { + suggestions = append(suggestions, v.Name) + } } return suggestions, cobra.ShellCompDirectiveNoFileComp } @@ -104,16 +142,16 @@ func getImages(toComplete string) ([]string, cobra.ShellCompDirective) { } for _, image := range images { - // FIXME: need ux testing - // discuss when image ids should be completed // include ids in suggestions if more then 2 chars are typed - if len(toComplete) > 1 { + if len(toComplete) > 1 && strings.HasPrefix(image.ID, toComplete) { suggestions = append(suggestions, image.ID[0:12]) } for _, repo := range image.RepoTags { if toComplete == "" { // suggest only full repo path if no input is given - suggestions = append(suggestions, repo) + if strings.HasPrefix(repo, toComplete) { + suggestions = append(suggestions, repo) + } } else { // suggested "registry.fedoraproject.org/f29/httpd:latest" as // - "registry.fedoraproject.org/f29/httpd:latest" @@ -125,8 +163,13 @@ func getImages(toComplete string) ([]string, cobra.ShellCompDirective) { paths := strings.Split(repo, "/") for i := range paths { suggestionWithTag := strings.Join(paths[i:], "/") + if strings.HasPrefix(suggestionWithTag, toComplete) { + suggestions = append(suggestions, suggestionWithTag) + } suggestionWithoutTag := strings.SplitN(strings.SplitN(suggestionWithTag, ":", 2)[0], "@", 2)[0] - suggestions = append(suggestions, suggestionWithTag, suggestionWithoutTag) + if strings.HasPrefix(suggestionWithoutTag, toComplete) { + suggestions = append(suggestions, suggestionWithoutTag) + } } } } @@ -143,9 +186,11 @@ func getRegistries() ([]string, cobra.ShellCompDirective) { return regs, cobra.ShellCompDirectiveNoFileComp } -func getNetworks() ([]string, cobra.ShellCompDirective) { +func getNetworks(toComplete string) ([]string, cobra.ShellCompDirective) { suggestions := []string{} - networkListOptions := entities.NetworkListOptions{} + networkListOptions := entities.NetworkListOptions{ + Filter: "name=" + toComplete, + } networks, err := registry.ContainerEngine().NetworkList(registry.Context(), networkListOptions) if err != nil { @@ -159,76 +204,154 @@ func getNetworks() ([]string, cobra.ShellCompDirective) { return suggestions, cobra.ShellCompDirectiveNoFileComp } +// validCurrentCmdLine validates the current cmd line +// It utilizes the Args function from the cmd struct +// In most cases the Args function validates the args length but it +// is also used to verify that --latest is not given with an argument. +// This function helps to makes sure we only complete valid arguments. +func validCurrentCmdLine(cmd *cobra.Command, args []string, toComplete string) bool { + if cmd.Args == nil { + // Without an Args function we cannot check so assume it's correct + return true + } + // We have to append toComplete to the args otherwise the + // argument count would not match the expected behavior + if err := cmd.Args(cmd, append(args, toComplete)); err != nil { + // Special case if we use ExactArgs(2) or MinimumNArgs(2), + // They will error if we try to complete the first arg. + // Lets try to parse the common error and compare if we have less args than + // required. In this case we are fine and should provide completion. + + // Clean the err msg so we can parse it with fmt.Sscanf + // Trim MinimumNArgs prefix + cleanErr := strings.TrimPrefix(err.Error(), "requires at least ") + // Trim MinimumNArgs "only" part + cleanErr = strings.ReplaceAll(cleanErr, "only received", "received") + // Trim ExactArgs prefix + cleanErr = strings.TrimPrefix(cleanErr, "accepts ") + var need, got int + cobra.CompDebugln(cleanErr, true) + _, err = fmt.Sscanf(cleanErr, "%d arg(s), received %d", &need, &got) + if err == nil { + if need >= got { + // We still need more arguments so provide more completions + return true + } + } + cobra.CompDebugln(err.Error(), true) + return false + } + return true +} + /* Autocomplete Functions for cobra ValidArgsFunction */ // AutocompleteContainers - Autocomplete all container names. func AutocompleteContainers(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return getContainers("all", toComplete) + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getContainers(toComplete) } // AutocompleteContainersCreated - Autocomplete only created container names. func AutocompleteContainersCreated(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return getContainers("created", toComplete) + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getContainers(toComplete, "created") } // AutocompleteContainersExited - Autocomplete only exited container names. func AutocompleteContainersExited(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return getContainers("exited", toComplete) + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getContainers(toComplete, "exited") } // AutocompleteContainersPaused - Autocomplete only paused container names. func AutocompleteContainersPaused(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return getContainers("paused", toComplete) + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getContainers(toComplete, "paused") } // AutocompleteContainersRunning - Autocomplete only running container names. func AutocompleteContainersRunning(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return getContainers("running", toComplete) + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getContainers(toComplete, "running") } // AutocompleteContainersStartable - Autocomplete only created and exited container names. func AutocompleteContainersStartable(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - containersCreated, _ := getContainers("created", toComplete) - containersExited, _ := getContainers("exited", toComplete) - return append(containersCreated, containersExited...), cobra.ShellCompDirectiveNoFileComp + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getContainers(toComplete, "created", "exited") } // AutocompletePods - Autocomplete all pod names. func AutocompletePods(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return getPods("all", toComplete) + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getPods(toComplete) } // AutocompletePodsRunning - Autocomplete only running pod names. +// It considers degraded as running. func AutocompletePodsRunning(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return getPods("running", toComplete) + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getPods(toComplete, "running", "degraded") } // AutocompleteContainersAndPods - Autocomplete container names and pod names. func AutocompleteContainersAndPods(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - containers, _ := getContainers("all", toComplete) - pods, _ := getPods("all", toComplete) + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + containers, _ := getContainers(toComplete) + pods, _ := getPods(toComplete) return append(containers, pods...), cobra.ShellCompDirectiveNoFileComp } // AutocompleteContainersAndImages - Autocomplete container names and pod names. func AutocompleteContainersAndImages(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - containers, _ := getContainers("all", toComplete) + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + containers, _ := getContainers(toComplete) images, _ := getImages(toComplete) return append(containers, images...), cobra.ShellCompDirectiveNoFileComp } // AutocompleteVolumes - Autocomplete volumes. func AutocompleteVolumes(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return getVolumes() + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getVolumes(toComplete) } // AutocompleteImages - Autocomplete images. func AutocompleteImages(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } return getImages(toComplete) } // AutocompleteCreateRun - Autocomplete only the fist argument as image and then do file completion. func AutocompleteCreateRun(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } if len(args) < 1 { return getImages(toComplete) } @@ -238,18 +361,27 @@ func AutocompleteCreateRun(cmd *cobra.Command, args []string, toComplete string) // AutocompleteRegistries - Autocomplete registries. func AutocompleteRegistries(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } return getRegistries() } // AutocompleteNetworks - Autocomplete networks. func AutocompleteNetworks(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return getNetworks() + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return getNetworks(toComplete) } // AutocompleteCpCommand - Autocomplete podman cp command args. func AutocompleteCpCommand(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } if len(args) < 2 { - containers, _ := getContainers("all", toComplete) + containers, _ := getContainers(toComplete) for _, container := range containers { // TODO: Add path completion for inside the container if possible if strings.HasPrefix(container, toComplete) { @@ -265,6 +397,9 @@ func AutocompleteCpCommand(cmd *cobra.Command, args []string, toComplete string) // AutocompleteSystemConnections - Autocomplete system connections. func AutocompleteSystemConnections(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if !validCurrentCmdLine(cmd, args, toComplete) { + return nil, cobra.ShellCompDirectiveNoFileComp + } suggestions := []string{} cfg, err := config.ReadCustomConfig() if err != nil { @@ -318,7 +453,7 @@ func AutocompleteNamespace(cmd *cobra.Command, args []string, toComplete string) switch { case strings.HasPrefix(toComplete, "container:"): // Complete containers after colon - containers, _ := getContainers("all", toComplete[10:]) //trim "container:" + containers, _ := getContainers(toComplete[10:]) //trim "container:" // add "container:" in front of the suggestions var suggestions []string @@ -504,21 +639,13 @@ func AutocompleteMountFlag(cmd *cobra.Command, args []string, toComplete string) // AutocompleteVolumeFlag - Autocomplete volume flag options. // -> volumes and paths func AutocompleteVolumeFlag(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - result := []string{} - volumes, _ := getVolumes() - for _, volume := range volumes { - // If we don't filter on "toComplete", zsh and fish will not do file completion - // even if the prefix typed by the user does not match the returned completions - if strings.HasPrefix(volume, toComplete) { - result = append(result, volume) - } - } + volumes, _ := getVolumes(toComplete) directive := cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveDefault if strings.Contains(toComplete, ":") { // add space after second path directive = cobra.ShellCompDirectiveDefault } - return result, directive + return volumes, directive } // AutocompleteJSONFormat - Autocomplete format flag option. diff --git a/cmd/podman/containers/start.go b/cmd/podman/containers/start.go index dba2c3c3e..7e57bb576 100644 --- a/cmd/podman/containers/start.go +++ b/cmd/podman/containers/start.go @@ -21,6 +21,7 @@ var ( Short: "Start one or more containers", Long: startDescription, RunE: start, + Args: validateStart, ValidArgsFunction: common.AutocompleteContainersStartable, Example: `podman start --latest podman start 860a4b231279 5421ab43b45 @@ -32,6 +33,7 @@ var ( Short: startCommand.Short, Long: startCommand.Long, RunE: startCommand.RunE, + Args: startCommand.Args, ValidArgsFunction: startCommand.ValidArgsFunction, Example: `podman container start --latest podman container start 860a4b231279 5421ab43b45 @@ -76,8 +78,7 @@ func init() { validate.AddLatestFlag(containerStartCommand, &startOptions.Latest) } -func start(cmd *cobra.Command, args []string) error { - var errs utils.OutputErrors +func validateStart(cmd *cobra.Command, args []string) error { if len(args) == 0 && !startOptions.Latest { return errors.New("start requires at least one argument") } @@ -87,7 +88,11 @@ func start(cmd *cobra.Command, args []string) error { if len(args) > 1 && startOptions.Attach { return errors.Errorf("you cannot start and attach multiple containers at once") } + return nil +} +func start(cmd *cobra.Command, args []string) error { + var errs utils.OutputErrors sigProxy := startOptions.SigProxy || startOptions.Attach if cmd.Flag("sig-proxy").Changed { sigProxy = startOptions.SigProxy diff --git a/libpod/filters/pods.go b/libpod/filters/pods.go index 7d12eefa6..3cd97728f 100644 --- a/libpod/filters/pods.go +++ b/libpod/filters/pods.go @@ -88,7 +88,7 @@ func GeneratePodFilterFunc(filter, filterValue string) ( return match }, nil case "status": - if !util.StringInSlice(filterValue, []string{"stopped", "running", "paused", "exited", "dead", "created"}) { + if !util.StringInSlice(filterValue, []string{"stopped", "running", "paused", "exited", "dead", "created", "degraded"}) { return nil, errors.Errorf("%s is not a valid pod status", filterValue) } return func(p *libpod.Pod) bool { -- cgit v1.2.3-54-g00ecf