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