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