mirror of
https://github.com/spf13/cobra
synced 2024-11-24 22:57:12 +00:00
5a793296fb
We are no longer actively maintaining bash completion v1 in favor of its more rich v2 version. Previously, using bash completion v2 required projects to be aware of its existence and to explicitly call GenBashCompletionV2(). With this commit, any projects calling GenBashCompletion() will automatically be redirected to using the v2 version. One exception is if the project uses the legacy custom completion logic which is not supported in v2. We can detect that by looking for the use of the field `BashCompletionFunction` on the root command. Note that descriptions are kept off when calling GenBashCompletion(). This means that to enable completion descriptions for bash, a project must still explicitly call GenBashCompletionV2(). Signed-off-by: Marc Khouzam <marc.khouzam@gmail.com>
366 lines
12 KiB
Go
366 lines
12 KiB
Go
// Copyright 2013-2022 The Cobra 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 cobra
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func checkOmit(t *testing.T, found, unexpected string) {
|
|
if strings.Contains(found, unexpected) {
|
|
t.Errorf("Got: %q\nBut should not have!\n", unexpected)
|
|
}
|
|
}
|
|
|
|
func check(t *testing.T, found, expected string) {
|
|
if !strings.Contains(found, expected) {
|
|
t.Errorf("Expecting to contain: \n %q\nGot:\n %q\n", expected, found)
|
|
}
|
|
}
|
|
|
|
func checkNumOccurrences(t *testing.T, found, expected string, expectedOccurrences int) {
|
|
numOccurrences := strings.Count(found, expected)
|
|
if numOccurrences != expectedOccurrences {
|
|
t.Errorf("Expecting to contain %d occurrences of: \n %q\nGot %d:\n %q\n", expectedOccurrences, expected, numOccurrences, found)
|
|
}
|
|
}
|
|
|
|
func checkRegex(t *testing.T, found, pattern string) {
|
|
matched, err := regexp.MatchString(pattern, found)
|
|
if err != nil {
|
|
t.Errorf("Error thrown performing MatchString: \n %s\n", err)
|
|
}
|
|
if !matched {
|
|
t.Errorf("Expecting to match: \n %q\nGot:\n %q\n", pattern, found)
|
|
}
|
|
}
|
|
|
|
func runShellCheck(s string) error {
|
|
cmd := exec.Command("shellcheck", "-s", "bash", "-", "-e",
|
|
"SC2034", // PREFIX appears unused. Verify it or export it.
|
|
)
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Stdout = os.Stdout
|
|
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
go func() {
|
|
_, err := stdin.Write([]byte(s))
|
|
CheckErr(err)
|
|
|
|
stdin.Close()
|
|
}()
|
|
|
|
return cmd.Run()
|
|
}
|
|
|
|
// World worst custom function, just keep telling you to enter hello!
|
|
const bashCompletionFunc = `__root_custom_func() {
|
|
COMPREPLY=( "hello" )
|
|
}
|
|
`
|
|
|
|
func TestBashCompletions(t *testing.T) {
|
|
rootCmd := &Command{
|
|
Use: "root",
|
|
ArgAliases: []string{"pods", "nodes", "services", "replicationcontrollers", "po", "no", "svc", "rc"},
|
|
ValidArgs: []string{"pod", "node", "service", "replicationcontroller"},
|
|
BashCompletionFunction: bashCompletionFunc,
|
|
Run: emptyRun,
|
|
}
|
|
rootCmd.Flags().IntP("introot", "i", -1, "help message for flag introot")
|
|
assertNoErr(t, rootCmd.MarkFlagRequired("introot"))
|
|
|
|
// Filename.
|
|
rootCmd.Flags().String("filename", "", "Enter a filename")
|
|
assertNoErr(t, rootCmd.MarkFlagFilename("filename", "json", "yaml", "yml"))
|
|
|
|
// Persistent filename.
|
|
rootCmd.PersistentFlags().String("persistent-filename", "", "Enter a filename")
|
|
assertNoErr(t, rootCmd.MarkPersistentFlagFilename("persistent-filename"))
|
|
assertNoErr(t, rootCmd.MarkPersistentFlagRequired("persistent-filename"))
|
|
|
|
// Filename extensions.
|
|
rootCmd.Flags().String("filename-ext", "", "Enter a filename (extension limited)")
|
|
assertNoErr(t, rootCmd.MarkFlagFilename("filename-ext"))
|
|
rootCmd.Flags().String("custom", "", "Enter a filename (extension limited)")
|
|
assertNoErr(t, rootCmd.MarkFlagCustom("custom", "__complete_custom"))
|
|
|
|
// Subdirectories in a given directory.
|
|
rootCmd.Flags().String("theme", "", "theme to use (located in /themes/THEMENAME/)")
|
|
assertNoErr(t, rootCmd.Flags().SetAnnotation("theme", BashCompSubdirsInDir, []string{"themes"}))
|
|
|
|
// For two word flags check
|
|
rootCmd.Flags().StringP("two", "t", "", "this is two word flags")
|
|
rootCmd.Flags().BoolP("two-w-default", "T", false, "this is not two word flags")
|
|
|
|
echoCmd := &Command{
|
|
Use: "echo [string to echo]",
|
|
Aliases: []string{"say"},
|
|
Short: "Echo anything to the screen",
|
|
Long: "an utterly useless command for testing.",
|
|
Example: "Just run cobra-test echo",
|
|
Run: emptyRun,
|
|
}
|
|
|
|
echoCmd.Flags().String("filename", "", "Enter a filename")
|
|
assertNoErr(t, echoCmd.MarkFlagFilename("filename", "json", "yaml", "yml"))
|
|
echoCmd.Flags().String("config", "", "config to use (located in /config/PROFILE/)")
|
|
assertNoErr(t, echoCmd.Flags().SetAnnotation("config", BashCompSubdirsInDir, []string{"config"}))
|
|
|
|
printCmd := &Command{
|
|
Use: "print [string to print]",
|
|
Args: MinimumNArgs(1),
|
|
Short: "Print anything to the screen",
|
|
Long: "an absolutely utterly useless command for testing.",
|
|
Run: emptyRun,
|
|
}
|
|
|
|
deprecatedCmd := &Command{
|
|
Use: "deprecated [can't do anything here]",
|
|
Args: NoArgs,
|
|
Short: "A command which is deprecated",
|
|
Long: "an absolutely utterly useless command for testing deprecation!.",
|
|
Deprecated: "Please use echo instead",
|
|
Run: emptyRun,
|
|
}
|
|
|
|
colonCmd := &Command{
|
|
Use: "cmd:colon",
|
|
Run: emptyRun,
|
|
}
|
|
|
|
timesCmd := &Command{
|
|
Use: "times [# times] [string to echo]",
|
|
SuggestFor: []string{"counts"},
|
|
Args: OnlyValidArgs,
|
|
ValidArgs: []string{"one", "two", "three", "four"},
|
|
Short: "Echo anything to the screen more times",
|
|
Long: "a slightly useless command for testing.",
|
|
Run: emptyRun,
|
|
}
|
|
|
|
echoCmd.AddCommand(timesCmd)
|
|
rootCmd.AddCommand(echoCmd, printCmd, deprecatedCmd, colonCmd)
|
|
|
|
buf := new(bytes.Buffer)
|
|
assertNoErr(t, rootCmd.GenBashCompletion(buf))
|
|
output := buf.String()
|
|
|
|
check(t, output, "_root")
|
|
check(t, output, "_root_echo")
|
|
check(t, output, "_root_echo_times")
|
|
check(t, output, "_root_print")
|
|
check(t, output, "_root_cmd__colon")
|
|
|
|
// check for required flags
|
|
check(t, output, `must_have_one_flag+=("--introot=")`)
|
|
check(t, output, `must_have_one_flag+=("--persistent-filename=")`)
|
|
// check for custom completion function with both qualified and unqualified name
|
|
checkNumOccurrences(t, output, `__custom_func`, 2) // 1. check existence, 2. invoke
|
|
checkNumOccurrences(t, output, `__root_custom_func`, 3) // 1. check existence, 2. invoke, 3. actual definition
|
|
// check for custom completion function body
|
|
check(t, output, `COMPREPLY=( "hello" )`)
|
|
// check for required nouns
|
|
check(t, output, `must_have_one_noun+=("pod")`)
|
|
// check for noun aliases
|
|
check(t, output, `noun_aliases+=("pods")`)
|
|
check(t, output, `noun_aliases+=("rc")`)
|
|
checkOmit(t, output, `must_have_one_noun+=("pods")`)
|
|
// check for filename extension flags
|
|
check(t, output, `flags_completion+=("_filedir")`)
|
|
// check for filename extension flags
|
|
check(t, output, `must_have_one_noun+=("three")`)
|
|
// check for filename extension flags
|
|
check(t, output, fmt.Sprintf(`flags_completion+=("__%s_handle_filename_extension_flag json|yaml|yml")`, rootCmd.Name()))
|
|
// check for filename extension flags in a subcommand
|
|
checkRegex(t, output, fmt.Sprintf(`_root_echo\(\)\n{[^}]*flags_completion\+=\("__%s_handle_filename_extension_flag json\|yaml\|yml"\)`, rootCmd.Name()))
|
|
// check for custom flags
|
|
check(t, output, `flags_completion+=("__complete_custom")`)
|
|
// check for subdirs_in_dir flags
|
|
check(t, output, fmt.Sprintf(`flags_completion+=("__%s_handle_subdirs_in_dir_flag themes")`, rootCmd.Name()))
|
|
// check for subdirs_in_dir flags in a subcommand
|
|
checkRegex(t, output, fmt.Sprintf(`_root_echo\(\)\n{[^}]*flags_completion\+=\("__%s_handle_subdirs_in_dir_flag config"\)`, rootCmd.Name()))
|
|
|
|
// check two word flags
|
|
check(t, output, `two_word_flags+=("--two")`)
|
|
check(t, output, `two_word_flags+=("-t")`)
|
|
checkOmit(t, output, `two_word_flags+=("--two-w-default")`)
|
|
checkOmit(t, output, `two_word_flags+=("-T")`)
|
|
|
|
// check local nonpersistent flag
|
|
check(t, output, `local_nonpersistent_flags+=("--two")`)
|
|
check(t, output, `local_nonpersistent_flags+=("--two=")`)
|
|
check(t, output, `local_nonpersistent_flags+=("-t")`)
|
|
check(t, output, `local_nonpersistent_flags+=("--two-w-default")`)
|
|
check(t, output, `local_nonpersistent_flags+=("-T")`)
|
|
|
|
checkOmit(t, output, deprecatedCmd.Name())
|
|
|
|
// If available, run shellcheck against the script.
|
|
if err := exec.Command("which", "shellcheck").Run(); err != nil {
|
|
return
|
|
}
|
|
if err := runShellCheck(output); err != nil {
|
|
t.Fatalf("shellcheck failed: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBashCompletionHiddenFlag(t *testing.T) {
|
|
c := &Command{
|
|
Use: "c",
|
|
// Set the legacy BashCompletionFunction to force the
|
|
// use of bash completion V1
|
|
BashCompletionFunction: bashCompletionFunc,
|
|
Run: emptyRun,
|
|
}
|
|
|
|
const validFlagName = "valid-flag"
|
|
c.Flags().Bool(validFlagName, false, "")
|
|
|
|
const hiddenFlagName = "hiddenFlag"
|
|
c.Flags().Bool(hiddenFlagName, false, "")
|
|
assertNoErr(t, c.Flags().MarkHidden(hiddenFlagName))
|
|
|
|
buf := new(bytes.Buffer)
|
|
assertNoErr(t, c.GenBashCompletion(buf))
|
|
output := buf.String()
|
|
|
|
if !strings.Contains(output, validFlagName) {
|
|
t.Errorf("Expected completion to include %q flag: Got %v", validFlagName, output)
|
|
}
|
|
|
|
if strings.Contains(output, hiddenFlagName) {
|
|
t.Errorf("Expected completion to not include %q flag: Got %v", hiddenFlagName, output)
|
|
}
|
|
}
|
|
|
|
func TestBashCompletionDeprecatedFlag(t *testing.T) {
|
|
c := &Command{
|
|
Use: "c",
|
|
// Set the legacy BashCompletionFunction to force the
|
|
// use of bash completion V1
|
|
BashCompletionFunction: bashCompletionFunc,
|
|
Run: emptyRun,
|
|
}
|
|
|
|
const validFlagName = "valid-flag"
|
|
c.Flags().Bool(validFlagName, false, "")
|
|
|
|
const deprecatedFlagName = "deprecated-flag"
|
|
c.Flags().Bool(deprecatedFlagName, false, "")
|
|
assertNoErr(t, c.Flags().MarkDeprecated(deprecatedFlagName, "use --not-deprecated instead"))
|
|
|
|
buf := new(bytes.Buffer)
|
|
assertNoErr(t, c.GenBashCompletion(buf))
|
|
output := buf.String()
|
|
|
|
if !strings.Contains(output, validFlagName) {
|
|
t.Errorf("expected completion to include %q flag: Got %v", validFlagName, output)
|
|
}
|
|
|
|
if strings.Contains(output, deprecatedFlagName) {
|
|
t.Errorf("expected completion to not include %q flag: Got %v", deprecatedFlagName, output)
|
|
}
|
|
}
|
|
|
|
func TestBashCompletionTraverseChildren(t *testing.T) {
|
|
c := &Command{
|
|
Use: "c",
|
|
// Set the legacy BashCompletionFunction to force the
|
|
// use of bash completion V1
|
|
BashCompletionFunction: bashCompletionFunc,
|
|
Run: emptyRun,
|
|
TraverseChildren: true,
|
|
}
|
|
|
|
c.Flags().StringP("string-flag", "s", "", "string flag")
|
|
c.Flags().BoolP("bool-flag", "b", false, "bool flag")
|
|
|
|
buf := new(bytes.Buffer)
|
|
assertNoErr(t, c.GenBashCompletion(buf))
|
|
output := buf.String()
|
|
|
|
if !strings.Contains(output, "bool-flag") {
|
|
t.Errorf("Expected completion to include bool-flag flag: Got %v", output)
|
|
}
|
|
|
|
// check that local nonpersistent flag are not set since we have TraverseChildren set to true
|
|
checkOmit(t, output, `local_nonpersistent_flags+=("--string-flag")`)
|
|
checkOmit(t, output, `local_nonpersistent_flags+=("--string-flag=")`)
|
|
checkOmit(t, output, `local_nonpersistent_flags+=("-s")`)
|
|
checkOmit(t, output, `local_nonpersistent_flags+=("--bool-flag")`)
|
|
checkOmit(t, output, `local_nonpersistent_flags+=("-b")`)
|
|
}
|
|
|
|
func TestBashCompletionNoActiveHelp(t *testing.T) {
|
|
c := &Command{
|
|
Use: "c",
|
|
// Set the legacy BashCompletionFunction to force the
|
|
// use of bash completion V1
|
|
BashCompletionFunction: bashCompletionFunc,
|
|
Run: emptyRun,
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
assertNoErr(t, c.GenBashCompletion(buf))
|
|
output := buf.String()
|
|
|
|
// check that active help is being disabled
|
|
activeHelpVar := activeHelpEnvVar(c.Name())
|
|
check(t, output, fmt.Sprintf("%s=0", activeHelpVar))
|
|
}
|
|
|
|
func TestBashCompletionV1WhenBashCompletionFunction(t *testing.T) {
|
|
c := &Command{
|
|
Use: "c",
|
|
// Include the legacy BashCompletionFunction field
|
|
// and check that we are generating bash completion V1
|
|
BashCompletionFunction: bashCompletionFunc,
|
|
Run: emptyRun,
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
assertNoErr(t, c.GenBashCompletion(buf))
|
|
output := buf.String()
|
|
|
|
// Check the generated script is the V1 version
|
|
checkOmit(t, output, "# bash completion V2 for")
|
|
check(t, output, "# bash completion for")
|
|
}
|
|
|
|
func TestBashCompletionV2WhenNoBashCompletionFunction(t *testing.T) {
|
|
c := &Command{
|
|
Use: "c",
|
|
// Do NOT include the legacy BashCompletionFunction
|
|
// and check that we are generating bash completion V2
|
|
Run: emptyRun,
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
assertNoErr(t, c.GenBashCompletion(buf))
|
|
output := buf.String()
|
|
|
|
// Check the generated script is the V2 version
|
|
check(t, output, "# bash completion V2 for")
|
|
}
|