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