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 }