Tests for CLI parsing
This commit is contained in:
parent
04022ed363
commit
c4ab964d09
@ -69,6 +69,12 @@ type Option struct {
|
|||||||
seen_option string
|
seen_option string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *Option) reset() {
|
||||||
|
self.values_from_cmdline = self.values_from_cmdline[:0]
|
||||||
|
self.parsed_values_from_cmdline = self.parsed_values_from_cmdline[:0]
|
||||||
|
self.seen_option = ""
|
||||||
|
}
|
||||||
|
|
||||||
func (self *Option) needs_argument() bool {
|
func (self *Option) needs_argument() bool {
|
||||||
return self.OptionType != BoolOption && self.OptionType != CountOption
|
return self.OptionType != BoolOption && self.OptionType != CountOption
|
||||||
}
|
}
|
||||||
@ -129,7 +135,7 @@ func (self *Option) parse_value(val string) (any, error) {
|
|||||||
return nil, &ParseError{Option: self, Message: fmt.Sprintf(
|
return nil, &ParseError{Option: self, Message: fmt.Sprintf(
|
||||||
":yellow:`%s` is not a valid number for :bold:`%s`. Only integers in decimal, hexadecimal, binary or octal notation are accepted.", val, self.seen_option)}
|
":yellow:`%s` is not a valid number for :bold:`%s`. Only integers in decimal, hexadecimal, binary or octal notation are accepted.", val, self.seen_option)}
|
||||||
}
|
}
|
||||||
return pval, nil
|
return int(pval), nil
|
||||||
case FloatOption:
|
case FloatOption:
|
||||||
pval, err := strconv.ParseFloat(val, 64)
|
pval, err := strconv.ParseFloat(val, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -286,7 +292,6 @@ type Command struct { // {{{
|
|||||||
Run func(cmd *Command, args []string) (int, error)
|
Run func(cmd *Command, args []string) (int, error)
|
||||||
|
|
||||||
option_map map[string]*Option
|
option_map map[string]*Option
|
||||||
exe_name string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Command) Clone(parent *Command) *Command {
|
func (self *Command) Clone(parent *Command) *Command {
|
||||||
@ -355,40 +360,45 @@ func (self *Command) Validate() error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
seen_flags := make(map[string]bool)
|
seen_flags := make(map[string]bool)
|
||||||
seen_dests := make(map[string]bool)
|
|
||||||
for _, g := range self.OptionGroups {
|
self.option_map = make(map[string]*Option, 128)
|
||||||
for _, o := range g.Options {
|
validate_options := func(opt *Option) error {
|
||||||
if seen_dests[o.Name] {
|
if self.option_map[opt.Name] != nil {
|
||||||
return &ParseError{Message: fmt.Sprintf("The option :yellow:`%s` occurs twice inside %s", o.Name, self.Name)}
|
return &ParseError{Message: fmt.Sprintf("The option :yellow:`%s` occurs twice inside %s", opt.Name, self.Name)}
|
||||||
}
|
|
||||||
seen_dests[o.Name] = true
|
|
||||||
for _, a := range o.Aliases {
|
|
||||||
q := a.String()
|
|
||||||
if seen_flags[q] {
|
|
||||||
return &ParseError{Message: fmt.Sprintf("The option :yellow:`%s` occurs twice inside %s", q, self.Name)}
|
|
||||||
}
|
|
||||||
seen_flags[q] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
for _, a := range opt.Aliases {
|
||||||
|
q := a.String()
|
||||||
|
if seen_flags[q] {
|
||||||
|
return &ParseError{Message: fmt.Sprintf("The option :yellow:`%s` occurs twice inside %s", q, self.Name)}
|
||||||
|
}
|
||||||
|
seen_flags[q] = true
|
||||||
|
}
|
||||||
|
self.option_map[opt.Name] = opt
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
if !seen_dests["Help"] {
|
err := self.VisitAllOptions(validate_options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.option_map["Help"] == nil {
|
||||||
if seen_flags["-h"] || seen_flags["--help"] {
|
if seen_flags["-h"] || seen_flags["--help"] {
|
||||||
return &ParseError{Message: fmt.Sprintf("The --help or -h flags are assigned to an option other than Help in %s", self.Name)}
|
return &ParseError{Message: fmt.Sprintf("The --help or -h flags are assigned to an option other than Help in %s", self.Name)}
|
||||||
}
|
}
|
||||||
self.Add(OptionSpec{Name: "--help -h", Type: "bool-set", Help: "Show help for this command"})
|
self.option_map["Help"] = self.Add(OptionSpec{Name: "--help -h", Type: "bool-set", Help: "Show help for this command"})
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.Parent == nil && !seen_dests["Version"] {
|
if self.Parent == nil && self.option_map["Version"] == nil {
|
||||||
if seen_flags["--version"] {
|
if seen_flags["--version"] {
|
||||||
return &ParseError{Message: fmt.Sprintf("The --version flag is assigned to an option other than Version in %s", self.Name)}
|
return &ParseError{Message: fmt.Sprintf("The --version flag is assigned to an option other than Version in %s", self.Name)}
|
||||||
}
|
}
|
||||||
self.Add(OptionSpec{Name: "--version", Type: "bool-set", Help: "Show version"})
|
self.option_map["Version"] = self.Add(OptionSpec{Name: "--version", Type: "bool-set", Help: "Show version"})
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Command) Root(args []string) *Command {
|
func (self *Command) Root() *Command {
|
||||||
p := self
|
p := self
|
||||||
for p.Parent != nil {
|
for p.Parent != nil {
|
||||||
p = p.Parent
|
p = p.Parent
|
||||||
@ -402,8 +412,6 @@ func (self *Command) CommandStringForUsage() string {
|
|||||||
for p != nil {
|
for p != nil {
|
||||||
if p.Name != "" {
|
if p.Name != "" {
|
||||||
names = append(names, p.Name)
|
names = append(names, p.Name)
|
||||||
} else if p.exe_name != "" {
|
|
||||||
names = append(names, p.Name)
|
|
||||||
}
|
}
|
||||||
p = p.Parent
|
p = p.Parent
|
||||||
}
|
}
|
||||||
@ -411,8 +419,7 @@ func (self *Command) CommandStringForUsage() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (self *Command) ParseArgs(args []string) (*Command, error) {
|
func (self *Command) ParseArgs(args []string) (*Command, error) {
|
||||||
if self.Parent != nil {
|
for ; self.Parent != nil; self = self.Parent {
|
||||||
return nil, &ParseError{Message: "ParseArgs() must be called on the Root command"}
|
|
||||||
}
|
}
|
||||||
err := self.Validate()
|
err := self.Validate()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -425,18 +432,26 @@ func (self *Command) ParseArgs(args []string) (*Command, error) {
|
|||||||
return nil, &ParseError{Message: "At least one arg must be supplied"}
|
return nil, &ParseError{Message: "At least one arg must be supplied"}
|
||||||
}
|
}
|
||||||
ctx := Context{SeenCommands: make([]*Command, 0, 4)}
|
ctx := Context{SeenCommands: make([]*Command, 0, 4)}
|
||||||
self.exe_name = args[0]
|
|
||||||
err = self.parse_args(&ctx, args[1:])
|
err = self.parse_args(&ctx, args[1:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
self.option_map = make(map[string]*Option, 128)
|
return ctx.SeenCommands[len(ctx.SeenCommands)-1], nil
|
||||||
for _, g := range self.OptionGroups {
|
}
|
||||||
for _, o := range g.Options {
|
|
||||||
self.option_map[o.Name] = o
|
func (self *Command) ResetAfterParseArgs() {
|
||||||
|
for _, g := range self.SubCommandGroups {
|
||||||
|
for _, sc := range g.SubCommands {
|
||||||
|
sc.ResetAfterParseArgs()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return ctx.SeenCommands[len(ctx.SeenCommands)-1], nil
|
|
||||||
|
for _, g := range self.OptionGroups {
|
||||||
|
for _, o := range g.Options {
|
||||||
|
o.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.Args = make([]string, 0, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *Command) HasSubCommands() bool {
|
func (self *Command) HasSubCommands() bool {
|
||||||
@ -457,6 +472,31 @@ func (self *Command) HasVisibleSubCommands() bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *Command) VisitAllOptions(callback func(*Option) error) error {
|
||||||
|
depth := 0
|
||||||
|
iter_opts := func(cmd *Command) error {
|
||||||
|
for _, g := range cmd.OptionGroups {
|
||||||
|
for _, o := range g.Options {
|
||||||
|
if o.Depth >= depth {
|
||||||
|
err := callback(o)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for p := self; p != nil; p = p.Parent {
|
||||||
|
err := iter_opts(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
depth++
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (self *Command) GetVisibleOptions() ([]string, map[string][]*Option) {
|
func (self *Command) GetVisibleOptions() ([]string, map[string][]*Option) {
|
||||||
group_titles := make([]string, 0, len(self.OptionGroups))
|
group_titles := make([]string, 0, len(self.OptionGroups))
|
||||||
gmap := make(map[string][]*Option)
|
gmap := make(map[string][]*Option)
|
||||||
@ -634,13 +674,13 @@ func (self *Command) Exec(args ...string) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
help_opt := cmd.option_map["Help"]
|
help_opt := cmd.option_map["Help"]
|
||||||
version_opt := cmd.option_map["Version"]
|
version_opt := root.option_map["Version"]
|
||||||
exit_code := 0
|
exit_code := 0
|
||||||
if help_opt != nil && help_opt.parsed_value().(bool) {
|
if help_opt != nil && help_opt.parsed_value().(bool) {
|
||||||
cmd.ShowHelp()
|
cmd.ShowHelp()
|
||||||
os.Exit(exit_code)
|
os.Exit(exit_code)
|
||||||
} else if version_opt != nil && version_opt.parsed_value().(bool) {
|
} else if version_opt != nil && version_opt.parsed_value().(bool) {
|
||||||
cmd.ShowVersion()
|
root.ShowVersion()
|
||||||
os.Exit(exit_code)
|
os.Exit(exit_code)
|
||||||
} else if cmd.Run != nil {
|
} else if cmd.Run != nil {
|
||||||
exit_code, err = cmd.Run(cmd, cmd.Args)
|
exit_code, err = cmd.Run(cmd, cmd.Args)
|
||||||
|
|||||||
77
tools/cli/types_test.go
Normal file
77
tools/cli/types_test.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
|
||||||
|
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/shlex"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = fmt.Print
|
||||||
|
|
||||||
|
type options struct {
|
||||||
|
SimpleString string
|
||||||
|
Choices string
|
||||||
|
FromParent int
|
||||||
|
SetMe bool
|
||||||
|
Int int
|
||||||
|
Float float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCLIParsing(t *testing.T) {
|
||||||
|
|
||||||
|
rt := func(expected_cmd *Command, cmdline string, expected_options any, expected_args ...string) {
|
||||||
|
cp, err := shlex.Split(cmdline)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
cmd, err := expected_cmd.ParseArgs(cp)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
actual_options := reflect.New(reflect.TypeOf(expected_options).Elem()).Interface()
|
||||||
|
err = cmd.GetOptionValues(actual_options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(expected_options, actual_options) {
|
||||||
|
t.Fatalf("Option values incorrect (expected != actual):\nCommand line: %s\n%#v != %#v", cmdline, expected_options, actual_options)
|
||||||
|
}
|
||||||
|
if expected_args == nil {
|
||||||
|
expected_args = []string{}
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(expected_args, cmd.Args) {
|
||||||
|
t.Fatalf("Argument values incorrect (expected != actual):\nCommand line: %s\n%#v != %#v", cmdline, expected_args, cmd.Args)
|
||||||
|
}
|
||||||
|
cmd.Root().ResetAfterParseArgs()
|
||||||
|
}
|
||||||
|
|
||||||
|
root := NewRootCommand()
|
||||||
|
root.Add(OptionSpec{Name: "--from-parent -p", Type: "count", Depth: 1})
|
||||||
|
child1 := root.AddSubCommand("", "child1")
|
||||||
|
child1.Add(OptionSpec{Name: "--choices", Choices: "a b c"})
|
||||||
|
child1.Add(OptionSpec{Name: "--simple-string -s"})
|
||||||
|
child1.Add(OptionSpec{Name: "--set-me", Type: "bool-set"})
|
||||||
|
child1.Add(OptionSpec{Name: "--int", Type: "int"})
|
||||||
|
child1.Add(OptionSpec{Name: "--float", Type: "float"})
|
||||||
|
|
||||||
|
rt(
|
||||||
|
child1, "test --from-parent child1 -ps ss --choices b --from-parent one two",
|
||||||
|
&options{SimpleString: "ss", Choices: "b", FromParent: 3},
|
||||||
|
"one", "two",
|
||||||
|
)
|
||||||
|
rt(child1, "test child1", &options{Choices: "a"})
|
||||||
|
rt(child1, "test child1 --set-me --simple-string=foo one", &options{Choices: "a", SimpleString: "foo", SetMe: true}, "one")
|
||||||
|
rt(child1, "test child1 --set-me --simple-string= one", &options{Choices: "a", SetMe: true}, "one")
|
||||||
|
rt(child1, "test child1 --int -3 --simple-string -s --float=3.3", &options{Choices: "a", SimpleString: "-s", Int: -3, Float: 3.3})
|
||||||
|
|
||||||
|
_, err := child1.ParseArgs(strings.Split("test child1 --choices x", " "))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Invalid choice not caught")
|
||||||
|
}
|
||||||
|
root.ResetAfterParseArgs()
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user