1package cobra
2
3import (
4	"bytes"
5	"regexp"
6	"strings"
7	"testing"
8)
9
10func TestGenZshCompletion(t *testing.T) {
11	var debug bool
12	var option string
13
14	tcs := []struct {
15		name                string
16		root                *Command
17		expectedExpressions []string
18		invocationArgs      []string
19		skip                string
20	}{
21		{
22			name: "simple command",
23			root: func() *Command {
24				r := &Command{
25					Use:  "mycommand",
26					Long: "My Command long description",
27					Run:  emptyRun,
28				}
29				r.Flags().BoolVar(&debug, "debug", debug, "description")
30				return r
31			}(),
32			expectedExpressions: []string{
33				`(?s)function _mycommand {\s+_arguments \\\s+'--debug\[description\]'.*--help.*}`,
34				"#compdef _mycommand mycommand",
35			},
36		},
37		{
38			name: "flags with both long and short flags",
39			root: func() *Command {
40				r := &Command{
41					Use:  "testcmd",
42					Long: "long description",
43					Run:  emptyRun,
44				}
45				r.Flags().BoolVarP(&debug, "debug", "d", debug, "debug description")
46				return r
47			}(),
48			expectedExpressions: []string{
49				`'\(-d --debug\)'{-d,--debug}'\[debug description\]'`,
50			},
51		},
52		{
53			name: "command with subcommands and flags with values",
54			root: func() *Command {
55				r := &Command{
56					Use:  "rootcmd",
57					Long: "Long rootcmd description",
58				}
59				d := &Command{
60					Use:   "subcmd1",
61					Short: "Subcmd1 short description",
62					Run:   emptyRun,
63				}
64				e := &Command{
65					Use:  "subcmd2",
66					Long: "Subcmd2 short description",
67					Run:  emptyRun,
68				}
69				r.PersistentFlags().BoolVar(&debug, "debug", debug, "description")
70				d.Flags().StringVarP(&option, "option", "o", option, "option description")
71				r.AddCommand(d, e)
72				return r
73			}(),
74			expectedExpressions: []string{
75				`commands=\(\n\s+"help:.*\n\s+"subcmd1:.*\n\s+"subcmd2:.*\n\s+\)`,
76				`_arguments \\\n.*'--debug\[description]'`,
77				`_arguments -C \\\n.*'--debug\[description]'`,
78				`function _rootcmd_subcmd1 {`,
79				`function _rootcmd_subcmd1 {`,
80				`_arguments \\\n.*'\(-o --option\)'{-o,--option}'\[option description]:' \\\n`,
81			},
82		},
83		{
84			name: "filename completion with and without globs",
85			root: func() *Command {
86				var file string
87				r := &Command{
88					Use:   "mycmd",
89					Short: "my command short description",
90					Run:   emptyRun,
91				}
92				r.Flags().StringVarP(&file, "config", "c", file, "config file")
93				r.MarkFlagFilename("config")
94				r.Flags().String("output", "", "output file")
95				r.MarkFlagFilename("output", "*.log", "*.txt")
96				return r
97			}(),
98			expectedExpressions: []string{
99				`\n +'\(-c --config\)'{-c,--config}'\[config file]:filename:_files'`,
100				`:_files -g "\*.log" -g "\*.txt"`,
101			},
102		},
103		{
104			name: "repeated variables both with and without value",
105			root: func() *Command {
106				r := genTestCommand("mycmd", true)
107				_ = r.Flags().BoolSliceP("debug", "d", []bool{}, "debug usage")
108				_ = r.Flags().StringArray("option", []string{}, "options")
109				return r
110			}(),
111			expectedExpressions: []string{
112				`'\*--option\[options]`,
113				`'\(\*-d \*--debug\)'{\\\*-d,\\\*--debug}`,
114			},
115		},
116		{
117			name: "generated flags --help and --version should be created even when not executing root cmd",
118			root: func() *Command {
119				r := &Command{
120					Use:     "mycmd",
121					Short:   "mycmd short description",
122					Version: "myversion",
123				}
124				s := genTestCommand("sub1", true)
125				r.AddCommand(s)
126				return s
127			}(),
128			expectedExpressions: []string{
129				"--version",
130				"--help",
131			},
132			invocationArgs: []string{
133				"sub1",
134			},
135			skip: "--version and --help are currently not generated when not running on root command",
136		},
137		{
138			name: "zsh generation should run on root command",
139			root: func() *Command {
140				r := genTestCommand("root", false)
141				s := genTestCommand("sub1", true)
142				r.AddCommand(s)
143				return s
144			}(),
145			expectedExpressions: []string{
146				"function _root {",
147			},
148		},
149		{
150			name: "flag description with single quote (') shouldn't break quotes in completion file",
151			root: func() *Command {
152				r := genTestCommand("root", true)
153				r.Flags().Bool("private", false, "Don't show public info")
154				return r
155			}(),
156			expectedExpressions: []string{
157				`--private\[Don'\\''t show public info]`,
158			},
159		},
160		{
161			name: "argument completion for file with and without patterns",
162			root: func() *Command {
163				r := genTestCommand("root", true)
164				r.MarkZshCompPositionalArgumentFile(1, "*.log")
165				r.MarkZshCompPositionalArgumentFile(2)
166				return r
167			}(),
168			expectedExpressions: []string{
169				`'1: :_files -g "\*.log"' \\\n\s+'2: :_files`,
170			},
171		},
172		{
173			name: "argument zsh completion for words",
174			root: func() *Command {
175				r := genTestCommand("root", true)
176				r.MarkZshCompPositionalArgumentWords(1, "word1", "word2")
177				return r
178			}(),
179			expectedExpressions: []string{
180				`'1: :\("word1" "word2"\)`,
181			},
182		},
183		{
184			name: "argument completion for words with spaces",
185			root: func() *Command {
186				r := genTestCommand("root", true)
187				r.MarkZshCompPositionalArgumentWords(1, "single", "multiple words")
188				return r
189			}(),
190			expectedExpressions: []string{
191				`'1: :\("single" "multiple words"\)'`,
192			},
193		},
194		{
195			name: "argument completion when command has ValidArgs and no annotation for argument completion",
196			root: func() *Command {
197				r := genTestCommand("root", true)
198				r.ValidArgs = []string{"word1", "word2"}
199				return r
200			}(),
201			expectedExpressions: []string{
202				`'1: :\("word1" "word2"\)'`,
203			},
204		},
205		{
206			name: "argument completion when command has ValidArgs and no annotation for argument at argPosition 1",
207			root: func() *Command {
208				r := genTestCommand("root", true)
209				r.ValidArgs = []string{"word1", "word2"}
210				r.MarkZshCompPositionalArgumentFile(2)
211				return r
212			}(),
213			expectedExpressions: []string{
214				`'1: :\("word1" "word2"\)' \\`,
215			},
216		},
217		{
218			name: "directory completion for flag",
219			root: func() *Command {
220				r := genTestCommand("root", true)
221				r.Flags().String("test", "", "test")
222				r.PersistentFlags().String("ptest", "", "ptest")
223				r.MarkFlagDirname("test")
224				r.MarkPersistentFlagDirname("ptest")
225				return r
226			}(),
227			expectedExpressions: []string{
228				`--test\[test]:filename:_files -g "-\(/\)"`,
229				`--ptest\[ptest]:filename:_files -g "-\(/\)"`,
230			},
231		},
232	}
233
234	for _, tc := range tcs {
235		t.Run(tc.name, func(t *testing.T) {
236			if tc.skip != "" {
237				t.Skip(tc.skip)
238			}
239			tc.root.Root().SetArgs(tc.invocationArgs)
240			tc.root.Execute()
241			buf := new(bytes.Buffer)
242			if err := tc.root.GenZshCompletion(buf); err != nil {
243				t.Error(err)
244			}
245			output := buf.Bytes()
246
247			for _, expr := range tc.expectedExpressions {
248				rgx, err := regexp.Compile(expr)
249				if err != nil {
250					t.Errorf("error compiling expression (%s): %v", expr, err)
251				}
252				if !rgx.Match(output) {
253					t.Errorf("expected completion (%s) to match '%s'", buf.String(), expr)
254				}
255			}
256		})
257	}
258}
259
260func TestGenZshCompletionHidden(t *testing.T) {
261	tcs := []struct {
262		name                string
263		root                *Command
264		expectedExpressions []string
265	}{
266		{
267			name: "hidden commands",
268			root: func() *Command {
269				r := &Command{
270					Use:   "main",
271					Short: "main short description",
272				}
273				s1 := &Command{
274					Use:    "sub1",
275					Hidden: true,
276					Run:    emptyRun,
277				}
278				s2 := &Command{
279					Use:   "sub2",
280					Short: "short sub2 description",
281					Run:   emptyRun,
282				}
283				r.AddCommand(s1, s2)
284
285				return r
286			}(),
287			expectedExpressions: []string{
288				"sub1",
289			},
290		},
291		{
292			name: "hidden flags",
293			root: func() *Command {
294				var hidden string
295				r := &Command{
296					Use:   "root",
297					Short: "root short description",
298					Run:   emptyRun,
299				}
300				r.Flags().StringVarP(&hidden, "hidden", "H", hidden, "hidden usage")
301				if err := r.Flags().MarkHidden("hidden"); err != nil {
302					t.Errorf("Error setting flag hidden: %v\n", err)
303				}
304				return r
305			}(),
306			expectedExpressions: []string{
307				"--hidden",
308			},
309		},
310	}
311
312	for _, tc := range tcs {
313		t.Run(tc.name, func(t *testing.T) {
314			tc.root.Execute()
315			buf := new(bytes.Buffer)
316			if err := tc.root.GenZshCompletion(buf); err != nil {
317				t.Error(err)
318			}
319			output := buf.String()
320
321			for _, expr := range tc.expectedExpressions {
322				if strings.Contains(output, expr) {
323					t.Errorf("Expected completion (%s) not to contain '%s' but it does", output, expr)
324				}
325			}
326		})
327	}
328}
329
330func TestMarkZshCompPositionalArgumentFile(t *testing.T) {
331	t.Run("Doesn't allow overwriting existing positional argument", func(t *testing.T) {
332		c := &Command{}
333		if err := c.MarkZshCompPositionalArgumentFile(1, "*.log"); err != nil {
334			t.Errorf("Received error when we shouldn't have: %v\n", err)
335		}
336		if err := c.MarkZshCompPositionalArgumentFile(1); err == nil {
337			t.Error("Didn't receive an error when trying to overwrite argument position")
338		}
339	})
340
341	t.Run("Refuses to accept argPosition less then 1", func(t *testing.T) {
342		c := &Command{}
343		err := c.MarkZshCompPositionalArgumentFile(0, "*")
344		if err == nil {
345			t.Fatal("Error was not thrown when indicating argument position 0")
346		}
347		if !strings.Contains(err.Error(), "position") {
348			t.Errorf("expected error message '%s' to contain 'position'", err.Error())
349		}
350	})
351}
352
353func TestMarkZshCompPositionalArgumentWords(t *testing.T) {
354	t.Run("Doesn't allow overwriting existing positional argument", func(t *testing.T) {
355		c := &Command{}
356		if err := c.MarkZshCompPositionalArgumentFile(1, "*.log"); err != nil {
357			t.Errorf("Received error when we shouldn't have: %v\n", err)
358		}
359		if err := c.MarkZshCompPositionalArgumentWords(1, "hello"); err == nil {
360			t.Error("Didn't receive an error when trying to overwrite argument position")
361		}
362	})
363
364	t.Run("Doesn't allow calling without words", func(t *testing.T) {
365		c := &Command{}
366		if err := c.MarkZshCompPositionalArgumentWords(0); err == nil {
367			t.Error("Should not allow saving empty word list for annotation")
368		}
369	})
370
371	t.Run("Refuses to accept argPosition less then 1", func(t *testing.T) {
372		c := &Command{}
373		err := c.MarkZshCompPositionalArgumentWords(0, "word")
374		if err == nil {
375			t.Fatal("Should not allow setting argument position less then 1")
376		}
377		if !strings.Contains(err.Error(), "position") {
378			t.Errorf("Expected error '%s' to contain 'position' but didn't", err.Error())
379		}
380	})
381}
382
383func BenchmarkMediumSizeConstruct(b *testing.B) {
384	root := constructLargeCommandHierarchy()
385	// if err := root.GenZshCompletionFile("_mycmd"); err != nil {
386	// 	b.Error(err)
387	// }
388
389	for i := 0; i < b.N; i++ {
390		buf := new(bytes.Buffer)
391		err := root.GenZshCompletion(buf)
392		if err != nil {
393			b.Error(err)
394		}
395	}
396}
397
398func TestExtractFlags(t *testing.T) {
399	var debug, cmdc, cmdd bool
400	c := &Command{
401		Use:  "cmdC",
402		Long: "Command C",
403	}
404	c.PersistentFlags().BoolVarP(&debug, "debug", "d", debug, "debug mode")
405	c.Flags().BoolVar(&cmdc, "cmd-c", cmdc, "Command C")
406	d := &Command{
407		Use:  "CmdD",
408		Long: "Command D",
409	}
410	d.Flags().BoolVar(&cmdd, "cmd-d", cmdd, "Command D")
411	c.AddCommand(d)
412
413	resC := zshCompExtractFlag(c)
414	resD := zshCompExtractFlag(d)
415
416	if len(resC) != 2 {
417		t.Errorf("expected Command C to return 2 flags, got %d", len(resC))
418	}
419	if len(resD) != 2 {
420		t.Errorf("expected Command D to return 2 flags, got %d", len(resD))
421	}
422}
423
424func constructLargeCommandHierarchy() *Command {
425	var config, st1, st2 string
426	var long, debug bool
427	var in1, in2 int
428	var verbose []bool
429
430	r := genTestCommand("mycmd", false)
431	r.PersistentFlags().StringVarP(&config, "config", "c", config, "config usage")
432	if err := r.MarkPersistentFlagFilename("config", "*"); err != nil {
433		panic(err)
434	}
435	s1 := genTestCommand("sub1", true)
436	s1.Flags().BoolVar(&long, "long", long, "long description")
437	s1.Flags().BoolSliceVar(&verbose, "verbose", verbose, "verbose description")
438	s1.Flags().StringArray("option", []string{}, "various options")
439	s2 := genTestCommand("sub2", true)
440	s2.PersistentFlags().BoolVar(&debug, "debug", debug, "debug description")
441	s3 := genTestCommand("sub3", true)
442	s3.Hidden = true
443	s1_1 := genTestCommand("sub1sub1", true)
444	s1_1.Flags().StringVar(&st1, "st1", st1, "st1 description")
445	s1_1.Flags().StringVar(&st2, "st2", st2, "st2 description")
446	s1_2 := genTestCommand("sub1sub2", true)
447	s1_3 := genTestCommand("sub1sub3", true)
448	s1_3.Flags().IntVar(&in1, "int1", in1, "int1 description")
449	s1_3.Flags().IntVar(&in2, "int2", in2, "int2 description")
450	s1_3.Flags().StringArrayP("option", "O", []string{}, "more options")
451	s2_1 := genTestCommand("sub2sub1", true)
452	s2_2 := genTestCommand("sub2sub2", true)
453	s2_3 := genTestCommand("sub2sub3", true)
454	s2_4 := genTestCommand("sub2sub4", true)
455	s2_5 := genTestCommand("sub2sub5", true)
456
457	s1.AddCommand(s1_1, s1_2, s1_3)
458	s2.AddCommand(s2_1, s2_2, s2_3, s2_4, s2_5)
459	r.AddCommand(s1, s2, s3)
460	r.Execute()
461	return r
462}
463
464func genTestCommand(name string, withRun bool) *Command {
465	r := &Command{
466		Use:   name,
467		Short: name + " short description",
468		Long:  "Long description for " + name,
469	}
470	if withRun {
471		r.Run = emptyRun
472	}
473
474	return r
475}
476