git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/cobra/bash_completions_test.go (about)

     1  // Copyright 2013-2022 The Cobra Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package cobra
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"os"
    21  	"os/exec"
    22  	"regexp"
    23  	"strings"
    24  	"testing"
    25  )
    26  
    27  func checkOmit(t *testing.T, found, unexpected string) {
    28  	if strings.Contains(found, unexpected) {
    29  		t.Errorf("Got: %q\nBut should not have!\n", unexpected)
    30  	}
    31  }
    32  
    33  func check(t *testing.T, found, expected string) {
    34  	if !strings.Contains(found, expected) {
    35  		t.Errorf("Expecting to contain: \n %q\nGot:\n %q\n", expected, found)
    36  	}
    37  }
    38  
    39  func checkNumOccurrences(t *testing.T, found, expected string, expectedOccurrences int) {
    40  	numOccurrences := strings.Count(found, expected)
    41  	if numOccurrences != expectedOccurrences {
    42  		t.Errorf("Expecting to contain %d occurrences of: \n %q\nGot %d:\n %q\n", expectedOccurrences, expected, numOccurrences, found)
    43  	}
    44  }
    45  
    46  func checkRegex(t *testing.T, found, pattern string) {
    47  	matched, err := regexp.MatchString(pattern, found)
    48  	if err != nil {
    49  		t.Errorf("Error thrown performing MatchString: \n %s\n", err)
    50  	}
    51  	if !matched {
    52  		t.Errorf("Expecting to match: \n %q\nGot:\n %q\n", pattern, found)
    53  	}
    54  }
    55  
    56  func runShellCheck(s string) error {
    57  	cmd := exec.Command("shellcheck", "-s", "bash", "-", "-e",
    58  		"SC2034", // PREFIX appears unused. Verify it or export it.
    59  	)
    60  	cmd.Stderr = os.Stderr
    61  	cmd.Stdout = os.Stdout
    62  
    63  	stdin, err := cmd.StdinPipe()
    64  	if err != nil {
    65  		return err
    66  	}
    67  	go func() {
    68  		_, err := stdin.Write([]byte(s))
    69  		CheckErr(err)
    70  
    71  		stdin.Close()
    72  	}()
    73  
    74  	return cmd.Run()
    75  }
    76  
    77  // World worst custom function, just keep telling you to enter hello!
    78  const bashCompletionFunc = `__root_custom_func() {
    79  	COMPREPLY=( "hello" )
    80  }
    81  `
    82  
    83  func TestBashCompletions(t *testing.T) {
    84  	rootCmd := &Command{
    85  		Use:                    "root",
    86  		ArgAliases:             []string{"pods", "nodes", "services", "replicationcontrollers", "po", "no", "svc", "rc"},
    87  		ValidArgs:              []string{"pod", "node", "service", "replicationcontroller"},
    88  		BashCompletionFunction: bashCompletionFunc,
    89  		Run:                    emptyRun,
    90  	}
    91  	rootCmd.Flags().IntP("introot", "i", -1, "help message for flag introot")
    92  	assertNoErr(t, rootCmd.MarkFlagRequired("introot"))
    93  
    94  	// Filename.
    95  	rootCmd.Flags().String("filename", "", "Enter a filename")
    96  	assertNoErr(t, rootCmd.MarkFlagFilename("filename", "json", "yaml", "yml"))
    97  
    98  	// Persistent filename.
    99  	rootCmd.PersistentFlags().String("persistent-filename", "", "Enter a filename")
   100  	assertNoErr(t, rootCmd.MarkPersistentFlagFilename("persistent-filename"))
   101  	assertNoErr(t, rootCmd.MarkPersistentFlagRequired("persistent-filename"))
   102  
   103  	// Filename extensions.
   104  	rootCmd.Flags().String("filename-ext", "", "Enter a filename (extension limited)")
   105  	assertNoErr(t, rootCmd.MarkFlagFilename("filename-ext"))
   106  	rootCmd.Flags().String("custom", "", "Enter a filename (extension limited)")
   107  	assertNoErr(t, rootCmd.MarkFlagCustom("custom", "__complete_custom"))
   108  
   109  	// Subdirectories in a given directory.
   110  	rootCmd.Flags().String("theme", "", "theme to use (located in /themes/THEMENAME/)")
   111  	assertNoErr(t, rootCmd.Flags().SetAnnotation("theme", BashCompSubdirsInDir, []string{"themes"}))
   112  
   113  	// For two word flags check
   114  	rootCmd.Flags().StringP("two", "t", "", "this is two word flags")
   115  	rootCmd.Flags().BoolP("two-w-default", "T", false, "this is not two word flags")
   116  
   117  	echoCmd := &Command{
   118  		Use:     "echo [string to echo]",
   119  		Aliases: []string{"say"},
   120  		Short:   "Echo anything to the screen",
   121  		Long:    "an utterly useless command for testing.",
   122  		Example: "Just run cobra-test echo",
   123  		Run:     emptyRun,
   124  	}
   125  
   126  	echoCmd.Flags().String("filename", "", "Enter a filename")
   127  	assertNoErr(t, echoCmd.MarkFlagFilename("filename", "json", "yaml", "yml"))
   128  	echoCmd.Flags().String("config", "", "config to use (located in /config/PROFILE/)")
   129  	assertNoErr(t, echoCmd.Flags().SetAnnotation("config", BashCompSubdirsInDir, []string{"config"}))
   130  
   131  	printCmd := &Command{
   132  		Use:   "print [string to print]",
   133  		Args:  MinimumNArgs(1),
   134  		Short: "Print anything to the screen",
   135  		Long:  "an absolutely utterly useless command for testing.",
   136  		Run:   emptyRun,
   137  	}
   138  
   139  	deprecatedCmd := &Command{
   140  		Use:        "deprecated [can't do anything here]",
   141  		Args:       NoArgs,
   142  		Short:      "A command which is deprecated",
   143  		Long:       "an absolutely utterly useless command for testing deprecation!.",
   144  		Deprecated: "Please use echo instead",
   145  		Run:        emptyRun,
   146  	}
   147  
   148  	colonCmd := &Command{
   149  		Use: "cmd:colon",
   150  		Run: emptyRun,
   151  	}
   152  
   153  	timesCmd := &Command{
   154  		Use:        "times [# times] [string to echo]",
   155  		SuggestFor: []string{"counts"},
   156  		Args:       OnlyValidArgs,
   157  		ValidArgs:  []string{"one", "two", "three", "four"},
   158  		Short:      "Echo anything to the screen more times",
   159  		Long:       "a slightly useless command for testing.",
   160  		Run:        emptyRun,
   161  	}
   162  
   163  	echoCmd.AddCommand(timesCmd)
   164  	rootCmd.AddCommand(echoCmd, printCmd, deprecatedCmd, colonCmd)
   165  
   166  	buf := new(bytes.Buffer)
   167  	assertNoErr(t, rootCmd.GenBashCompletion(buf))
   168  	output := buf.String()
   169  
   170  	check(t, output, "_root")
   171  	check(t, output, "_root_echo")
   172  	check(t, output, "_root_echo_times")
   173  	check(t, output, "_root_print")
   174  	check(t, output, "_root_cmd__colon")
   175  
   176  	// check for required flags
   177  	check(t, output, `must_have_one_flag+=("--introot=")`)
   178  	check(t, output, `must_have_one_flag+=("--persistent-filename=")`)
   179  	// check for custom completion function with both qualified and unqualified name
   180  	checkNumOccurrences(t, output, `__custom_func`, 2)      // 1. check existence, 2. invoke
   181  	checkNumOccurrences(t, output, `__root_custom_func`, 3) // 1. check existence, 2. invoke, 3. actual definition
   182  	// check for custom completion function body
   183  	check(t, output, `COMPREPLY=( "hello" )`)
   184  	// check for required nouns
   185  	check(t, output, `must_have_one_noun+=("pod")`)
   186  	// check for noun aliases
   187  	check(t, output, `noun_aliases+=("pods")`)
   188  	check(t, output, `noun_aliases+=("rc")`)
   189  	checkOmit(t, output, `must_have_one_noun+=("pods")`)
   190  	// check for filename extension flags
   191  	check(t, output, `flags_completion+=("_filedir")`)
   192  	// check for filename extension flags
   193  	check(t, output, `must_have_one_noun+=("three")`)
   194  	// check for filename extension flags
   195  	check(t, output, fmt.Sprintf(`flags_completion+=("__%s_handle_filename_extension_flag json|yaml|yml")`, rootCmd.Name()))
   196  	// check for filename extension flags in a subcommand
   197  	checkRegex(t, output, fmt.Sprintf(`_root_echo\(\)\n{[^}]*flags_completion\+=\("__%s_handle_filename_extension_flag json\|yaml\|yml"\)`, rootCmd.Name()))
   198  	// check for custom flags
   199  	check(t, output, `flags_completion+=("__complete_custom")`)
   200  	// check for subdirs_in_dir flags
   201  	check(t, output, fmt.Sprintf(`flags_completion+=("__%s_handle_subdirs_in_dir_flag themes")`, rootCmd.Name()))
   202  	// check for subdirs_in_dir flags in a subcommand
   203  	checkRegex(t, output, fmt.Sprintf(`_root_echo\(\)\n{[^}]*flags_completion\+=\("__%s_handle_subdirs_in_dir_flag config"\)`, rootCmd.Name()))
   204  
   205  	// check two word flags
   206  	check(t, output, `two_word_flags+=("--two")`)
   207  	check(t, output, `two_word_flags+=("-t")`)
   208  	checkOmit(t, output, `two_word_flags+=("--two-w-default")`)
   209  	checkOmit(t, output, `two_word_flags+=("-T")`)
   210  
   211  	// check local nonpersistent flag
   212  	check(t, output, `local_nonpersistent_flags+=("--two")`)
   213  	check(t, output, `local_nonpersistent_flags+=("--two=")`)
   214  	check(t, output, `local_nonpersistent_flags+=("-t")`)
   215  	check(t, output, `local_nonpersistent_flags+=("--two-w-default")`)
   216  	check(t, output, `local_nonpersistent_flags+=("-T")`)
   217  
   218  	checkOmit(t, output, deprecatedCmd.Name())
   219  
   220  	// If available, run shellcheck against the script.
   221  	if err := exec.Command("which", "shellcheck").Run(); err != nil {
   222  		return
   223  	}
   224  	if err := runShellCheck(output); err != nil {
   225  		t.Fatalf("shellcheck failed: %v", err)
   226  	}
   227  }
   228  
   229  func TestBashCompletionHiddenFlag(t *testing.T) {
   230  	c := &Command{Use: "c", Run: emptyRun}
   231  
   232  	const flagName = "hiddenFlag"
   233  	c.Flags().Bool(flagName, false, "")
   234  	assertNoErr(t, c.Flags().MarkHidden(flagName))
   235  
   236  	buf := new(bytes.Buffer)
   237  	assertNoErr(t, c.GenBashCompletion(buf))
   238  	output := buf.String()
   239  
   240  	if strings.Contains(output, flagName) {
   241  		t.Errorf("Expected completion to not include %q flag: Got %v", flagName, output)
   242  	}
   243  }
   244  
   245  func TestBashCompletionDeprecatedFlag(t *testing.T) {
   246  	c := &Command{Use: "c", Run: emptyRun}
   247  
   248  	const flagName = "deprecated-flag"
   249  	c.Flags().Bool(flagName, false, "")
   250  	assertNoErr(t, c.Flags().MarkDeprecated(flagName, "use --not-deprecated instead"))
   251  
   252  	buf := new(bytes.Buffer)
   253  	assertNoErr(t, c.GenBashCompletion(buf))
   254  	output := buf.String()
   255  
   256  	if strings.Contains(output, flagName) {
   257  		t.Errorf("expected completion to not include %q flag: Got %v", flagName, output)
   258  	}
   259  }
   260  
   261  func TestBashCompletionTraverseChildren(t *testing.T) {
   262  	c := &Command{Use: "c", Run: emptyRun, TraverseChildren: true}
   263  
   264  	c.Flags().StringP("string-flag", "s", "", "string flag")
   265  	c.Flags().BoolP("bool-flag", "b", false, "bool flag")
   266  
   267  	buf := new(bytes.Buffer)
   268  	assertNoErr(t, c.GenBashCompletion(buf))
   269  	output := buf.String()
   270  
   271  	// check that local nonpersistent flag are not set since we have TraverseChildren set to true
   272  	checkOmit(t, output, `local_nonpersistent_flags+=("--string-flag")`)
   273  	checkOmit(t, output, `local_nonpersistent_flags+=("--string-flag=")`)
   274  	checkOmit(t, output, `local_nonpersistent_flags+=("-s")`)
   275  	checkOmit(t, output, `local_nonpersistent_flags+=("--bool-flag")`)
   276  	checkOmit(t, output, `local_nonpersistent_flags+=("-b")`)
   277  }
   278  
   279  func TestBashCompletionNoActiveHelp(t *testing.T) {
   280  	c := &Command{Use: "c", Run: emptyRun}
   281  
   282  	buf := new(bytes.Buffer)
   283  	assertNoErr(t, c.GenBashCompletion(buf))
   284  	output := buf.String()
   285  
   286  	// check that active help is being disabled
   287  	activeHelpVar := activeHelpEnvVar(c.Name())
   288  	check(t, output, fmt.Sprintf("%s=0", activeHelpVar))
   289  }