feat(comp): Allow fuzzy matching during completion

We had made the assumption that when doing shell completion, we only
needed choices that had for *prefix* what the user had typed.
However, the zsh and fish shells have a more advanced matching system
which first matches on prefix, but if no match is found, then does
more advanced matching attempts, such as sub-strings; fish even matches
on descriptions of completions.

For example,
  helm status nginx<tab>
would match releases such as
  ingress-nginx
  ingress-nginx-release
as long as no release had a prefix of "nginx".

Such fuzzy matching can make completion even more useful for users in
cases where identical prefixes are common.

Signed-off-by: Marc Khouzam <marc.khouzam@montreal.ca>
pull/10519/head
Marc Khouzam 3 years ago committed by Marc Khouzam
parent 66fb403804
commit b8d3535991

@ -69,14 +69,7 @@ func newDocsCmd(out io.Writer) *cobra.Command {
f.BoolVar(&o.generateHeaders, "generate-headers", false, "generate standard headers for markdown files") f.BoolVar(&o.generateHeaders, "generate-headers", false, "generate standard headers for markdown files")
cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
types := []string{"bash", "man", "markdown"} return []string{"bash", "man", "markdown"}, cobra.ShellCompDirectiveNoFileComp
var comps []string
for _, t := range types {
if strings.HasPrefix(t, toComplete) {
comps = append(comps, t)
}
}
return comps, cobra.ShellCompDirectiveNoFileComp
}) })
return cmd return cmd

@ -26,9 +26,9 @@ func TestDocsTypeFlagCompletion(t *testing.T) {
cmd: "__complete docs --type ''", cmd: "__complete docs --type ''",
golden: "output/docs-type-comp.txt", golden: "output/docs-type-comp.txt",
}, { }, {
name: "completion for docs --type", name: "completion for docs --type, no filter",
cmd: "__complete docs --type mar", cmd: "__complete docs --type mar",
golden: "output/docs-type-filtered-comp.txt", golden: "output/docs-type-comp.txt",
}} }}
runTestCmd(t, tests) runTestCmd(t, tests)
} }

@ -69,9 +69,7 @@ func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) {
err := cmd.RegisterFlagCompletionFunc(outputFlag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { err := cmd.RegisterFlagCompletionFunc(outputFlag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
var formatNames []string var formatNames []string
for format, desc := range output.FormatsWithDesc() { for format, desc := range output.FormatsWithDesc() {
if strings.HasPrefix(format, toComplete) { formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc))
formatNames = append(formatNames, fmt.Sprintf("%s\t%s", format, desc))
}
} }
// Sort the results to get a deterministic order for the tests // Sort the results to get a deterministic order for the tests
@ -153,24 +151,21 @@ func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellC
var versions []string var versions []string
if indexFile, err := repo.LoadIndexFile(path); err == nil { if indexFile, err := repo.LoadIndexFile(path); err == nil {
for _, details := range indexFile.Entries[chartName] { for _, details := range indexFile.Entries[chartName] {
version := details.Metadata.Version appVersion := details.Metadata.AppVersion
if strings.HasPrefix(version, toComplete) { appVersionDesc := ""
appVersion := details.Metadata.AppVersion if appVersion != "" {
appVersionDesc := "" appVersionDesc = fmt.Sprintf("App: %s, ", appVersion)
if appVersion != "" { }
appVersionDesc = fmt.Sprintf("App: %s, ", appVersion) created := details.Created.Format("January 2, 2006")
} createdDesc := ""
created := details.Created.Format("January 2, 2006") if created != "" {
createdDesc := "" createdDesc = fmt.Sprintf("Created: %s ", created)
if created != "" { }
createdDesc = fmt.Sprintf("Created: %s ", created) deprecated := ""
} if details.Metadata.Deprecated {
deprecated := "" deprecated = "(deprecated)"
if details.Metadata.Deprecated {
deprecated = "(deprecated)"
}
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", version, appVersionDesc, createdDesc, deprecated))
} }
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", details.Metadata.Version, appVersionDesc, createdDesc, deprecated))
} }
} }

@ -83,6 +83,13 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) {
rels: releasesMockWithStatus(&release.Info{ rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed, Status: release.StatusDeployed,
}), }),
}, {
name: "completion for output flag, no filter",
cmd: fmt.Sprintf("__complete %s --output jso", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}} }}
runTestCmd(t, tests) runTestCmd(t, tests)
} }

@ -20,7 +20,6 @@ import (
"fmt" "fmt"
"io" "io"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
@ -191,12 +190,9 @@ func compListRevisions(toComplete string, cfg *action.Configuration, releaseName
var revisions []string var revisions []string
if hist, err := client.Run(releaseName); err == nil { if hist, err := client.Run(releaseName); err == nil {
for _, release := range hist { for _, release := range hist {
version := strconv.Itoa(release.Version) appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion)
if strings.HasPrefix(version, toComplete) { chartDesc := fmt.Sprintf("Chart: %s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version)
appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion) revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", strconv.Itoa(release.Version), appVersion, chartDesc))
chartDesc := fmt.Sprintf("Chart: %s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version)
revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", version, appVersion, chartDesc))
}
} }
return revisions, cobra.ShellCompDirectiveNoFileComp return revisions, cobra.ShellCompDirectiveNoFileComp
} }

@ -95,6 +95,11 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) {
cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName), cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName),
rels: releases, rels: releases,
golden: "output/revision-comp.txt", golden: "output/revision-comp.txt",
}, {
name: "completion for revision flag, no filter",
cmd: fmt.Sprintf("__complete %s musketeers --revision 1", cmdName),
rels: releases,
golden: "output/revision-comp.txt",
}, { }, {
name: "completion for revision flag with too few args", name: "completion for revision flag with too few args",
cmd: fmt.Sprintf("__complete %s --revision ''", cmdName), cmd: fmt.Sprintf("__complete %s --revision ''", cmdName),

@ -275,6 +275,10 @@ func TestInstallVersionCompletion(t *testing.T) {
name: "completion for install version flag with generate-name", name: "completion for install version flag with generate-name",
cmd: fmt.Sprintf("%s __complete install --generate-name testing/alpine --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete install --generate-name testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt", golden: "output/version-comp.txt",
}, {
name: "completion for install version flag, no filter",
cmd: fmt.Sprintf("%s __complete install releasename testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, { }, {
name: "completion for install version flag too few args", name: "completion for install version flag too few args",
cmd: fmt.Sprintf("%s __complete install testing/alpine --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete install testing/alpine --version ''", repoSetup),

@ -224,7 +224,14 @@ func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *acti
client := action.NewList(cfg) client := action.NewList(cfg)
client.All = true client.All = true
client.Limit = 0 client.Limit = 0
client.Filter = fmt.Sprintf("^%s", toComplete) // Do not filter so as to get the entire list of releases.
// This will allow zsh and fish to match completion choices
// on other criteria then prefix. For example:
// helm status ingress<TAB>
// can match
// helm status nginx-ingress
//
// client.Filter = fmt.Sprintf("^%s", toComplete)
client.SetStateMask() client.SetStateMask()
releases, err := client.Run() releases, err := client.Run()

@ -18,7 +18,6 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -82,9 +81,7 @@ func compListPlugins(toComplete string, ignoredPluginNames []string) []string {
if err == nil && len(plugins) > 0 { if err == nil && len(plugins) > 0 {
filteredPlugins := filterPlugins(plugins, ignoredPluginNames) filteredPlugins := filterPlugins(plugins, ignoredPluginNames)
for _, p := range filteredPlugins { for _, p := range filteredPlugins {
if strings.HasPrefix(p.Metadata.Name, toComplete) { pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage))
pNames = append(pNames, fmt.Sprintf("%s\t%s", p.Metadata.Name, p.Metadata.Usage))
}
} }
} }
return pNames return pNames

@ -307,6 +307,11 @@ func TestPluginCmdsCompletion(t *testing.T) {
cmd: "__complete plugin update ''", cmd: "__complete plugin update ''",
golden: "output/plugin_list_comp.txt", golden: "output/plugin_list_comp.txt",
rels: []*release.Release{}, rels: []*release.Release{},
}, {
name: "completion for plugin update, no filter",
cmd: "__complete plugin update full",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, { }, {
name: "completion for plugin update repetition", name: "completion for plugin update repetition",
cmd: "__complete plugin update args ''", cmd: "__complete plugin update args ''",
@ -317,6 +322,11 @@ func TestPluginCmdsCompletion(t *testing.T) {
cmd: "__complete plugin uninstall ''", cmd: "__complete plugin uninstall ''",
golden: "output/plugin_list_comp.txt", golden: "output/plugin_list_comp.txt",
rels: []*release.Release{}, rels: []*release.Release{},
}, {
name: "completion for plugin uninstall, no filter",
cmd: "__complete plugin uninstall full",
golden: "output/plugin_list_comp.txt",
rels: []*release.Release{},
}, { }, {
name: "completion for plugin uninstall repetition", name: "completion for plugin uninstall repetition",
cmd: "__complete plugin uninstall args ''", cmd: "__complete plugin uninstall args ''",

@ -371,6 +371,10 @@ func TestPullVersionCompletion(t *testing.T) {
name: "completion for pull version flag", name: "completion for pull version flag",
cmd: fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt", golden: "output/version-comp.txt",
}, {
name: "completion for pull version flag, no filter",
cmd: fmt.Sprintf("%s __complete pull testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, { }, {
name: "completion for pull version flag too few args", name: "completion for pull version flag too few args",
cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup),

@ -19,7 +19,6 @@ package main
import ( import (
"fmt" "fmt"
"io" "io"
"strings"
"github.com/gosuri/uitable" "github.com/gosuri/uitable"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -131,9 +130,7 @@ func compListRepos(prefix string, ignoredRepoNames []string) []string {
if err == nil && len(f.Repositories) > 0 { if err == nil && len(f.Repositories) > 0 {
filteredRepos := filterRepos(f.Repositories, ignoredRepoNames) filteredRepos := filterRepos(f.Repositories, ignoredRepoNames)
for _, repo := range filteredRepos { for _, repo := range filteredRepos {
if strings.HasPrefix(repo.Name, prefix) { rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
rNames = append(rNames, fmt.Sprintf("%s\t%s", repo.Name, repo.URL))
}
} }
} }
return rNames return rNames

@ -197,6 +197,10 @@ func TestRepoRemoveCompletion(t *testing.T) {
name: "completion for repo remove", name: "completion for repo remove",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove ''", repoSetup), cmd: fmt.Sprintf("%s __completeNoDesc repo remove ''", repoSetup),
golden: "output/repo_list_comp.txt", golden: "output/repo_list_comp.txt",
}, {
name: "completion for repo remove, no filter",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove fo", repoSetup),
golden: "output/repo_list_comp.txt",
}, { }, {
name: "completion for repo remove repetition", name: "completion for repo remove repetition",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove foo ''", repoSetup), cmd: fmt.Sprintf("%s __completeNoDesc repo remove foo ''", repoSetup),

@ -106,9 +106,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
nsNames := []string{} nsNames := []string{}
if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil { if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil {
for _, ns := range namespaces.Items { for _, ns := range namespaces.Items {
if strings.HasPrefix(ns.Name, toComplete) { nsNames = append(nsNames, ns.Name)
nsNames = append(nsNames, ns.Name)
}
} }
return nsNames, cobra.ShellCompDirectiveNoFileComp return nsNames, cobra.ShellCompDirectiveNoFileComp
} }
@ -133,9 +131,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
&clientcmd.ConfigOverrides{}).RawConfig(); err == nil { &clientcmd.ConfigOverrides{}).RawConfig(); err == nil {
comps := []string{} comps := []string{}
for name, context := range config.Contexts { for name, context := range config.Contexts {
if strings.HasPrefix(name, toComplete) { comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster))
comps = append(comps, fmt.Sprintf("%s\t%s", name, context.Cluster))
}
} }
return comps, cobra.ShellCompDirectiveNoFileComp return comps, cobra.ShellCompDirectiveNoFileComp
} }

@ -312,8 +312,9 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
} }
repoWithSlash := fmt.Sprintf("%s/", repo) repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) { if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo // Must complete with charts within the specified repo.
completions = append(completions, compListChartsOfRepo(repo, toComplete)...) // Don't filter on toComplete to allow for shell fuzzy matching
completions = append(completions, compListChartsOfRepo(repo, "")...)
noSpace = false noSpace = false
break break
} else if strings.HasPrefix(repo, toComplete) { } else if strings.HasPrefix(repo, toComplete) {

@ -98,6 +98,10 @@ func TestShowVersionCompletion(t *testing.T) {
name: "completion for show version flag", name: "completion for show version flag",
cmd: fmt.Sprintf("%s __complete show chart testing/alpine --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete show chart testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt", golden: "output/version-comp.txt",
}, {
name: "completion for show version flag, no filter",
cmd: fmt.Sprintf("%s __complete show chart testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, { }, {
name: "completion for show version flag too few args", name: "completion for show version flag too few args",
cmd: fmt.Sprintf("%s __complete show chart --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete show chart --version ''", repoSetup),

@ -1,3 +0,0 @@
markdown
:4
Completion ended with directive: ShellCompDirectiveNoFileComp

@ -1,4 +1,5 @@
aramis Aramis-chart-0.0.0 -> uninstalled aramis Aramis-chart-0.0.0 -> uninstalled
athos Athos-chart-1.2.3 -> deployed athos Athos-chart-1.2.3 -> deployed
porthos Porthos-chart-111.222.333 -> failed
:4 :4
Completion ended with directive: ShellCompDirectiveNoFileComp Completion ended with directive: ShellCompDirectiveNoFileComp

@ -406,6 +406,10 @@ func TestUpgradeVersionCompletion(t *testing.T) {
name: "completion for upgrade version flag", name: "completion for upgrade version flag",
cmd: fmt.Sprintf("%s __complete upgrade releasename testing/alpine --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete upgrade releasename testing/alpine --version ''", repoSetup),
golden: "output/version-comp.txt", golden: "output/version-comp.txt",
}, {
name: "completion for upgrade version flag, no filter",
cmd: fmt.Sprintf("%s __complete upgrade releasename testing/alpine --version 0.3", repoSetup),
golden: "output/version-comp.txt",
}, { }, {
name: "completion for upgrade version flag too few args", name: "completion for upgrade version flag too few args",
cmd: fmt.Sprintf("%s __complete upgrade releasename --version ''", repoSetup), cmd: fmt.Sprintf("%s __complete upgrade releasename --version ''", repoSetup),

Loading…
Cancel
Save