Skip to content

Commit

Permalink
feat(comp): Allow fuzzy matching during completion
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
marckhouzam committed Jan 3, 2022
1 parent 66fb403 commit b8d3535
Show file tree
Hide file tree
Showing 19 changed files with 79 additions and 57 deletions.
9 changes: 1 addition & 8 deletions cmd/helm/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,14 +69,7 @@ func newDocsCmd(out io.Writer) *cobra.Command {
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) {
types := []string{"bash", "man", "markdown"}
var comps []string
for _, t := range types {
if strings.HasPrefix(t, toComplete) {
comps = append(comps, t)
}
}
return comps, cobra.ShellCompDirectiveNoFileComp
return []string{"bash", "man", "markdown"}, cobra.ShellCompDirectiveNoFileComp
})

return cmd
Expand Down
4 changes: 2 additions & 2 deletions cmd/helm/docs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ func TestDocsTypeFlagCompletion(t *testing.T) {
cmd: "__complete docs --type ''",
golden: "output/docs-type-comp.txt",
}, {
name: "completion for docs --type",
name: "completion for docs --type, no filter",
cmd: "__complete docs --type mar",
golden: "output/docs-type-filtered-comp.txt",
golden: "output/docs-type-comp.txt",
}}
runTestCmd(t, tests)
}
Expand Down
35 changes: 15 additions & 20 deletions cmd/helm/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
var formatNames []string
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
Expand Down Expand Up @@ -153,24 +151,21 @@ func compVersionFlag(chartRef string, toComplete string) ([]string, cobra.ShellC
var versions []string
if indexFile, err := repo.LoadIndexFile(path); err == nil {
for _, details := range indexFile.Entries[chartName] {
version := details.Metadata.Version
if strings.HasPrefix(version, toComplete) {
appVersion := details.Metadata.AppVersion
appVersionDesc := ""
if appVersion != "" {
appVersionDesc = fmt.Sprintf("App: %s, ", appVersion)
}
created := details.Created.Format("January 2, 2006")
createdDesc := ""
if created != "" {
createdDesc = fmt.Sprintf("Created: %s ", created)
}
deprecated := ""
if details.Metadata.Deprecated {
deprecated = "(deprecated)"
}
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", version, appVersionDesc, createdDesc, deprecated))
appVersion := details.Metadata.AppVersion
appVersionDesc := ""
if appVersion != "" {
appVersionDesc = fmt.Sprintf("App: %s, ", appVersion)
}
created := details.Created.Format("January 2, 2006")
createdDesc := ""
if created != "" {
createdDesc = fmt.Sprintf("Created: %s ", created)
}
deprecated := ""
if details.Metadata.Deprecated {
deprecated = "(deprecated)"
}
versions = append(versions, fmt.Sprintf("%s\t%s%s%s", details.Metadata.Version, appVersionDesc, createdDesc, deprecated))
}
}

Expand Down
7 changes: 7 additions & 0 deletions cmd/helm/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ func outputFlagCompletionTest(t *testing.T, cmdName string) {
rels: releasesMockWithStatus(&release.Info{
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)
}
10 changes: 3 additions & 7 deletions cmd/helm/history.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"fmt"
"io"
"strconv"
"strings"
"time"

"github.com/gosuri/uitable"
Expand Down Expand Up @@ -191,12 +190,9 @@ func compListRevisions(toComplete string, cfg *action.Configuration, releaseName
var revisions []string
if hist, err := client.Run(releaseName); err == nil {
for _, release := range hist {
version := strconv.Itoa(release.Version)
if strings.HasPrefix(version, toComplete) {
appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion)
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))
}
appVersion := fmt.Sprintf("App: %s", release.Chart.Metadata.AppVersion)
chartDesc := fmt.Sprintf("Chart: %s-%s", release.Chart.Metadata.Name, release.Chart.Metadata.Version)
revisions = append(revisions, fmt.Sprintf("%s\t%s, %s", strconv.Itoa(release.Version), appVersion, chartDesc))
}
return revisions, cobra.ShellCompDirectiveNoFileComp
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/helm/history_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ func revisionFlagCompletionTest(t *testing.T, cmdName string) {
cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName),
rels: releases,
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",
cmd: fmt.Sprintf("__complete %s --revision ''", cmdName),
Expand Down
4 changes: 4 additions & 0 deletions cmd/helm/install_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,10 @@ func TestInstallVersionCompletion(t *testing.T) {
name: "completion for install version flag with generate-name",
cmd: fmt.Sprintf("%s __complete install --generate-name testing/alpine --version ''", repoSetup),
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",
cmd: fmt.Sprintf("%s __complete install testing/alpine --version ''", repoSetup),
Expand Down
9 changes: 8 additions & 1 deletion cmd/helm/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,14 @@ func compListReleases(toComplete string, ignoredReleaseNames []string, cfg *acti
client := action.NewList(cfg)
client.All = true
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()
releases, err := client.Run()
Expand Down
5 changes: 1 addition & 4 deletions cmd/helm/plugin_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package main
import (
"fmt"
"io"
"strings"

"github.com/gosuri/uitable"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -82,9 +81,7 @@ func compListPlugins(toComplete string, ignoredPluginNames []string) []string {
if err == nil && len(plugins) > 0 {
filteredPlugins := filterPlugins(plugins, ignoredPluginNames)
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
Expand Down
10 changes: 10 additions & 0 deletions cmd/helm/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,11 @@ func TestPluginCmdsCompletion(t *testing.T) {
cmd: "__complete plugin update ''",
golden: "output/plugin_list_comp.txt",
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",
cmd: "__complete plugin update args ''",
Expand All @@ -317,6 +322,11 @@ func TestPluginCmdsCompletion(t *testing.T) {
cmd: "__complete plugin uninstall ''",
golden: "output/plugin_list_comp.txt",
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",
cmd: "__complete plugin uninstall args ''",
Expand Down
4 changes: 4 additions & 0 deletions cmd/helm/pull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@ func TestPullVersionCompletion(t *testing.T) {
name: "completion for pull version flag",
cmd: fmt.Sprintf("%s __complete pull testing/alpine --version ''", repoSetup),
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",
cmd: fmt.Sprintf("%s __complete pull --version ''", repoSetup),
Expand Down
5 changes: 1 addition & 4 deletions cmd/helm/repo_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package main
import (
"fmt"
"io"
"strings"

"github.com/gosuri/uitable"
"github.com/pkg/errors"
Expand Down Expand Up @@ -131,9 +130,7 @@ func compListRepos(prefix string, ignoredRepoNames []string) []string {
if err == nil && len(f.Repositories) > 0 {
filteredRepos := filterRepos(f.Repositories, ignoredRepoNames)
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
Expand Down
4 changes: 4 additions & 0 deletions cmd/helm/repo_remove_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,10 @@ func TestRepoRemoveCompletion(t *testing.T) {
name: "completion for repo remove",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove ''", repoSetup),
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",
cmd: fmt.Sprintf("%s __completeNoDesc repo remove foo ''", repoSetup),
Expand Down
8 changes: 2 additions & 6 deletions cmd/helm/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
nsNames := []string{}
if namespaces, err := client.CoreV1().Namespaces().List(context.Background(), metav1.ListOptions{TimeoutSeconds: &to}); err == nil {
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
}
Expand All @@ -133,9 +131,7 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
&clientcmd.ConfigOverrides{}).RawConfig(); err == nil {
comps := []string{}
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
}
Expand Down
5 changes: 3 additions & 2 deletions cmd/helm/search_repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,9 @@ func compListCharts(toComplete string, includeFiles bool) ([]string, cobra.Shell
}
repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo
completions = append(completions, compListChartsOfRepo(repo, toComplete)...)
// Must complete with charts within the specified repo.
// Don't filter on toComplete to allow for shell fuzzy matching
completions = append(completions, compListChartsOfRepo(repo, "")...)
noSpace = false
break
} else if strings.HasPrefix(repo, toComplete) {
Expand Down
4 changes: 4 additions & 0 deletions cmd/helm/show_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ func TestShowVersionCompletion(t *testing.T) {
name: "completion for show version flag",
cmd: fmt.Sprintf("%s __complete show chart testing/alpine --version ''", repoSetup),
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",
cmd: fmt.Sprintf("%s __complete show chart --version ''", repoSetup),
Expand Down
3 changes: 0 additions & 3 deletions cmd/helm/testdata/output/docs-type-filtered-comp.txt

This file was deleted.

1 change: 1 addition & 0 deletions cmd/helm/testdata/output/status-comp.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
aramis Aramis-chart-0.0.0 -> uninstalled
athos Athos-chart-1.2.3 -> deployed
porthos Porthos-chart-111.222.333 -> failed
:4
Completion ended with directive: ShellCompDirectiveNoFileComp
4 changes: 4 additions & 0 deletions cmd/helm/upgrade_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,10 @@ func TestUpgradeVersionCompletion(t *testing.T) {
name: "completion for upgrade version flag",
cmd: fmt.Sprintf("%s __complete upgrade releasename testing/alpine --version ''", repoSetup),
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",
cmd: fmt.Sprintf("%s __complete upgrade releasename --version ''", repoSetup),
Expand Down

0 comments on commit b8d3535

Please sign in to comment.