package cobra import ( "bytes" "regexp" "strings" "testing" ) func TestGenZshCompletion(t *testing.T) { var debug bool var option string tcs := []struct { name string root *Command expectedExpressions []string invocationArgs []string skip string }{ { name: "simple command", root: func() *Command { r := &Command{ Use: "mycommand", Long: "My Command long description", Run: emptyRun, } r.Flags().BoolVar(&debug, "debug", debug, "description") return r }(), expectedExpressions: []string{ `(?s)function _mycommand {\s+_arguments \\\s+'--debug\[description\]'.*--help.*}`, "#compdef _mycommand mycommand", }, }, { name: "flags with both long and short flags", root: func() *Command { r := &Command{ Use: "testcmd", Long: "long description", Run: emptyRun, } r.Flags().BoolVarP(&debug, "debug", "d", debug, "debug description") return r }(), expectedExpressions: []string{ `'\(-d --debug\)'{-d,--debug}'\[debug description\]'`, }, }, { name: "command with subcommands and flags with values", root: func() *Command { r := &Command{ Use: "rootcmd", Long: "Long rootcmd description", } d := &Command{ Use: "subcmd1", Short: "Subcmd1 short descrition", Run: emptyRun, } e := &Command{ Use: "subcmd2", Long: "Subcmd2 short description", Run: emptyRun, } r.PersistentFlags().BoolVar(&debug, "debug", debug, "description") d.Flags().StringVarP(&option, "option", "o", option, "option description") r.AddCommand(d, e) return r }(), expectedExpressions: []string{ `commands=\(\n\s+"help:.*\n\s+"subcmd1:.*\n\s+"subcmd2:.*\n\s+\)`, `_arguments \\\n.*'--debug\[description]'`, `_arguments -C \\\n.*'--debug\[description]'`, `function _rootcmd_subcmd1 {`, `function _rootcmd_subcmd1 {`, `_arguments \\\n.*'\(-o --option\)'{-o,--option}'\[option description]:' \\\n`, }, }, { name: "filename completion with and without globs", root: func() *Command { var file string r := &Command{ Use: "mycmd", Short: "my command short description", Run: emptyRun, } r.Flags().StringVarP(&file, "config", "c", file, "config file") r.MarkFlagFilename("config") r.Flags().String("output", "", "output file") r.MarkFlagFilename("output", "*.log", "*.txt") return r }(), expectedExpressions: []string{ `\n +'\(-c --config\)'{-c,--config}'\[config file]:filename:_files'`, `:_files -g "\*.log" -g "\*.txt"`, }, }, { name: "repeated variables both with and without value", root: func() *Command { r := genTestCommand("mycmd", true) _ = r.Flags().BoolSliceP("debug", "d", []bool{}, "debug usage") _ = r.Flags().StringArray("option", []string{}, "options") return r }(), expectedExpressions: []string{ `'\*--option\[options]`, `'\(\*-d \*--debug\)'{\\\*-d,\\\*--debug}`, }, }, { name: "generated flags --help and --version should be created even when not executing root cmd", root: func() *Command { r := &Command{ Use: "mycmd", Short: "mycmd short description", Version: "myversion", } s := genTestCommand("sub1", true) r.AddCommand(s) return s }(), expectedExpressions: []string{ "--version", "--help", }, invocationArgs: []string{ "sub1", }, skip: "--version and --help are currently not generated when not running on root command", }, { name: "zsh generation should run on root commannd", root: func() *Command { r := genTestCommand("root", false) s := genTestCommand("sub1", true) r.AddCommand(s) return s }(), expectedExpressions: []string{ "function _root {", }, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { if tc.skip != "" { t.Skip(tc.skip) } tc.root.Root().SetArgs(tc.invocationArgs) tc.root.Execute() buf := new(bytes.Buffer) if err := tc.root.GenZshCompletion(buf); err != nil { t.Error(err) } output := buf.Bytes() for _, expr := range tc.expectedExpressions { rgx, err := regexp.Compile(expr) if err != nil { t.Errorf("error compiling expression (%s): %v", expr, err) } if !rgx.Match(output) { t.Errorf("expeced completion (%s) to match '%s'", buf.String(), expr) } } }) } } func TestGenZshCompletionHidden(t *testing.T) { tcs := []struct { name string root *Command expectedExpressions []string }{ { name: "hidden commmands", root: func() *Command { r := &Command{ Use: "main", Short: "main short description", } s1 := &Command{ Use: "sub1", Hidden: true, Run: emptyRun, } s2 := &Command{ Use: "sub2", Short: "short sub2 description", Run: emptyRun, } r.AddCommand(s1, s2) return r }(), expectedExpressions: []string{ "sub1", }, }, { name: "hidden flags", root: func() *Command { var hidden string r := &Command{ Use: "root", Short: "root short description", Run: emptyRun, } r.Flags().StringVarP(&hidden, "hidden", "H", hidden, "hidden usage") if err := r.Flags().MarkHidden("hidden"); err != nil { t.Errorf("Error setting flag hidden: %v\n", err) } return r }(), expectedExpressions: []string{ "--hidden", }, }, } for _, tc := range tcs { t.Run(tc.name, func(t *testing.T) { tc.root.Execute() buf := new(bytes.Buffer) if err := tc.root.GenZshCompletion(buf); err != nil { t.Error(err) } output := buf.String() for _, expr := range tc.expectedExpressions { if strings.Contains(output, expr) { t.Errorf("Expected completion (%s) not to contain '%s' but it does", output, expr) } } }) } } func BenchmarkMediumSizeConstruct(b *testing.B) { root := constructLargeCommandHeirarchy() // if err := root.GenZshCompletionFile("_mycmd"); err != nil { // b.Error(err) // } for i := 0; i < b.N; i++ { buf := new(bytes.Buffer) err := root.GenZshCompletion(buf) if err != nil { b.Error(err) } } } func TestExtractFlags(t *testing.T) { var debug, cmdc, cmdd bool c := &Command{ Use: "cmdC", Long: "Command C", } c.PersistentFlags().BoolVarP(&debug, "debug", "d", debug, "debug mode") c.Flags().BoolVar(&cmdc, "cmd-c", cmdc, "Command C") d := &Command{ Use: "CmdD", Long: "Command D", } d.Flags().BoolVar(&cmdd, "cmd-d", cmdd, "Command D") c.AddCommand(d) resC := extractFlags(c) resD := extractFlags(d) if len(resC) != 2 { t.Errorf("expected Command C to return 2 flags, got %d", len(resC)) } if len(resD) != 2 { t.Errorf("expected Command D to return 2 flags, got %d", len(resD)) } } func constructLargeCommandHeirarchy() *Command { var config, st1, st2 string var long, debug bool var in1, in2 int var verbose []bool r := genTestCommand("mycmd", false) r.PersistentFlags().StringVarP(&config, "config", "c", config, "config usage") if err := r.MarkPersistentFlagFilename("config", "*"); err != nil { panic(err) } s1 := genTestCommand("sub1", true) s1.Flags().BoolVar(&long, "long", long, "long descriptin") s1.Flags().BoolSliceVar(&verbose, "verbose", verbose, "verbose description") s1.Flags().StringArray("option", []string{}, "various options") s2 := genTestCommand("sub2", true) s2.PersistentFlags().BoolVar(&debug, "debug", debug, "debug description") s3 := genTestCommand("sub3", true) s3.Hidden = true s1_1 := genTestCommand("sub1sub1", true) s1_1.Flags().StringVar(&st1, "st1", st1, "st1 description") s1_1.Flags().StringVar(&st2, "st2", st2, "st2 description") s1_2 := genTestCommand("sub1sub2", true) s1_3 := genTestCommand("sub1sub3", true) s1_3.Flags().IntVar(&in1, "int1", in1, "int1 descriptionn") s1_3.Flags().IntVar(&in2, "int2", in2, "int2 descriptionn") s1_3.Flags().StringArrayP("option", "O", []string{}, "more options") s2_1 := genTestCommand("sub2sub1", true) s2_2 := genTestCommand("sub2sub2", true) s2_3 := genTestCommand("sub2sub3", true) s2_4 := genTestCommand("sub2sub4", true) s2_5 := genTestCommand("sub2sub5", true) s1.AddCommand(s1_1, s1_2, s1_3) s2.AddCommand(s2_1, s2_2, s2_3, s2_4, s2_5) r.AddCommand(s1, s2, s3) r.Execute() return r } func genTestCommand(name string, withRun bool) *Command { r := &Command{ Use: name, Short: name + " short description", Long: "Long description for " + name, } if withRun { r.Run = emptyRun } return r }