diff --git a/tools/cli/types.go b/tools/cli/command.go similarity index 61% rename from tools/cli/types.go rename to tools/cli/command.go index 45baf11f6..a17272cd9 100644 --- a/tools/cli/types.go +++ b/tools/cli/command.go @@ -7,282 +7,14 @@ import ( "os" "path/filepath" "reflect" - "strconv" "strings" - "kitty/tools/cli/completion" "kitty/tools/utils" - - "golang.org/x/exp/slices" ) var _ = fmt.Print -// Option {{{ -type OptionType int - -const ( - StringOption OptionType = iota - IntegerOption - FloatOption - BoolOption - CountOption -) - -type Alias struct { - NameWithoutHyphens string - IsShort bool - IsUnset bool -} - -func (self *Alias) String() string { - if self.IsShort { - return "-" + self.NameWithoutHyphens - } - return "--" + self.NameWithoutHyphens -} - -type OptionSpec struct { - Name string - Type string - Dest string - Choices string - Depth int - Default string - Help string - CompletionFunc *completion.CompletionFunc -} - -type Option struct { - Name string - Aliases []Alias - Choices []string - Default string - OptionType OptionType - Hidden bool - Depth int - Help string - IsList bool - Parent *Command - CompletionFunc *completion.CompletionFunc - - values_from_cmdline []string - parsed_values_from_cmdline []any - parsed_default any - 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 { - return self.OptionType != BoolOption && self.OptionType != CountOption -} - -func (self *Option) HasAlias(name_without_hyphens string, is_short bool) bool { - for _, a := range self.Aliases { - if a.IsShort == is_short && a.NameWithoutHyphens == name_without_hyphens { - return true - } - } - return false -} - -type ParseError struct { - Option *Option - Message string -} - -func (self *ParseError) Error() string { return self.Message } - -func NormalizeOptionName(name string) string { - return strings.ReplaceAll(strings.TrimLeft(name, "-"), "_", "-") -} - -func (self *Option) parsed_value() any { - 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 { - ans := make([]string, len(self.parsed_values_from_cmdline)) - for i, x := range self.parsed_values_from_cmdline { - ans[i] = x.(string) - } - return ans - } - fallthrough - default: - return self.parsed_values_from_cmdline[len(self.parsed_values_from_cmdline)-1] - } -} - -func (self *Option) parse_value(val string) (any, 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 int(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 { - case BoolOption: - for _, x := range self.Aliases { - if x.NameWithoutHyphens == name_without_hyphens { - if x.IsUnset { - self.values_from_cmdline = append(self.values_from_cmdline, "false") - self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, false) - } else { - self.values_from_cmdline = append(self.values_from_cmdline, "true") - self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, true) - } - return nil - } - } - case StringOption: - if self.Choices != nil && !slices.Contains(self.Choices, val) { - return &ParseError{Option: self, Message: fmt.Sprintf(":yellow:`%s` is not a valid value for :bold:`%s`. Valid values: %s", - val, self.seen_option, strings.Join(self.Choices, ", "), - )} - } - self.values_from_cmdline = append(self.values_from_cmdline, val) - self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, val) - case IntegerOption, FloatOption: - pval, err := self.parse_value(val) - if err != nil { - return err - } - self.values_from_cmdline = append(self.values_from_cmdline, val) - self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, pval) - case CountOption: - self.values_from_cmdline = append(self.values_from_cmdline, val) - self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, 1) - } - return nil -} - -// }}} - -// Groups {{{ -type CommandGroup struct { - SubCommands []*Command - Title string -} - -func (self *CommandGroup) HasVisibleSubCommands() bool { - for _, c := range self.SubCommands { - if !c.Hidden { - return true - } - } - return false -} - -func (self *CommandGroup) Clone(parent *Command) *CommandGroup { - ans := CommandGroup{Title: self.Title, SubCommands: make([]*Command, 0, len(self.SubCommands))} - for i, o := range self.SubCommands { - self.SubCommands[i] = o.Clone(parent) - } - return &ans -} - -func (self *CommandGroup) AddSubCommand(parent *Command, name string) *Command { - ans := NewRootCommand() - ans.Parent = parent - ans.Name = name - self.SubCommands = append(self.SubCommands, ans) - return ans -} - -func (self *CommandGroup) FindSubCommand(name string) *Command { - for _, c := range self.SubCommands { - if c.Name == name { - return c - } - } - return nil -} - -type OptionGroup struct { - Options []*Option - Title string -} - -func (self *OptionGroup) Clone(parent *Command) *OptionGroup { - ans := OptionGroup{Title: self.Title, Options: make([]*Option, len(self.Options))} - for i, o := range self.Options { - c := *o - c.init_option() - c.Parent = parent - ans.Options[i] = &c - } - return &ans -} - -func (self *OptionGroup) AddOption(parent *Command, spec OptionSpec) (*Option, error) { - ans, err := option_from_spec(spec) - if err == nil { - ans.Parent = parent - self.Options = append(self.Options, ans) - } - return ans, err -} - -func (self *OptionGroup) AddOptionFromString(parent *Command, items ...string) (*Option, error) { - ans, err := OptionFromString(items...) - if err == nil { - ans.Parent = parent - self.Options = append(self.Options, ans) - } - return ans, err -} - -func (self *OptionGroup) FindOption(name_with_hyphens string) *Option { - is_short := !strings.HasPrefix(name_with_hyphens, "--") - option_name := NormalizeOptionName(name_with_hyphens) - for _, q := range self.Options { - if q.HasAlias(option_name, is_short) { - return q - } - } - return nil -} - -// }}} - -type Command struct { // {{{ +type Command struct { Name, Group string Usage, ShortDescription, HelpText string Hidden bool @@ -721,5 +453,3 @@ func (self *Command) Exec(args ...string) { } os.Exit(exit_code) } - -// }}} diff --git a/tools/cli/group.go b/tools/cli/group.go new file mode 100644 index 000000000..29d412a37 --- /dev/null +++ b/tools/cli/group.go @@ -0,0 +1,94 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package cli + +import ( + "fmt" + "strings" +) + +var _ = fmt.Print + +type CommandGroup struct { + SubCommands []*Command + Title string +} + +func (self *CommandGroup) HasVisibleSubCommands() bool { + for _, c := range self.SubCommands { + if !c.Hidden { + return true + } + } + return false +} + +func (self *CommandGroup) Clone(parent *Command) *CommandGroup { + ans := CommandGroup{Title: self.Title, SubCommands: make([]*Command, 0, len(self.SubCommands))} + for i, o := range self.SubCommands { + self.SubCommands[i] = o.Clone(parent) + } + return &ans +} + +func (self *CommandGroup) AddSubCommand(parent *Command, name string) *Command { + ans := NewRootCommand() + ans.Parent = parent + ans.Name = name + self.SubCommands = append(self.SubCommands, ans) + return ans +} + +func (self *CommandGroup) FindSubCommand(name string) *Command { + for _, c := range self.SubCommands { + if c.Name == name { + return c + } + } + return nil +} + +type OptionGroup struct { + Options []*Option + Title string +} + +func (self *OptionGroup) Clone(parent *Command) *OptionGroup { + ans := OptionGroup{Title: self.Title, Options: make([]*Option, len(self.Options))} + for i, o := range self.Options { + c := *o + c.init_option() + c.Parent = parent + ans.Options[i] = &c + } + return &ans +} + +func (self *OptionGroup) AddOption(parent *Command, spec OptionSpec) (*Option, error) { + ans, err := option_from_spec(spec) + if err == nil { + ans.Parent = parent + self.Options = append(self.Options, ans) + } + return ans, err +} + +func (self *OptionGroup) AddOptionFromString(parent *Command, items ...string) (*Option, error) { + ans, err := OptionFromString(items...) + if err == nil { + ans.Parent = parent + self.Options = append(self.Options, ans) + } + return ans, err +} + +func (self *OptionGroup) FindOption(name_with_hyphens string) *Option { + is_short := !strings.HasPrefix(name_with_hyphens, "--") + option_name := NormalizeOptionName(name_with_hyphens) + for _, q := range self.Options { + if q.HasAlias(option_name, is_short) { + return q + } + } + return nil +} diff --git a/tools/cli/option.go b/tools/cli/option.go new file mode 100644 index 000000000..f14e0ab9e --- /dev/null +++ b/tools/cli/option.go @@ -0,0 +1,189 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package cli + +import ( + "fmt" + "strconv" + "strings" + + "golang.org/x/exp/slices" + + "kitty/tools/cli/completion" +) + +var _ = fmt.Print + +type OptionType int + +const ( + StringOption OptionType = iota + IntegerOption + FloatOption + BoolOption + CountOption +) + +type Alias struct { + NameWithoutHyphens string + IsShort bool + IsUnset bool +} + +func (self *Alias) String() string { + if self.IsShort { + return "-" + self.NameWithoutHyphens + } + return "--" + self.NameWithoutHyphens +} + +type OptionSpec struct { + Name string + Type string + Dest string + Choices string + Depth int + Default string + Help string + CompletionFunc *completion.CompletionFunc +} + +type Option struct { + Name string + Aliases []Alias + Choices []string + Default string + OptionType OptionType + Hidden bool + Depth int + Help string + IsList bool + Parent *Command + CompletionFunc *completion.CompletionFunc + + values_from_cmdline []string + parsed_values_from_cmdline []any + parsed_default any + 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 { + return self.OptionType != BoolOption && self.OptionType != CountOption +} + +func (self *Option) HasAlias(name_without_hyphens string, is_short bool) bool { + for _, a := range self.Aliases { + if a.IsShort == is_short && a.NameWithoutHyphens == name_without_hyphens { + return true + } + } + return false +} + +type ParseError struct { + Option *Option + Message string +} + +func (self *ParseError) Error() string { return self.Message } + +func NormalizeOptionName(name string) string { + return strings.ReplaceAll(strings.TrimLeft(name, "-"), "_", "-") +} + +func (self *Option) parsed_value() any { + 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 { + ans := make([]string, len(self.parsed_values_from_cmdline)) + for i, x := range self.parsed_values_from_cmdline { + ans[i] = x.(string) + } + return ans + } + fallthrough + default: + return self.parsed_values_from_cmdline[len(self.parsed_values_from_cmdline)-1] + } +} + +func (self *Option) parse_value(val string) (any, 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 int(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 { + case BoolOption: + for _, x := range self.Aliases { + if x.NameWithoutHyphens == name_without_hyphens { + if x.IsUnset { + self.values_from_cmdline = append(self.values_from_cmdline, "false") + self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, false) + } else { + self.values_from_cmdline = append(self.values_from_cmdline, "true") + self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, true) + } + return nil + } + } + case StringOption: + if self.Choices != nil && !slices.Contains(self.Choices, val) { + return &ParseError{Option: self, Message: fmt.Sprintf(":yellow:`%s` is not a valid value for :bold:`%s`. Valid values: %s", + val, self.seen_option, strings.Join(self.Choices, ", "), + )} + } + self.values_from_cmdline = append(self.values_from_cmdline, val) + self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, val) + case IntegerOption, FloatOption: + pval, err := self.parse_value(val) + if err != nil { + return err + } + self.values_from_cmdline = append(self.values_from_cmdline, val) + self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, pval) + case CountOption: + self.values_from_cmdline = append(self.values_from_cmdline, val) + self.parsed_values_from_cmdline = append(self.parsed_values_from_cmdline, 1) + } + return nil +}