diff --git a/tools/cli/option-from-string.go b/tools/cli/option-from-string.go index c4812004a..44713d05b 100644 --- a/tools/cli/option-from-string.go +++ b/tools/cli/option-from-string.go @@ -12,8 +12,6 @@ import ( var _ = fmt.Print -// OptionFromString {{{ - /* Create an [Option] from a string. Syntax is:: @@ -36,7 +34,10 @@ func OptionFromString(entries ...string) (*Option, error) { if mpat == nil { mpat = regexp.MustCompile("^([a-z]+)=(.+)") } - ans := Option{} + ans := Option{ + values_from_cmdline: make([]string, 0, 1), + parsed_values_from_cmdline: make([]interface{}, 0, 1), + } scanner := bufio.NewScanner(strings.NewReader(strings.Join(entries, "\n"))) in_help := false prev_indent := 0 @@ -47,6 +48,12 @@ func OptionFromString(entries ...string) (*Option, error) { return len(x) - len(strings.TrimLeft(x, " \n\t\v\f")) } + set_default := func(x string) { + if ans.Default == "" { + ans.Default = x + } + } + for scanner.Scan() { line := scanner.Text() if ans.Aliases == nil { @@ -117,18 +124,26 @@ func OptionFromString(entries ...string) (*Option, error) { ans.OptionType = StringOption case "int": ans.OptionType = IntegerOption + set_default("0") case "float": ans.OptionType = FloatOption + set_default("0") case "count": ans.OptionType = CountOption + set_default("0") case "bool-set": ans.OptionType = BoolOption + set_default("false") case "bool-reset": ans.OptionType = BoolOption + set_default("true") for _, a := range ans.Aliases { a.IsUnset = true } - case "list", "str", "string": + case "list": + ans.IsList = true + fallthrough + case "str", "string": ans.OptionType = StringOption default: return nil, fmt.Errorf("Unknown option type: %s", v) @@ -138,5 +153,13 @@ func OptionFromString(entries ...string) (*Option, error) { } ans.HelpText = help.String() ans.Hidden = ans.HelpText == "!" + pval, err := ans.parse_value(ans.Default) + if err != nil { + return nil, err + } + ans.parsed_default = pval + if ans.IsList { + ans.parsed_default = []string{} + } return &ans, nil -} // }}} +} diff --git a/tools/cli/parse-args.go b/tools/cli/parse-args.go index af5e9b93f..2e53637d2 100644 --- a/tools/cli/parse-args.go +++ b/tools/cli/parse-args.go @@ -25,13 +25,16 @@ func (self *Command) parse_args(ctx *Context, args []string) error { return &ParseError{Message: fmt.Sprintf("Unknown option: :yellow:`%s`", opt_str)} } opt.seen_option = opt_str + needs_arg := opt.needs_argument() if has_val { - if !opt.needs_argument() { + if !needs_arg { return &ParseError{Message: fmt.Sprintf("The option: :yellow:`%s` does not take values", opt_str)} } return opt.add_value(opt_val) - } else if opt.needs_argument() { + } else if needs_arg { expecting_arg_for = opt + } else { + opt.add_value("") } return nil } diff --git a/tools/cli/types.go b/tools/cli/types.go index ba387ae62..f2393e42b 100644 --- a/tools/cli/types.go +++ b/tools/cli/types.go @@ -5,6 +5,7 @@ package cli import ( "fmt" "os" + "reflect" "regexp" "strconv" "strings" @@ -44,10 +45,12 @@ type Option struct { Hidden bool Depth int HelpText string + IsList bool Parent *Command values_from_cmdline []string parsed_values_from_cmdline []interface{} + parsed_default interface{} seen_option string } @@ -77,6 +80,55 @@ func NormalizeOptionName(name string) string { return strings.ReplaceAll(strings.TrimLeft(name, "-"), "_", "-") } +func (self *Option) parsed_value() interface{} { + if len(self.values_from_cmdline) == 0 { + return self.parsed_default + } + switch self.OptionType { + case CountOption: + return len(self.parsed_values_from_cmdline) + case StringOption: + if self.IsList { + return self.parsed_values_from_cmdline + } + fallthrough + default: + return self.parsed_values_from_cmdline[len(self.parsed_values_from_cmdline)-1] + } +} + +func (self *Option) parse_value(val string) (interface{}, error) { + switch self.OptionType { + case BoolOption: + switch val { + case "true": + return true, nil + case "false": + return false, nil + default: + return nil, &ParseError{Option: self, Message: fmt.Sprintf(":yellow:`%s` is not a valid value for :bold:`%s`.", val, self.seen_option)} + } + case StringOption: + return val, nil + case IntegerOption, CountOption: + pval, err := strconv.ParseInt(val, 0, 0) + if err != nil { + 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)} + } + return pval, nil + case FloatOption: + pval, err := strconv.ParseFloat(val, 64) + if err != nil { + return nil, &ParseError{Option: self, Message: fmt.Sprintf( + ":yellow:`%s` is not a valid number for :bold:`%s`. Only floats in decimal and hexadecimal notation are accepted.", val, self.seen_option)} + } + return pval, nil + default: + return nil, &ParseError{Option: self, Message: fmt.Sprintf("Unknown option type for %s", self.Name)} + } +} + func (self *Option) add_value(val string) error { name_without_hyphens := NormalizeOptionName(self.seen_option) switch self.OptionType { @@ -105,19 +157,10 @@ func (self *Option) add_value(val string) error { } self.values_from_cmdline = append(self.values_from_cmdline, val) self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, val) - case IntegerOption: - pval, err := strconv.ParseInt(val, 0, 0) + case IntegerOption, FloatOption: + pval, err := self.parse_value(val) if err != nil { - return &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)} - } - self.values_from_cmdline = append(self.values_from_cmdline, val) - self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, pval) - case FloatOption: - pval, err := strconv.ParseFloat(val, 64) - if err != nil { - return &ParseError{Option: self, Message: fmt.Sprintf( - ":yellow:`%s` is not a valid number for :bold:`%s`. Only floats in decimal and hexadecimal notation are accepted.", val, self.seen_option)} + return err } self.values_from_cmdline = append(self.values_from_cmdline, val) self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, pval) @@ -333,3 +376,69 @@ func (self *Command) FindOption(name_with_hyphens string) *Option { type Context struct { SeenCommands []*Command } + +func (self *Command) GetOptionValues(pointer_to_options_struct interface{}) error { + m := make(map[string]*Option, 128) + for _, g := range self.OptionGroups { + for _, o := range g.Options { + field_name := strings.ReplaceAll(strings.ToUpper(o.Name[:1]+o.Name[1:]), "-", "_") + m[field_name] = o + } + } + val := reflect.ValueOf(pointer_to_options_struct).Elem() + if val.Kind() != reflect.Struct { + return fmt.Errorf("Need a pointer to a struct to set option values on") + } + for i := 0; i < val.NumField(); i++ { + f := val.Field(i) + field_name := val.Type().Field(i).Name + if strings.ToUpper(field_name[:1]) != field_name[:1] || !f.CanSet() { + continue + } + opt := m[field_name] + if opt == nil { + return fmt.Errorf("No option with the name: %s", field_name) + } + switch opt.OptionType { + case IntegerOption, CountOption: + if f.Kind() != reflect.Int { + return fmt.Errorf("The field: %s must be an integer", field_name) + } + v := int64(opt.parsed_value().(int)) + if f.OverflowInt(v) { + return fmt.Errorf("The value: %d is too large for the integer type used for the option: %s", v, field_name) + } + f.SetInt(v) + case FloatOption: + if f.Kind() != reflect.Float64 { + return fmt.Errorf("The field: %s must be a float64", field_name) + } + v := opt.parsed_value().(float64) + if f.OverflowFloat(v) { + return fmt.Errorf("The value: %f is too large for the integer type used for the option: %s", v, field_name) + } + f.SetFloat(v) + case BoolOption: + if f.Kind() != reflect.Bool { + return fmt.Errorf("The field: %s must be a boolean", field_name) + } + v := opt.parsed_value().(bool) + f.SetBool(v) + case StringOption: + if opt.IsList { + if f.Kind() != reflect.Slice { + return fmt.Errorf("The field: %s must be a slice", field_name) + } + v := opt.parsed_value().([]string) + f.Set(reflect.ValueOf(v)) + } else { + if f.Kind() != reflect.String { + return fmt.Errorf("The field: %s must be a string", field_name) + } + v := opt.parsed_value().(string) + f.SetString(v) + } + } + } + return nil +}