Merge pull request #7323 from VilledeMontreal/feat/dynamicCompletionInGo

Auto-completion implemented in Go
pull/7435/head
Marc Khouzam 5 years ago committed by GitHub
commit a6b2c9e212
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/cli/values"
@ -52,10 +53,19 @@ func addChartPathOptionsFlags(f *pflag.FlagSet, c *action.ChartPathOptions) {
// bindOutputFlag will add the output flag to the given command and bind the
// value to the given format pointer
func bindOutputFlag(cmd *cobra.Command, varRef *output.Format) {
cmd.Flags().VarP(newOutputValue(output.Table, varRef), outputFlag, "o",
f := cmd.Flags()
flag := f.VarPF(newOutputValue(output.Table, varRef), outputFlag, "o",
fmt.Sprintf("prints the output in the specified format. Allowed values: %s", strings.Join(output.Formats(), ", ")))
// Setup shell completion for the flag
cmd.MarkFlagCustom(outputFlag, "__helm_output_options")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
var formatNames []string
for _, format := range output.Formats() {
if strings.HasPrefix(format, toComplete) {
formatNames = append(formatNames, format)
}
}
return formatNames, completion.BashCompDirectiveDefault
})
}
type outputValue output.Format

@ -0,0 +1,88 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"fmt"
"testing"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/release"
helmtime "helm.sh/helm/v3/pkg/time"
)
func outputFlagCompletionTest(t *testing.T, cmdName string) {
releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release {
info.LastDeployed = helmtime.Unix(1452902400, 0).UTC()
return []*release.Release{{
Name: "athos",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "porthos",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "aramis",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "dartagnan",
Namespace: "gascony",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}}
}
tests := []cmdTestCase{{
name: "completion for output flag long and before arg",
cmd: fmt.Sprintf("__complete %s --output ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for output flag long and after arg",
cmd: fmt.Sprintf("__complete %s aramis --output ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for output flag short and before arg",
cmd: fmt.Sprintf("__complete %s -o ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for output flag short and after arg",
cmd: fmt.Sprintf("__complete %s aramis -o ''", cmdName),
golden: "output/output-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}}
runTestCmd(t, tests)
}

@ -22,6 +22,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
)
@ -56,8 +57,24 @@ func newGetAllCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
f.StringVar(&template, "template", "", "go template for formatting the output, eg: {{.Release.Name}}")
return cmd

@ -41,3 +41,7 @@ func TestGetCmd(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestGetAllRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get all")
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -52,7 +53,23 @@ func newGetHooksCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision")
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -36,3 +36,7 @@ func TestGetHooks(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestGetHooksRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get hooks")
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -52,7 +53,23 @@ func newGetManifestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
},
}
cmd.Flags().IntVar(&client.Version, "revision", 0, "get the named release with revision")
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -36,3 +36,7 @@ func TestGetManifest(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestGetManifestRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get manifest")
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -50,8 +51,23 @@ func newGetNotesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -36,3 +36,7 @@ func TestGetNotesCmd(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestGetNotesRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get notes")
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
)
@ -54,8 +55,23 @@ func newGetValuesCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Version, "revision", 0, "get the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
f.BoolVarP(&client.AllValues, "all", "a", false, "dump all (computed) values")
bindOutputFlag(cmd, &outfmt)

@ -52,3 +52,11 @@ func TestGetValuesCmd(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestGetValuesRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "get values")
}
func TestGetValuesOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "get values")
}

@ -18,6 +18,7 @@ package main
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
@ -96,11 +97,39 @@ func storageFixture() *storage.Storage {
return storage.Init(driver.NewMemory())
}
// go-shellwords does not handle empty arguments properly
// https://github.com/mattn/go-shellwords/issues/5#issuecomment-573431458
//
// This method checks if the last argument was an empty one,
// and if go-shellwords missed it, we add it ourselves.
//
// This is important for completion tests as completion often
// uses an empty last parameter.
func checkLastEmpty(in string, out []string) []string {
lastIndex := len(in) - 1
if lastIndex >= 1 && (in[lastIndex] == '"' && in[lastIndex-1] == '"' ||
in[lastIndex] == '\'' && in[lastIndex-1] == '\'') {
// The last parameter of 'in' was empty ("" or ''), let's make sure it was detected.
if len(out) > 0 && out[len(out)-1] != "" {
// Bug from go-shellwords:
// 'out' does not have the empty parameter. We add it ourselves as a workaround.
out = append(out, "")
} else {
fmt.Println("WARNING: go-shellwords seems to have been fixed. This workaround can be removed.")
}
}
return out
}
func executeActionCommandC(store *storage.Storage, cmd string) (*cobra.Command, string, error) {
args, err := shellwords.Parse(cmd)
if err != nil {
return nil, "", err
}
// Workaround the bug in shellwords
args = checkLastEmpty(cmd, args)
buf := new(bytes.Buffer)
actionConfig := &action.Configuration{

@ -19,12 +19,14 @@ package main
import (
"fmt"
"io"
"strconv"
"time"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/cli/output"
@ -69,6 +71,14 @@ func newHistoryCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.IntVar(&client.Max, "max", 256, "maximum number of revision to include in history")
bindOutputFlag(cmd, &outfmt)
@ -176,3 +186,16 @@ func min(x, y int) int {
}
return y
}
func compListRevisions(cfg *action.Configuration, releaseName string) ([]string, completion.BashCompDirective) {
client := action.NewHistory(cfg)
var revisions []string
if hist, err := client.Run(releaseName); err == nil {
for _, release := range hist {
revisions = append(revisions, strconv.Itoa(release.Version))
}
return revisions, completion.BashCompDirectiveDefault
}
return nil, completion.BashCompDirectiveError
}

@ -17,6 +17,7 @@ limitations under the License.
package main
import (
"fmt"
"testing"
"helm.sh/helm/v3/pkg/release"
@ -68,3 +69,42 @@ func TestHistoryCmd(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestHistoryOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "history")
}
func revisionFlagCompletionTest(t *testing.T, cmdName string) {
mk := func(name string, vers int, status release.Status) *release.Release {
return release.Mock(&release.MockReleaseOptions{
Name: name,
Version: vers,
Status: status,
})
}
releases := []*release.Release{
mk("musketeers", 11, release.StatusDeployed),
mk("musketeers", 10, release.StatusSuperseded),
mk("musketeers", 9, release.StatusSuperseded),
mk("musketeers", 8, release.StatusSuperseded),
}
tests := []cmdTestCase{{
name: "completion for revision flag",
cmd: fmt.Sprintf("__complete %s musketeers --revision ''", cmdName),
rels: releases,
golden: "output/revision-comp.txt",
}, {
name: "completion for revision flag with too few args",
cmd: fmt.Sprintf("__complete %s --revision ''", cmdName),
rels: releases,
golden: "output/revision-wrong-args-comp.txt",
}, {
name: "completion for revision flag with too many args",
cmd: fmt.Sprintf("__complete %s three musketeers --revision ''", cmdName),
rels: releases,
golden: "output/revision-wrong-args-comp.txt",
}}
runTestCmd(t, tests)
}

@ -26,6 +26,7 @@ import (
"github.com/spf13/pflag"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart"
"helm.sh/helm/v3/pkg/chart/loader"
@ -122,6 +123,11 @@ func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
return compInstall(args, toComplete, client)
})
addInstallFlags(cmd.Flags(), client, valueOpts)
bindOutputFlag(cmd, &outfmt)
@ -225,3 +231,15 @@ func isChartInstallable(ch *chart.Chart) (bool, error) {
}
return false, errors.Errorf("%s charts are not installable", ch.Metadata.Type)
}
// Provide dynamic auto-completion for the install and template commands
func compInstall(args []string, toComplete string, client *action.Install) ([]string, completion.BashCompDirective) {
requiredArgs := 1
if client.GenerateName {
requiredArgs = 0
}
if len(args) == requiredArgs {
return compListCharts(toComplete, true)
}
return nil, completion.BashCompDirectiveNoFileComp
}

@ -193,3 +193,7 @@ func TestInstall(t *testing.T) {
runTestActionCmd(t, tests)
}
func TestInstallOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "install")
}

@ -26,6 +26,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/release"
@ -164,3 +165,26 @@ func (r *releaseListWriter) WriteJSON(out io.Writer) error {
func (r *releaseListWriter) WriteYAML(out io.Writer) error {
return output.EncodeYAML(out, r.releases)
}
// Provide dynamic auto-completion for release names
func compListReleases(toComplete string, cfg *action.Configuration) ([]string, completion.BashCompDirective) {
completion.CompDebugln(fmt.Sprintf("compListReleases with toComplete %s", toComplete))
client := action.NewList(cfg)
client.All = true
client.Limit = 0
client.Filter = fmt.Sprintf("^%s", toComplete)
client.SetStateMask()
results, err := client.Run()
if err != nil {
return nil, completion.BashCompDirectiveDefault
}
var choices []string
for _, res := range results {
choices = append(choices, res.Name)
}
return choices, completion.BashCompDirectiveNoFileComp
}

@ -206,3 +206,7 @@ func TestListCmd(t *testing.T) {
}}
runTestCmd(t, tests)
}
func TestListOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "list")
}

@ -18,6 +18,7 @@ package main
import (
"fmt"
"io"
"strings"
"github.com/gosuri/uitable"
"github.com/spf13/cobra"
@ -46,3 +47,17 @@ func newPluginListCmd(out io.Writer) *cobra.Command {
}
return cmd
}
// Provide dynamic auto-completion for plugin names
func compListPlugins(toComplete string) []string {
var pNames []string
plugins, err := findPlugins(settings.PluginsDirectory)
if err == nil {
for _, p := range plugins {
if strings.HasPrefix(p.Metadata.Name, toComplete) {
pNames = append(pNames, p.Metadata.Name)
}
}
}
return pNames
}

@ -24,6 +24,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/plugin"
)
@ -33,6 +34,7 @@ type pluginUninstallOptions struct {
func newPluginUninstallCmd(out io.Writer) *cobra.Command {
o := &pluginUninstallOptions{}
cmd := &cobra.Command{
Use: "uninstall <plugin>...",
Aliases: []string{"rm", "remove"},
@ -44,6 +46,15 @@ func newPluginUninstallCmd(out io.Writer) *cobra.Command {
return o.run(out)
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListPlugins(toComplete), completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -24,6 +24,7 @@ import (
"github.com/pkg/errors"
"github.com/spf13/cobra"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/plugin"
"helm.sh/helm/v3/pkg/plugin/installer"
)
@ -34,6 +35,7 @@ type pluginUpdateOptions struct {
func newPluginUpdateCmd(out io.Writer) *cobra.Command {
o := &pluginUpdateOptions{}
cmd := &cobra.Command{
Use: "update <plugin>...",
Aliases: []string{"up"},
@ -45,6 +47,15 @@ func newPluginUpdateCmd(out io.Writer) *cobra.Command {
return o.run(out)
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListPlugins(toComplete), completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -68,6 +69,14 @@ func newPullCmd(out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListCharts(toComplete, false)
})
f := cmd.Flags()
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.")
f.BoolVar(&client.Untar, "untar", false, "if set to true, will untar the chart after downloading it")

@ -24,6 +24,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
)
@ -71,6 +72,14 @@ func newReleaseTestCmd(cfg *action.Configuration, out io.Writer) *cobra.Command
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.DurationVar(&client.Timeout, "timeout", 300*time.Second, "time to wait for any individual Kubernetes operation (like Jobs for hooks)")
f.BoolVar(&outputLogs, "logs", false, "Dump the logs from test pods (this runs after all tests are complete, but before any cleanup)")

@ -18,6 +18,7 @@ package main
import (
"io"
"strings"
"github.com/gosuri/uitable"
"github.com/pkg/errors"
@ -95,3 +96,18 @@ func (r *repoListWriter) encodeByFormat(out io.Writer, format output.Format) err
// WriteJSON and WriteYAML, we shouldn't get invalid types
return nil
}
// Provide dynamic auto-completion for repo names
func compListRepos(prefix string) []string {
var rNames []string
f, err := repo.LoadFile(settings.RepositoryConfig)
if err == nil && len(f.Repositories) > 0 {
for _, repo := range f.Repositories {
if strings.HasPrefix(repo.Name, prefix) {
rNames = append(rNames, repo.Name)
}
}
}
return rNames
}

@ -0,0 +1,25 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"testing"
)
func TestRepoListOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "repo list")
}

@ -26,6 +26,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/repo"
)
@ -38,6 +39,7 @@ type repoRemoveOptions struct {
func newRepoRemoveCmd(out io.Writer) *cobra.Command {
o := &repoRemoveOptions{}
cmd := &cobra.Command{
Use: "remove [NAME]",
Aliases: []string{"rm"},
@ -51,6 +53,14 @@ func newRepoRemoveCmd(out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListRepos(toComplete), completion.BashCompDirectiveNoFileComp
})
return cmd
}

@ -25,6 +25,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -64,6 +65,14 @@ func newRollbackCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.BoolVar(&client.DryRun, "dry-run", false, "simulate a rollback")
f.BoolVar(&client.Recreate, "recreate-pods", false, "performs pods restart for the resource if applicable")

@ -23,55 +23,16 @@ import (
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/internal/experimental/registry"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/cli/output"
)
const (
bashCompletionFunc = `
__helm_override_flag_list=(--kubeconfig --kube-context --namespace -n)
__helm_override_flags()
{
local ${__helm_override_flag_list[*]##*-} two_word_of of var
for w in "${words[@]}"; do
if [ -n "${two_word_of}" ]; then
eval "${two_word_of##*-}=\"${two_word_of}=\${w}\""
two_word_of=
continue
fi
for of in "${__helm_override_flag_list[@]}"; do
case "${w}" in
${of}=*)
eval "${of##*-}=\"${w}\""
;;
${of})
two_word_of="${of}"
;;
esac
done
done
for var in "${__helm_override_flag_list[@]##*-}"; do
if eval "test -n \"\$${var}\""; then
eval "echo \${${var}}"
fi
done
}
__helm_override_flags_to_kubectl_flags()
{
# --kubeconfig, -n, --namespace stay the same for kubectl
# --kube-context becomes --context for kubectl
__helm_debug "${FUNCNAME[0]}: flags to convert: $1"
echo "$1" | \sed s/kube-context/context/
}
__helm_get_repos()
{
eval $(__helm_binary_name) repo list 2>/dev/null | \tail -n +2 | \cut -f1
}
contextCompFunc = `
__helm_get_contexts()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
@ -81,231 +42,6 @@ __helm_get_contexts()
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
__helm_get_namespaces()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local template out
template="{{ range .items }}{{ .metadata.name }} {{ end }}"
flags=$(__helm_override_flags_to_kubectl_flags "$(__helm_override_flags)")
__helm_debug "${FUNCNAME[0]}: override flags for kubectl are: $flags"
# Must use eval in case the flags contain a variable such as $HOME
if out=$(eval kubectl get ${flags} -o template --template=\"${template}\" namespace 2>/dev/null); then
COMPREPLY+=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
__helm_output_options()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
COMPREPLY+=( $( compgen -W "%[1]s" -- "$cur" ) )
}
__helm_binary_name()
{
local helm_binary
helm_binary="${words[0]}"
__helm_debug "${FUNCNAME[0]}: helm_binary is ${helm_binary}"
echo ${helm_binary}
}
# This function prevents the zsh shell from adding a space after
# a completion by adding a second, fake completion
__helm_zsh_comp_nospace() {
__helm_debug "${FUNCNAME[0]}: in is ${in[*]}"
local out in=("$@")
# The shell will normally add a space after these completions.
# To avoid that we should use "compopt -o nospace". However, it is not
# available in zsh.
# Instead, we trick the shell by pretending there is a second, longer match.
# We only do this if there is a single choice left for completion
# to reduce the times the user could be presented with the fake completion choice.
out=($(echo ${in[*]} | \tr " " "\n" | \grep "^${cur}"))
__helm_debug "${FUNCNAME[0]}: out is ${out[*]}"
[ ${#out[*]} -eq 1 ] && out+=("${out}.")
__helm_debug "${FUNCNAME[0]}: out is now ${out[*]}"
echo "${out[*]}"
}
# $1 = 1 if the completion should include local charts (which means file completion)
__helm_list_charts()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local prefix chart repo url file out=() nospace=0 wantFiles=$1
# Handle completions for repos
for repo in $(__helm_get_repos); do
if [[ "${cur}" =~ ^${repo}/.* ]]; then
# We are doing completion from within a repo
local cacheFile=$(eval $(__helm_binary_name) env 2>/dev/null | \grep HELM_REPOSITORY_CACHE | \cut -d= -f2 | \sed s/\"//g)/${repo}-charts.txt
if [ -f "$cacheFile" ]; then
# Get the list of charts from the cached file
prefix=${cur#${repo}/}
for chart in $(\grep ^$prefix $cacheFile); do
out+=(${repo}/${chart})
done
else
# If there is no cached list file, fallback to helm search, which is much slower
# This will happen after the caching feature is first installed but before the user
# does a 'helm repo update' to generate the cached list file.
out=$(eval $(__helm_binary_name) search repo ${cur} 2>/dev/null | \cut -f1 | \grep ^${cur})
fi
nospace=0
elif [[ ${repo} =~ ^${cur}.* ]]; then
# We are completing a repo name
out+=(${repo}/)
nospace=1
fi
done
__helm_debug "${FUNCNAME[0]}: out after repos is ${out[*]}"
# Handle completions for url prefixes
for url in https:// http:// file://; do
if [[ "${cur}" =~ ^${url}.* ]]; then
# The user already put in the full url prefix. Return it
# back as a completion to avoid the shell doing path completion
out="${cur}"
nospace=1
elif [[ ${url} =~ ^${cur}.* ]]; then
# We are completing a url prefix
out+=(${url})
nospace=1
fi
done
__helm_debug "${FUNCNAME[0]}: out after urls is ${out[*]}"
# Handle completion for files.
# We only do this if:
# 1- There are other completions found (if there are no completions,
# the shell will do file completion itself)
# 2- If there is some input from the user (or else we will end up
# listing the entire content of the current directory which will
# be too many choices for the user to find the real repos)
if [ $wantFiles -eq 1 ] && [ -n "${out[*]}" ] && [ -n "${cur}" ]; then
for file in $(\ls); do
if [[ ${file} =~ ^${cur}.* ]]; then
# We are completing a file prefix
out+=(${file})
nospace=1
fi
done
fi
__helm_debug "${FUNCNAME[0]}: out after files is ${out[*]}"
# If the user didn't provide any input to completion,
# we provide a hint that a path can also be used
[ $wantFiles -eq 1 ] && [ -z "${cur}" ] && out+=(./ /)
__helm_debug "${FUNCNAME[0]}: out after checking empty input is ${out[*]}"
if [ $nospace -eq 1 ]; then
if [[ -n "${ZSH_VERSION}" ]]; then
# Don't let the shell add a space after the completion
local tmpout=$(__helm_zsh_comp_nospace "${out[@]}")
unset out
out=$tmpout
elif [[ $(type -t compopt) = "builtin" ]]; then
compopt -o nospace
fi
fi
__helm_debug "${FUNCNAME[0]}: final out is ${out[*]}"
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
}
__helm_list_releases()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local out filter
# Use ^ to map from the start of the release name
filter="^${words[c]}"
# Use eval in case helm_binary_name or __helm_override_flags contains a variable (e.g., $HOME/bin/h3)
if out=$(eval $(__helm_binary_name) list $(__helm_override_flags) -a -q -m 1000 -f ${filter} 2>/dev/null); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
__helm_list_repos()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local out
# Use eval in case helm_binary_name contains a variable (e.g., $HOME/bin/h3)
if out=$(__helm_get_repos); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
__helm_list_plugins()
{
__helm_debug "${FUNCNAME[0]}: c is $c words[c] is ${words[c]}"
local out
# Use eval in case helm_binary_name contains a variable (e.g., $HOME/bin/h3)
if out=$(eval $(__helm_binary_name) plugin list 2>/dev/null | \tail -n +2 | \cut -f1); then
COMPREPLY=( $( compgen -W "${out[*]}" -- "$cur" ) )
fi
}
__helm_list_charts_after_name() {
__helm_debug "${FUNCNAME[0]}: last_command is $last_command"
if [[ ${#nouns[@]} -eq 1 ]]; then
__helm_list_charts 1
fi
}
__helm_list_releases_then_charts() {
__helm_debug "${FUNCNAME[0]}: last_command is $last_command"
if [[ ${#nouns[@]} -eq 0 ]]; then
__helm_list_releases
elif [[ ${#nouns[@]} -eq 1 ]]; then
__helm_list_charts 1
fi
}
__helm_custom_func()
{
__helm_debug "${FUNCNAME[0]}: last_command is $last_command"
case ${last_command} in
helm_pull)
__helm_list_charts 0
return
;;
helm_show_*)
__helm_list_charts 1
return
;;
helm_install | helm_template)
__helm_list_charts_after_name
return
;;
helm_upgrade)
__helm_list_releases_then_charts
return
;;
helm_uninstall | helm_history | helm_status | helm_test |\
helm_rollback | helm_get_*)
__helm_list_releases
return
;;
helm_repo_remove)
__helm_list_repos
return
;;
helm_plugin_uninstall | helm_plugin_update)
__helm_list_plugins
return
;;
*)
;;
esac
}
`
)
@ -313,7 +49,8 @@ var (
// Mapping of global flags that can have dynamic completion and the
// completion function to be used.
bashCompletionFlags = map[string]string{
"namespace": "__helm_get_namespaces",
// Cannot convert the kube-context flag to Go completion yet because
// an incomplete kube-context will make actionConfig.Init() fail at the very start
"kube-context": "__helm_get_contexts",
}
)
@ -364,12 +101,34 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
Long: globalUsage,
SilenceUsage: true,
Args: require.NoArgs,
BashCompletionFunction: fmt.Sprintf(bashCompletionFunc, strings.Join(output.Formats(), " ")),
BashCompletionFunction: fmt.Sprintf("%s%s", contextCompFunc, completion.GetBashCustomFunction()),
}
flags := cmd.PersistentFlags()
settings.AddFlags(flags)
flag := flags.Lookup("namespace")
// Setup shell completion for the namespace flag
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if client, err := actionConfig.KubernetesClientSet(); err == nil {
// Choose a long enough timeout that the user notices somethings is not working
// but short enough that the user is not made to wait very long
to := int64(3)
completion.CompDebugln(fmt.Sprintf("About to call kube client for namespaces with timeout of: %d", to))
nsNames := []string{}
if namespaces, err := client.CoreV1().Namespaces().List(metav1.ListOptions{TimeoutSeconds: &to}); err == nil {
for _, ns := range namespaces.Items {
if strings.HasPrefix(ns.Name, toComplete) {
nsNames = append(nsNames, ns.Name)
}
}
return nsNames, completion.BashCompDirectiveNoFileComp
}
}
return nil, completion.BashCompDirectiveDefault
})
// We can safely ignore any errors that flags.Parse encounters since
// those errors will be caught later during the call to cmd.Execution.
// This call is required to gather configuration information prior to
@ -409,6 +168,9 @@ func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string
// Hidden documentation generator command: 'helm docs'
newDocsCmd(out),
// Setup the special hidden __complete command to allow for dynamic auto-completion
completion.NewCompleteCmd(settings, out),
)
// Add annotation to flags for which we can generate completion choices

@ -49,5 +49,8 @@ func TestSearchHubCmd(t *testing.T) {
t.Log(out)
t.Log(expected)
}
}
func TestSearchHubOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "search hub")
}

@ -17,8 +17,11 @@ limitations under the License.
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"io/ioutil"
"path/filepath"
"strings"
@ -28,6 +31,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/search"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/cli/output"
"helm.sh/helm/v3/pkg/helmpath"
"helm.sh/helm/v3/pkg/repo"
@ -246,3 +250,137 @@ func (r *repoSearchWriter) encodeByFormat(out io.Writer, format output.Format) e
// WriteJSON and WriteYAML, we shouldn't get invalid types
return nil
}
// Provides the list of charts that are part of the specified repo, and that starts with 'prefix'.
func compListChartsOfRepo(repoName string, prefix string) []string {
var charts []string
path := filepath.Join(settings.RepositoryCache, helmpath.CacheChartsFile(repoName))
content, err := ioutil.ReadFile(path)
if err == nil {
scanner := bufio.NewScanner(bytes.NewReader(content))
for scanner.Scan() {
fullName := fmt.Sprintf("%s/%s", repoName, scanner.Text())
if strings.HasPrefix(fullName, prefix) {
charts = append(charts, fullName)
}
}
return charts
}
if isNotExist(err) {
// If there is no cached charts file, fallback to the full index file.
// This is much slower but can happen after the caching feature is first
// installed but before the user does a 'helm repo update' to generate the
// first cached charts file.
path = filepath.Join(settings.RepositoryCache, helmpath.CacheIndexFile(repoName))
if indexFile, err := repo.LoadIndexFile(path); err == nil {
for name := range indexFile.Entries {
fullName := fmt.Sprintf("%s/%s", repoName, name)
if strings.HasPrefix(fullName, prefix) {
charts = append(charts, fullName)
}
}
return charts
}
}
return []string{}
}
// Provide dynamic auto-completion for commands that operate on charts (e.g., helm show)
// When true, the includeFiles argument indicates that completion should include local files (e.g., local charts)
func compListCharts(toComplete string, includeFiles bool) ([]string, completion.BashCompDirective) {
completion.CompDebugln(fmt.Sprintf("compListCharts with toComplete %s", toComplete))
noSpace := false
noFile := false
var completions []string
// First check completions for repos
repos := compListRepos("")
for _, repo := range repos {
repoWithSlash := fmt.Sprintf("%s/", repo)
if strings.HasPrefix(toComplete, repoWithSlash) {
// Must complete with charts within the specified repo
completions = append(completions, compListChartsOfRepo(repo, toComplete)...)
noSpace = false
break
} else if strings.HasPrefix(repo, toComplete) {
// Must complete the repo name
completions = append(completions, repoWithSlash)
noSpace = true
}
}
completion.CompDebugln(fmt.Sprintf("Completions after repos: %v", completions))
// Now handle completions for url prefixes
for _, url := range []string{"https://", "http://", "file://"} {
if strings.HasPrefix(toComplete, url) {
// The user already put in the full url prefix; we don't have
// anything to add, but make sure the shell does not default
// to file completion since we could be returning an empty array.
noFile = true
noSpace = true
} else if strings.HasPrefix(url, toComplete) {
// We are completing a url prefix
completions = append(completions, url)
noSpace = true
}
}
completion.CompDebugln(fmt.Sprintf("Completions after urls: %v", completions))
// Finally, provide file completion if we need to.
// We only do this if:
// 1- There are other completions found (if there are no completions,
// the shell will do file completion itself)
// 2- If there is some input from the user (or else we will end up
// listing the entire content of the current directory which will
// be too many choices for the user to find the real repos)
if includeFiles && len(completions) > 0 && len(toComplete) > 0 {
if files, err := ioutil.ReadDir("."); err == nil {
for _, file := range files {
if strings.HasPrefix(file.Name(), toComplete) {
// We are completing a file prefix
completions = append(completions, file.Name())
}
}
}
}
completion.CompDebugln(fmt.Sprintf("Completions after files: %v", completions))
// If the user didn't provide any input to completion,
// we provide a hint that a path can also be used
if includeFiles && len(toComplete) == 0 {
completions = append(completions, "./", "/")
}
completion.CompDebugln(fmt.Sprintf("Completions after checking empty input: %v", completions))
directive := completion.BashCompDirectiveDefault
if noFile {
directive = directive | completion.BashCompDirectiveNoFileComp
}
if noSpace {
directive = directive | completion.BashCompDirectiveNoSpace
// The completion.BashCompDirective flags do not work for zsh right now.
// We handle it ourselves instead.
completions = compEnforceNoSpace(completions)
}
return completions, directive
}
// This function prevents the shell from adding a space after
// a completion by adding a second, fake completion.
// It is only needed for zsh, but we cannot tell which shell
// is being used here, so we do the fake completion all the time;
// there are no real downsides to doing this for bash as well.
func compEnforceNoSpace(completions []string) []string {
// To prevent the shell from adding space after the completion,
// we trick it by pretending there is a second, longer match.
// We only do this if there is a single choice for completion.
if len(completions) == 1 {
completions = append(completions, completions[0]+".")
completion.CompDebugln(fmt.Sprintf("compEnforceNoSpace: completions now are %v", completions))
}
return completions
}

@ -83,3 +83,7 @@ func TestSearchRepositoriesCmd(t *testing.T) {
}
runTestCmd(t, tests)
}
func TestSearchRepoOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "search repo")
}

@ -23,6 +23,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -61,6 +62,14 @@ func newShowCmd(out io.Writer) *cobra.Command {
Args: require.NoArgs,
}
// Function providing dynamic auto-completion
validArgsFunc := func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListCharts(toComplete, true)
}
all := &cobra.Command{
Use: "all [CHART]",
Short: "shows all information of the chart",
@ -145,6 +154,9 @@ func newShowCmd(out io.Writer) *cobra.Command {
for _, subCmd := range cmds {
addChartPathOptionsFlags(subCmd.Flags(), &client.ChartPathOptions)
showCommand.AddCommand(subCmd)
// Register the completion function for each subcommand
completion.RegisterValidArgsFunc(subCmd, validArgsFunc)
}
return showCommand

@ -25,6 +25,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli/output"
@ -65,8 +66,25 @@ func newStatusCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.PersistentFlags()
f.IntVar(&client.Version, "revision", 0, "if set, display the status of the named release with revision")
flag := f.Lookup("revision")
completion.RegisterFlagCompletionFunc(flag, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 1 {
return compListRevisions(cfg, args[0])
}
return nil, completion.BashCompDirectiveNoFileComp
})
bindOutputFlag(cmd, &outfmt)
return cmd

@ -108,3 +108,66 @@ func mustParseTime(t string) helmtime.Time {
res, _ := helmtime.Parse(time.RFC3339, t)
return res
}
func TestStatusCompletion(t *testing.T) {
releasesMockWithStatus := func(info *release.Info, hooks ...*release.Hook) []*release.Release {
info.LastDeployed = helmtime.Unix(1452902400, 0).UTC()
return []*release.Release{{
Name: "athos",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "porthos",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "aramis",
Namespace: "default",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}, {
Name: "dartagnan",
Namespace: "gascony",
Info: info,
Chart: &chart.Chart{},
Hooks: hooks,
}}
}
tests := []cmdTestCase{{
name: "completion for status",
cmd: "__complete status a",
golden: "output/status-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for status with too many arguments",
cmd: "__complete status dartagnan ''",
golden: "output/status-wrong-args-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}, {
name: "completion for status with too many arguments",
cmd: "__complete status --debug a",
golden: "output/status-comp.txt",
rels: releasesMockWithStatus(&release.Info{
Status: release.StatusDeployed,
}),
}}
runTestCmd(t, tests)
}
func TestStatusRevisionCompletion(t *testing.T) {
revisionFlagCompletionTest(t, "status")
}
func TestStatusOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "status")
}

@ -29,6 +29,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chartutil"
"helm.sh/helm/v3/pkg/cli/values"
@ -123,6 +124,11 @@ func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
return compInstall(args, toComplete, client)
})
f := cmd.Flags()
addInstallFlags(f, client, valueOpts)
f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")

@ -0,0 +1,4 @@
table
json
yaml
:0

@ -0,0 +1,3 @@
aramis
athos
:4

@ -24,6 +24,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
)
@ -64,6 +65,14 @@ func newUninstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) != 0 {
return nil, completion.BashCompDirectiveNoFileComp
}
return compListReleases(toComplete, cfg)
})
f := cmd.Flags()
f.BoolVar(&client.DryRun, "dry-run", false, "simulate a uninstall")
f.BoolVar(&client.DisableHooks, "no-hooks", false, "prevent hooks from running during uninstallation")

@ -25,6 +25,7 @@ import (
"github.com/spf13/cobra"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/internal/completion"
"helm.sh/helm/v3/pkg/action"
"helm.sh/helm/v3/pkg/chart/loader"
"helm.sh/helm/v3/pkg/cli/output"
@ -144,6 +145,17 @@ func newUpgradeCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
},
}
// Function providing dynamic auto-completion
completion.RegisterValidArgsFunc(cmd, func(cmd *cobra.Command, args []string, toComplete string) ([]string, completion.BashCompDirective) {
if len(args) == 0 {
return compListReleases(toComplete, cfg)
}
if len(args) == 1 {
return compListCharts(toComplete, true)
}
return nil, completion.BashCompDirectiveNoFileComp
})
f := cmd.Flags()
f.BoolVarP(&client.Install, "install", "i", false, "if a release by this name doesn't already exist, run an install")
f.BoolVar(&client.Devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored")

@ -258,3 +258,7 @@ func prepareMockRelease(releaseName string, t *testing.T) (func(n string, v int,
return relMock, ch, chartPath
}
func TestUpgradeOutputCompletion(t *testing.T) {
outputFlagCompletionTest(t, "upgrade")
}

@ -0,0 +1,388 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package completion
import (
"errors"
"fmt"
"io"
"log"
"os"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"helm.sh/helm/v3/cmd/helm/require"
"helm.sh/helm/v3/pkg/cli"
)
// ==================================================================================
// The below code supports dynamic shell completion in Go.
// This should ultimately be pushed down into Cobra.
// ==================================================================================
// compRequestCmd Hidden command to request completion results from the program.
// Used by the shell completion script.
const compRequestCmd = "__complete"
// Global map allowing to find completion functions for commands or flags.
var validArgsFunctions = map[interface{}]func(cmd *cobra.Command, args []string, toComplete string) ([]string, BashCompDirective){}
// BashCompDirective is a bit map representing the different behaviors the shell
// can be instructed to have once completions have been provided.
type BashCompDirective int
const (
// BashCompDirectiveError indicates an error occurred and completions should be ignored.
BashCompDirectiveError BashCompDirective = 1 << iota
// BashCompDirectiveNoSpace indicates that the shell should not add a space
// after the completion even if there is a single completion provided.
BashCompDirectiveNoSpace
// BashCompDirectiveNoFileComp indicates that the shell should not provide
// file completion even when no completion is provided.
// This currently does not work for zsh or bash < 4
BashCompDirectiveNoFileComp
// BashCompDirectiveDefault indicates to let the shell perform its default
// behavior after completions have been provided.
BashCompDirectiveDefault BashCompDirective = 0
)
// GetBashCustomFunction returns the bash code to handle custom go completion
// This should eventually be provided by Cobra
func GetBashCustomFunction() string {
return fmt.Sprintf(`
__helm_custom_func()
{
__helm_debug "${FUNCNAME[0]}: c is $c, words[@] is ${words[@]}, #words[@] is ${#words[@]}"
__helm_debug "${FUNCNAME[0]}: cur is ${cur}, cword is ${cword}, words is ${words}"
local out requestComp lastParam lastChar
requestComp="${words[0]} %[1]s ${words[@]:1}"
lastParam=${words[$((${#words[@]}-1))]}
lastChar=${lastParam:$((${#lastParam}-1)):1}
__helm_debug "${FUNCNAME[0]}: lastParam ${lastParam}, lastChar ${lastChar}"
if [ -z "${cur}" ] && [ "${lastChar}" != "=" ]; then
# If the last parameter is complete (there is a space following it)
# We add an extra empty parameter so we can indicate this to the go method.
__helm_debug "${FUNCNAME[0]}: Adding extra empty parameter"
requestComp="${requestComp} \"\""
fi
__helm_debug "${FUNCNAME[0]}: calling ${requestComp}"
# Use eval to handle any environment variables and such
out=$(eval ${requestComp} 2>/dev/null)
# Extract the directive int at the very end of the output following a :
directive=${out##*:}
# Remove the directive
out=${out%%:*}
if [ "${directive}" = "${out}" ]; then
# There is not directive specified
directive=0
fi
__helm_debug "${FUNCNAME[0]}: the completion directive is: ${directive}"
__helm_debug "${FUNCNAME[0]}: the completions are: ${out[*]}"
if [ $((${directive} & %[2]d)) -ne 0 ]; then
__helm_debug "${FUNCNAME[0]}: received error, completion failed"
else
if [ $((${directive} & %[3]d)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__helm_debug "${FUNCNAME[0]}: activating no space"
compopt -o nospace
fi
fi
if [ $((${directive} & %[4]d)) -ne 0 ]; then
if [[ $(type -t compopt) = "builtin" ]]; then
__helm_debug "${FUNCNAME[0]}: activating no file completion"
compopt +o default
fi
fi
while IFS='' read -r comp; do
COMPREPLY+=("$comp")
done < <(compgen -W "${out[*]}" -- "$cur")
fi
}
`, compRequestCmd, BashCompDirectiveError, BashCompDirectiveNoSpace, BashCompDirectiveNoFileComp)
}
// RegisterValidArgsFunc should be called to register a function to provide argument completion for a command
func RegisterValidArgsFunc(cmd *cobra.Command, f func(cmd *cobra.Command, args []string, toComplete string) ([]string, BashCompDirective)) {
if _, exists := validArgsFunctions[cmd]; exists {
log.Fatal(fmt.Sprintf("RegisterValidArgsFunc: command '%s' already registered", cmd.Name()))
}
validArgsFunctions[cmd] = f
}
// RegisterFlagCompletionFunc should be called to register a function to provide completion for a flag
func RegisterFlagCompletionFunc(flag *pflag.Flag, f func(cmd *cobra.Command, args []string, toComplete string) ([]string, BashCompDirective)) {
if _, exists := validArgsFunctions[flag]; exists {
log.Fatal(fmt.Sprintf("RegisterFlagCompletionFunc: flag '%s' already registered", flag.Name))
}
validArgsFunctions[flag] = f
// Make sure the completion script call the __helm_custom_func for the registered flag.
// This is essential to make the = form work. E.g., helm -n=<TAB> or helm status --output=<TAB>
if flag.Annotations == nil {
flag.Annotations = map[string][]string{}
}
flag.Annotations[cobra.BashCompCustom] = []string{"__helm_custom_func"}
}
var debug = true
// Returns a string listing the different directive enabled in the specified parameter
func (d BashCompDirective) string() string {
var directives []string
if d&BashCompDirectiveError != 0 {
directives = append(directives, "BashCompDirectiveError")
}
if d&BashCompDirectiveNoSpace != 0 {
directives = append(directives, "BashCompDirectiveNoSpace")
}
if d&BashCompDirectiveNoFileComp != 0 {
directives = append(directives, "BashCompDirectiveNoFileComp")
}
if len(directives) == 0 {
directives = append(directives, "BashCompDirectiveDefault")
}
if d > BashCompDirectiveError+BashCompDirectiveNoSpace+BashCompDirectiveNoFileComp {
return fmt.Sprintf("ERROR: unexpected BashCompDirective value: %d", d)
}
return strings.Join(directives, ", ")
}
// NewCompleteCmd add a special hidden command that an be used to request completions
func NewCompleteCmd(settings *cli.EnvSettings, out io.Writer) *cobra.Command {
debug = settings.Debug
return &cobra.Command{
Use: fmt.Sprintf("%s [command-line]", compRequestCmd),
DisableFlagsInUseLine: true,
Hidden: true,
DisableFlagParsing: true,
Args: require.MinimumNArgs(1),
Short: "Request shell completion choices for the specified command-line",
Long: fmt.Sprintf("%s is a special command that is used by the shell completion logic\n%s",
compRequestCmd, "to request completion choices for the specified command-line."),
Run: func(cmd *cobra.Command, args []string) {
CompDebugln(fmt.Sprintf("%s was called with args %v", cmd.Name(), args))
flag, trimmedArgs, toComplete, err := checkIfFlagCompletion(cmd.Root(), args[:len(args)-1], args[len(args)-1])
if err != nil {
// Error while attempting to parse flags
CompErrorln(err.Error())
return
}
// Find the real command for which completion must be performed
finalCmd, finalArgs, err := cmd.Root().Find(trimmedArgs)
if err != nil {
// Unable to find the real command. E.g., helm invalidCmd <TAB>
return
}
CompDebugln(fmt.Sprintf("Found final command '%s', with finalArgs %v", finalCmd.Name(), finalArgs))
// Parse the flags and extract the arguments to prepare for calling the completion function
if err = finalCmd.ParseFlags(finalArgs); err != nil {
CompErrorln(fmt.Sprintf("Error while parsing flags from args %v: %s", finalArgs, err.Error()))
return
}
argsWoFlags := finalCmd.Flags().Args()
CompDebugln(fmt.Sprintf("Args without flags are '%v' with length %d", argsWoFlags, len(argsWoFlags)))
var key interface{}
var keyStr string
if flag != nil {
key = flag
keyStr = flag.Name
} else {
key = finalCmd
keyStr = finalCmd.Name()
}
// Find completion function for the flag or command
completionFn, ok := validArgsFunctions[key]
if !ok {
CompErrorln(fmt.Sprintf("Dynamic completion not supported/needed for flag or command: %s", keyStr))
return
}
CompDebugln(fmt.Sprintf("Calling completion method for subcommand '%s' with args '%v' and toComplete '%s'", finalCmd.Name(), argsWoFlags, toComplete))
completions, directive := completionFn(finalCmd, argsWoFlags, toComplete)
for _, comp := range completions {
// Print each possible completion to stdout for the completion script to consume.
fmt.Fprintln(out, comp)
}
// As the last printout, print the completion directive for the
// completion script to parse.
// The directive integer must be that last character following a single :
// The completion script expects :directive
fmt.Fprintln(out, fmt.Sprintf(":%d", directive))
// Print some helpful info to stderr for the user to understand.
// Output from stderr should be ignored from the completion script.
fmt.Fprintf(os.Stderr, "Completion ended with directive: %s\n", directive.string())
},
}
}
func isFlag(arg string) bool {
return len(arg) > 0 && arg[0] == '-'
}
func checkIfFlagCompletion(rootCmd *cobra.Command, args []string, lastArg string) (*pflag.Flag, []string, string, error) {
var flagName string
trimmedArgs := args
flagWithEqual := false
if isFlag(lastArg) {
if index := strings.Index(lastArg, "="); index >= 0 {
flagName = strings.TrimLeft(lastArg[:index], "-")
lastArg = lastArg[index+1:]
flagWithEqual = true
} else {
return nil, nil, "", errors.New("Unexpected completion request for flag")
}
}
if len(flagName) == 0 {
if len(args) > 0 {
prevArg := args[len(args)-1]
if isFlag(prevArg) {
// If the flag contains an = it means it has already been fully processed
if index := strings.Index(prevArg, "="); index < 0 {
flagName = strings.TrimLeft(prevArg, "-")
// Remove the uncompleted flag or else Cobra could complain about
// an invalid value for that flag e.g., helm status --output j<TAB>
trimmedArgs = args[:len(args)-1]
}
}
}
}
if len(flagName) == 0 {
// Not doing flag completion
return nil, trimmedArgs, lastArg, nil
}
// Find the real command for which completion must be performed
finalCmd, _, err := rootCmd.Find(trimmedArgs)
if err != nil {
// Unable to find the real command. E.g., helm invalidCmd <TAB>
return nil, nil, "", errors.New("Unable to find final command for completion")
}
CompDebugln(fmt.Sprintf("checkIfFlagCompletion: found final command '%s'", finalCmd.Name()))
flag := findFlag(finalCmd, flagName)
if flag == nil {
// Flag not supported by this command, nothing to complete
err = fmt.Errorf("Subcommand '%s' does not support flag '%s'", finalCmd.Name(), flagName)
return nil, nil, "", err
}
if !flagWithEqual {
if len(flag.NoOptDefVal) != 0 {
// We had assumed dealing with a two-word flag but the flag is a boolean flag.
// In that case, there is no value following it, so we are not really doing flag completion.
// Reset everything to do argument completion.
trimmedArgs = args
flag = nil
}
}
return flag, trimmedArgs, lastArg, nil
}
func findFlag(cmd *cobra.Command, name string) *pflag.Flag {
flagSet := cmd.Flags()
if len(name) == 1 {
// First convert the short flag into a long flag
// as the cmd.Flag() search only accepts long flags
if short := flagSet.ShorthandLookup(name); short != nil {
CompDebugln(fmt.Sprintf("checkIfFlagCompletion: found flag '%s' which we will change to '%s'", name, short.Name))
name = short.Name
} else {
set := cmd.InheritedFlags()
if short = set.ShorthandLookup(name); short != nil {
CompDebugln(fmt.Sprintf("checkIfFlagCompletion: found inherited flag '%s' which we will change to '%s'", name, short.Name))
name = short.Name
} else {
return nil
}
}
}
return cmd.Flag(name)
}
// CompDebug prints the specified string to the same file as where the
// completion script prints its logs.
// Note that completion printouts should never be on stdout as they would
// be wrongly interpreted as actual completion choices by the completion script.
func CompDebug(msg string) {
msg = fmt.Sprintf("[Debug] %s", msg)
// Such logs are only printed when the user has set the environment
// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
if path := os.Getenv("BASH_COMP_DEBUG_FILE"); path != "" {
f, err := os.OpenFile(path,
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err == nil {
defer f.Close()
f.WriteString(msg)
}
}
if debug {
// Must print to stderr for this not to be read by the completion script.
fmt.Fprintf(os.Stderr, msg)
}
}
// CompDebugln prints the specified string with a newline at the end
// to the same file as where the completion script prints its logs.
// Such logs are only printed when the user has set the environment
// variable BASH_COMP_DEBUG_FILE to the path of some file to be used.
func CompDebugln(msg string) {
CompDebug(fmt.Sprintf("%s\n", msg))
}
// CompError prints the specified completion message to stderr.
func CompError(msg string) {
msg = fmt.Sprintf("[Error] %s", msg)
CompDebug(msg)
// If not already printed by the call to CompDebug().
if !debug {
// Must print to stderr for this not to be read by the completion script.
fmt.Fprintf(os.Stderr, msg)
}
}
// CompErrorln prints the specified completion message to stderr with a newline at the end.
func CompErrorln(msg string) {
CompError(fmt.Sprintf("%s\n", msg))
}
Loading…
Cancel
Save