From 8221713995ba9c86e1440b2edbfe88d941cf4533 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Sep 2022 19:15:55 +0530 Subject: [PATCH] Allow defining options using a struct --- tools/cli/option-from-string.go | 243 +++++++++++++++++--------------- tools/cli/types.go | 48 +++++-- 2 files changed, 162 insertions(+), 129 deletions(-) diff --git a/tools/cli/option-from-string.go b/tools/cli/option-from-string.go index 2350e7947..fed84de8c 100644 --- a/tools/cli/option-from-string.go +++ b/tools/cli/option-from-string.go @@ -99,51 +99,52 @@ type multi_scan struct { var mpat *regexp.Regexp -func option_from_string(overrides map[string]string, entries ...string) (*Option, error) { - if mpat == nil { - mpat = regexp.MustCompile("^([a-z]+)=(.+)") - } +func option_from_spec(spec OptionSpec) (*Option, error) { ans := Option{ + Help: spec.Help, values_from_cmdline: make([]string, 0, 1), parsed_values_from_cmdline: make([]any, 0, 1), } - scanner := utils.NewScanLines(entries...) - in_help := false - prev_indent := 0 - help := strings.Builder{} - help.Grow(2048) - default_was_set := false - - indent_of_line := func(x string) int { - return len(x) - len(strings.TrimLeft(x, " \n\t\v\f")) + parts := strings.Split(spec.Name, " ") + ans.Name = camel_case_dest(parts[0]) + ans.Aliases = make([]Alias, 0, len(parts)) + for i, x := range parts { + ans.Aliases[i] = Alias{NameWithoutHyphens: strings.TrimLeft(x, "-"), IsShort: !strings.HasPrefix(x, "--")} } - - set_default := func(x string) { - if !default_was_set { - ans.Default = x - default_was_set = true + if spec.Dest != "" { + ans.Name = spec.Dest + } + ans.Depth = spec.Depth + if spec.Choices != "" { + parts := strings.Split(spec.Choices, ",") + ans.Choices = make(map[string]bool, len(parts)) + ans.OptionType = StringOption + for i, x := range parts { + x = strings.TrimSpace(x) + ans.Choices[x] = true + if i == 0 && ans.Default == "" { + ans.Default = x + } } - } - - set_type := func(v string) error { - switch v { + } else { + switch spec.Type { case "choice", "choices": ans.OptionType = StringOption case "int": ans.OptionType = IntegerOption - set_default("0") + ans.Default = "0" case "float": ans.OptionType = FloatOption - set_default("0") + ans.Default = "0" case "count": ans.OptionType = CountOption - set_default("0") + ans.Default = "0" case "bool-set": ans.OptionType = BoolOption - set_default("false") + ans.Default = "false" case "bool-reset": ans.OptionType = BoolOption - set_default("true") + ans.Default = "true" for _, a := range ans.Aliases { a.IsUnset = true } @@ -153,99 +154,14 @@ func option_from_string(overrides map[string]string, entries ...string) (*Option case "str", "string": ans.OptionType = StringOption default: - return fmt.Errorf("Unknown option type: %s", v) - } - return nil - } - - if dq, found := overrides["type"]; found { - err := set_type(dq) - if err != nil { - return nil, err + return nil, fmt.Errorf("Unknown option type: %s", spec.Type) } } - for scanner.Scan() { - line := scanner.Text() - if ans.Aliases == nil { - if strings.HasPrefix(line, "--") { - parts := strings.Split(line, " ") - if dq, found := overrides["dest"]; found { - ans.Name = camel_case_dest(dq) - } else { - ans.Name = camel_case_dest(parts[0]) - } - ans.Aliases = make([]Alias, 0, len(parts)) - for i, x := range parts { - ans.Aliases[i] = Alias{NameWithoutHyphens: strings.TrimLeft(x, "-"), IsShort: !strings.HasPrefix(x, "--")} - } - } - } else if in_help { - if line != "" { - current_indent := indent_of_line(line) - if current_indent > 1 { - if prev_indent == 0 { - help.WriteString("\n") - } else { - line = strings.TrimSpace(line) - } - } - prev_indent = current_indent - if !strings.HasSuffix(help.String(), "\n") { - help.WriteString(" ") - } - help.WriteString(line) - } else { - prev_indent = 0 - help.WriteString("\n") - if !strings.HasSuffix(help.String(), "::") { - help.WriteString("\n") - } - } - } else { - matches := mpat.FindStringSubmatch(line) - if matches == nil { - continue - } - k, v := matches[1], matches[2] - switch k { - case "choices": - parts := strings.Split(v, ",") - ans.Choices = make(map[string]bool, len(parts)) - ans.OptionType = StringOption - for i, x := range parts { - x = strings.TrimSpace(x) - ans.Choices[x] = true - if i == 0 && ans.Default == "" { - ans.Default = x - } - } - case "default": - ans.Default = v - case "dest": - if dq, found := overrides["dest"]; found { - ans.Name = camel_case_dest(dq) - } else { - ans.Name = camel_case_dest(v) - } - case "depth": - depth, err := strconv.ParseInt(v, 0, 0) - if err != nil { - return nil, err - } - ans.Depth = int(depth) - case "condition", "completion": - default: - return nil, fmt.Errorf("Unknown option metadata key: %s", k) - case "type": - err := set_type(v) - if err != nil { - return nil, err - } - } - } + if spec.Default != "" { + ans.Default = spec.Default } - ans.HelpText = help.String() - ans.Hidden = ans.HelpText == "!" + ans.Help = spec.Help + ans.Hidden = spec.Help == "!" pval, err := ans.parse_value(ans.Default) if err != nil { return nil, err @@ -262,3 +178,96 @@ func option_from_string(overrides map[string]string, entries ...string) (*Option } return &ans, nil } + +func indent_of_line(x string) int { + return len(x) - len(strings.TrimLeft(x, " \n\t\v\f")) +} + +func prepare_help_text_for_display(raw string) string { + help := strings.Builder{} + help.Grow(len(raw) + 256) + prev_indent := 0 + for _, line := range utils.Splitlines(raw) { + if line != "" { + current_indent := indent_of_line(line) + if current_indent > 1 { + if prev_indent == 0 { + help.WriteString("\n") + } else { + line = strings.TrimSpace(line) + } + } + prev_indent = current_indent + if !strings.HasSuffix(help.String(), "\n") { + help.WriteString(" ") + } + help.WriteString(line) + } else { + prev_indent = 0 + help.WriteString("\n") + if !strings.HasSuffix(help.String(), "::") { + help.WriteString("\n") + } + } + } + return help.String() +} + +func option_from_string(overrides map[string]string, entries ...string) (*Option, error) { + if mpat == nil { + mpat = regexp.MustCompile("^([a-z]+)=(.+)") + } + spec := OptionSpec{} + scanner := utils.NewScanLines(entries...) + in_help := false + help := strings.Builder{} + help.Grow(2048) + + if dq, found := overrides["type"]; found { + spec.Type = dq + } + if dq, found := overrides["dest"]; found { + spec.Dest = dq + } + for scanner.Scan() { + line := scanner.Text() + if spec.Name == "" { + if strings.HasPrefix(line, "--") { + spec.Name = line + } + } else if in_help { + spec.Help += line + "\n" + } else { + line = strings.TrimSpace(line) + matches := mpat.FindStringSubmatch(line) + if matches == nil { + continue + } + k, v := matches[1], matches[2] + switch k { + case "choices": + spec.Choices = v + case "default": + if overrides["default"] == "" { + spec.Default = v + } + case "dest": + if overrides["dest"] == "" { + spec.Dest = v + } + case "depth": + depth, err := strconv.ParseInt(v, 0, 0) + if err != nil { + return nil, err + } + spec.Depth = int(depth) + case "condition", "completion": + default: + return nil, fmt.Errorf("Unknown option metadata key: %s", k) + case "type": + spec.Type = v + } + } + } + return option_from_spec(spec) +} diff --git a/tools/cli/types.go b/tools/cli/types.go index d24eb203e..a5b669c91 100644 --- a/tools/cli/types.go +++ b/tools/cli/types.go @@ -45,7 +45,7 @@ type Option struct { OptionType OptionType Hidden bool Depth int - HelpText string + Help string IsList bool Parent *Command @@ -204,6 +204,16 @@ func (self *CommandGroup) FindSubCommand(name string) *Command { return nil } +type OptionSpec struct { + Name string + Type string + Dest string + Choices string + Depth int + Default string + Help string +} + type OptionGroup struct { // {{{ Options []*Option Title string @@ -219,7 +229,15 @@ func (self *OptionGroup) Clone(parent *Command) *OptionGroup { return &ans } -func (self *OptionGroup) AddOption(parent *Command, items ...string) (*Option, error) { +func (self *OptionGroup) AddOption(parent *Command, spec OptionSpec) (*Option, error) { + ans, err := option_from_spec(spec) + if err == nil { + ans.Parent = parent + } + return ans, err +} + +func (self *OptionGroup) AddOptionFromString(parent *Command, items ...string) (*Option, error) { ans, err := OptionFromString(items...) if err == nil { ans.Parent = parent @@ -338,12 +356,10 @@ func (self *Command) Validate() error { 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)} } - self.AddOption(fmt.Sprintf(` ---help -h -type: bool-set -Show help for this command -`)) - + _, err := self.Add(OptionSpec{Name: "--help -h", Type: "bool-set", Help: "Show help for this command"}) + if err != nil { + return err + } } return nil @@ -424,12 +440,20 @@ func (self *Command) AddOptionGroup(title string) *OptionGroup { return &ans } -func (self *Command) AddOption(items ...string) (*Option, error) { - return self.AddOptionGroup("").AddOption(self, items...) +func (self *Command) AddOptionFromString(items ...string) (*Option, error) { + return self.AddOptionGroup("").AddOptionFromString(self, items...) } -func (self *Command) AddOptionToGroup(group string, items ...string) (*Option, error) { - return self.AddOptionGroup(group).AddOption(self, items...) +func (self *Command) Add(s OptionSpec) (*Option, error) { + return self.AddOptionGroup("").AddOption(self, s) +} + +func (self *Command) AddOptionToGroupFromString(group string, items ...string) (*Option, error) { + return self.AddOptionGroup(group).AddOptionFromString(self, items...) +} + +func (self *Command) AddToGroup(group string, s OptionSpec) (*Option, error) { + return self.AddOptionGroup(group).AddOption(self, s) } func (self *Command) FindOption(name_with_hyphens string) *Option {