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 checkRegex(t *testing.T, found, pattern string) {
26	matched, err := regexp.MatchString(pattern, found)
27	if err != nil {
28		t.Errorf("Error thrown performing MatchString: \n %s\n", err)
29	}
30	if !matched {
31		t.Errorf("Expecting to match: \n %q\nGot:\n %q\n", pattern, found)
32	}
33}
34
35func runShellCheck(s string) error {
36	excluded := []string{
37		"SC2034", // PREFIX appears unused. Verify it or export it.
38	}
39	cmd := exec.Command("shellcheck", "-s", "bash", "-", "-e", strings.Join(excluded, ","))
40	cmd.Stderr = os.Stderr
41	cmd.Stdout = os.Stdout
42
43	stdin, err := cmd.StdinPipe()
44	if err != nil {
45		return err
46	}
47	go func() {
48		stdin.Write([]byte(s))
49		stdin.Close()
50	}()
51
52	return cmd.Run()
53}
54
55// World worst custom function, just keep telling you to enter hello!
56const bashCompletionFunc = `__custom_func() {
57	COMPREPLY=( "hello" )
58}
59`
60
61func TestBashCompletions(t *testing.T) {
62	rootCmd := &Command{
63		Use:                    "root",
64		ArgAliases:             []string{"pods", "nodes", "services", "replicationcontrollers", "po", "no", "svc", "rc"},
65		ValidArgs:              []string{"pod", "node", "service", "replicationcontroller"},
66		BashCompletionFunction: bashCompletionFunc,
67		Run: emptyRun,
68	}
69	rootCmd.Flags().IntP("introot", "i", -1, "help message for flag introot")
70	rootCmd.MarkFlagRequired("introot")
71
72	// Filename.
73	rootCmd.Flags().String("filename", "", "Enter a filename")
74	rootCmd.MarkFlagFilename("filename", "json", "yaml", "yml")
75
76	// Persistent filename.
77	rootCmd.PersistentFlags().String("persistent-filename", "", "Enter a filename")
78	rootCmd.MarkPersistentFlagFilename("persistent-filename")
79	rootCmd.MarkPersistentFlagRequired("persistent-filename")
80
81	// Filename extensions.
82	rootCmd.Flags().String("filename-ext", "", "Enter a filename (extension limited)")
83	rootCmd.MarkFlagFilename("filename-ext")
84	rootCmd.Flags().String("custom", "", "Enter a filename (extension limited)")
85	rootCmd.MarkFlagCustom("custom", "__complete_custom")
86
87	// Subdirectories in a given directory.
88	rootCmd.Flags().String("theme", "", "theme to use (located in /themes/THEMENAME/)")
89	rootCmd.Flags().SetAnnotation("theme", BashCompSubdirsInDir, []string{"themes"})
90
91	echoCmd := &Command{
92		Use:     "echo [string to echo]",
93		Aliases: []string{"say"},
94		Short:   "Echo anything to the screen",
95		Long:    "an utterly useless command for testing.",
96		Example: "Just run cobra-test echo",
97		Run:     emptyRun,
98	}
99
100	echoCmd.Flags().String("filename", "", "Enter a filename")
101	echoCmd.MarkFlagFilename("filename", "json", "yaml", "yml")
102	echoCmd.Flags().String("config", "", "config to use (located in /config/PROFILE/)")
103	echoCmd.Flags().SetAnnotation("config", BashCompSubdirsInDir, []string{"config"})
104
105	printCmd := &Command{
106		Use:   "print [string to print]",
107		Args:  MinimumNArgs(1),
108		Short: "Print anything to the screen",
109		Long:  "an absolutely utterly useless command for testing.",
110		Run:   emptyRun,
111	}
112
113	deprecatedCmd := &Command{
114		Use:        "deprecated [can't do anything here]",
115		Args:       NoArgs,
116		Short:      "A command which is deprecated",
117		Long:       "an absolutely utterly useless command for testing deprecation!.",
118		Deprecated: "Please use echo instead",
119		Run:        emptyRun,
120	}
121
122	colonCmd := &Command{
123		Use: "cmd:colon",
124		Run: emptyRun,
125	}
126
127	timesCmd := &Command{
128		Use:        "times [# times] [string to echo]",
129		SuggestFor: []string{"counts"},
130		Args:       OnlyValidArgs,
131		ValidArgs:  []string{"one", "two", "three", "four"},
132		Short:      "Echo anything to the screen more times",
133		Long:       "a slightly useless command for testing.",
134		Run:        emptyRun,
135	}
136
137	echoCmd.AddCommand(timesCmd)
138	rootCmd.AddCommand(echoCmd, printCmd, deprecatedCmd, colonCmd)
139
140	buf := new(bytes.Buffer)
141	rootCmd.GenBashCompletion(buf)
142	output := buf.String()
143
144	check(t, output, "_root")
145	check(t, output, "_root_echo")
146	check(t, output, "_root_echo_times")
147	check(t, output, "_root_print")
148	check(t, output, "_root_cmd__colon")
149
150	// check for required flags
151	check(t, output, `must_have_one_flag+=("--introot=")`)
152	check(t, output, `must_have_one_flag+=("--persistent-filename=")`)
153	// check for custom completion function
154	check(t, output, `COMPREPLY=( "hello" )`)
155	// check for required nouns
156	check(t, output, `must_have_one_noun+=("pod")`)
157	// check for noun aliases
158	check(t, output, `noun_aliases+=("pods")`)
159	check(t, output, `noun_aliases+=("rc")`)
160	checkOmit(t, output, `must_have_one_noun+=("pods")`)
161	// check for filename extension flags
162	check(t, output, `flags_completion+=("_filedir")`)
163	// check for filename extension flags
164	check(t, output, `must_have_one_noun+=("three")`)
165	// check for filename extension flags
166	check(t, output, fmt.Sprintf(`flags_completion+=("__%s_handle_filename_extension_flag json|yaml|yml")`, rootCmd.Name()))
167	// check for filename extension flags in a subcommand
168	checkRegex(t, output, fmt.Sprintf(`_root_echo\(\)\n{[^}]*flags_completion\+=\("__%s_handle_filename_extension_flag json\|yaml\|yml"\)`, rootCmd.Name()))
169	// check for custom flags
170	check(t, output, `flags_completion+=("__complete_custom")`)
171	// check for subdirs_in_dir flags
172	check(t, output, fmt.Sprintf(`flags_completion+=("__%s_handle_subdirs_in_dir_flag themes")`, rootCmd.Name()))
173	// check for subdirs_in_dir flags in a subcommand
174	checkRegex(t, output, fmt.Sprintf(`_root_echo\(\)\n{[^}]*flags_completion\+=\("__%s_handle_subdirs_in_dir_flag config"\)`, rootCmd.Name()))
175
176	checkOmit(t, output, deprecatedCmd.Name())
177
178	// If available, run shellcheck against the script.
179	if err := exec.Command("which", "shellcheck").Run(); err != nil {
180		return
181	}
182	if err := runShellCheck(output); err != nil {
183		t.Fatalf("shellcheck failed: %v", err)
184	}
185}
186
187func TestBashCompletionHiddenFlag(t *testing.T) {
188	c := &Command{Use: "c", Run: emptyRun}
189
190	const flagName = "hiddenFlag"
191	c.Flags().Bool(flagName, false, "")
192	c.Flags().MarkHidden(flagName)
193
194	buf := new(bytes.Buffer)
195	c.GenBashCompletion(buf)
196	output := buf.String()
197
198	if strings.Contains(output, flagName) {
199		t.Errorf("Expected completion to not include %q flag: Got %v", flagName, output)
200	}
201}
202
203func TestBashCompletionDeprecatedFlag(t *testing.T) {
204	c := &Command{Use: "c", Run: emptyRun}
205
206	const flagName = "deprecated-flag"
207	c.Flags().Bool(flagName, false, "")
208	c.Flags().MarkDeprecated(flagName, "use --not-deprecated instead")
209
210	buf := new(bytes.Buffer)
211	c.GenBashCompletion(buf)
212	output := buf.String()
213
214	if strings.Contains(output, flagName) {
215		t.Errorf("expected completion to not include %q flag: Got %v", flagName, output)
216	}
217}
218