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