From 34bd7544b179b16df72508254073a039a20b7e8d Mon Sep 17 00:00:00 2001 From: Evan Fiordeliso Date: Sat, 11 Nov 2023 20:49:38 -0500 Subject: [PATCH] Add option parsing --- command.go | 29 ++++++---- opts/bool.go | 8 ++- opts/float.go | 5 ++ opts/int.go | 7 ++- opts/option.go | 1 + opts/parser.go | 143 +++++++++++++++++++++++++++++++++++++++++++++++++ opts/set.go | 91 ++++++++++++++++++++++++------- opts/string.go | 5 ++ 8 files changed, 256 insertions(+), 33 deletions(-) create mode 100644 opts/parser.go diff --git a/command.go b/command.go index ee01327..746d5d3 100644 --- a/command.go +++ b/command.go @@ -67,27 +67,34 @@ func (c *Command) Execute(args []string) { args = args[1:] } - if err := c.opts.Parse(args); err != nil { + restArgs, err := c.opts.Parse(args) + if err != nil { c.ShowHelp() return } - if len(args) > 0 { - sc, ok := c.subcommands.Get(args[0]) - if ok { - sc.Execute(args[1:]) - return - } + restArgs, err = opts.Globals().Parse(restArgs) + if err != nil { + c.ShowHelp() + return + } - // TODO: remove when done with option parsing - if args[0] == "--help" { - c.ShowHelp() + if len(restArgs) > 0 { + sc, ok := c.subcommands.Get(restArgs[0]) + if ok { + sc.Execute(restArgs[1:]) return } } + helpOpt, ok := opts.Globals().GetBool("help") + if ok && helpOpt.Value() { + c.ShowHelp() + return + } + if c.CanRun() { - c.Run(args) + c.Run(restArgs) return } diff --git a/opts/bool.go b/opts/bool.go index 33d19c2..7bbf0c8 100644 --- a/opts/bool.go +++ b/opts/bool.go @@ -9,7 +9,7 @@ type BoolOption struct { value bool } -var _ Option = (*StringOption)(nil) +var _ Option = (*BoolOption)(nil) func Bool(name, shortName string, defaultValue bool, description string) *BoolOption { return &BoolOption{ @@ -39,6 +39,7 @@ func (o *BoolOption) ShortName() string { // Value implements Option. func (o *BoolOption) Parse(raw string) error { if raw == "" { + o.value = !o.value return nil } @@ -51,6 +52,11 @@ func (o *BoolOption) Parse(raw string) error { return nil } +// TakesArg implements Option. +func (*BoolOption) TakesArg() bool { + return false +} + func (o *BoolOption) Value() bool { return o.value } diff --git a/opts/float.go b/opts/float.go index b01c73f..bdc7617 100644 --- a/opts/float.go +++ b/opts/float.go @@ -51,6 +51,11 @@ func (o *FloatOption) Parse(raw string) error { return nil } +// TakesArg implements Option. +func (*FloatOption) TakesArg() bool { + return true +} + func (o *FloatOption) Value() float64 { return o.value } diff --git a/opts/int.go b/opts/int.go index 7b862fc..63341cb 100644 --- a/opts/int.go +++ b/opts/int.go @@ -9,7 +9,7 @@ type IntOption struct { value int } -var _ Option = (*StringOption)(nil) +var _ Option = (*IntOption)(nil) func Int(name, shortName string, defaultValue int, description string) *IntOption { return &IntOption{ @@ -51,6 +51,11 @@ func (o *IntOption) Parse(raw string) error { return nil } +// TakesArg implements Option. +func (*IntOption) TakesArg() bool { + return true +} + func (o *IntOption) Value() int { return o.value } diff --git a/opts/option.go b/opts/option.go index 3c1e30b..44280f3 100644 --- a/opts/option.go +++ b/opts/option.go @@ -7,6 +7,7 @@ type Option interface { ShortName() string Description() string Parse(raw string) error + TakesArg() bool } func Names(o Option) string { diff --git a/opts/parser.go b/opts/parser.go new file mode 100644 index 0000000..e3f9260 --- /dev/null +++ b/opts/parser.go @@ -0,0 +1,143 @@ +package opts + +import ( + "errors" + "fmt" + "strings" +) + +var ( + ErrInvalidShortOption = errors.New("invalid short option") + ErrCannotChainOption = errors.New("cannot chain option as it takes an argument") +) + +func (s Set) Parse(args []string) (restArgs []string, err error) { + for i := 0; i < len(args); i++ { + arg := args[i] + + // Handle regular argument + if !strings.HasPrefix(arg, "-") { + restArgs = append(restArgs, arg) + continue + } + + // Handle options terminator + if arg == "--" { + restArgs = append(restArgs, args[i+1:]...) + return + } + + // Handle long option + if strings.HasPrefix(arg, "--") { + longName := arg[2:] + value := "" + + equals := strings.Index(longName, "=") + if equals > 0 { + longName = longName[:equals] + value = longName[equals+1:] + } else if i < len(args)-1 && !strings.HasPrefix(args[i+1], "-") { + value = args[i+1] + i++ + } + + parsed := false + for _, opt := range s { + if opt.Name() != longName { + continue + } + + if err = opt.Parse(value); err != nil { + return + } + + parsed = true + break + } + + if !parsed { + restArgs = append(restArgs, arg) + } + + continue + } + + // Handle short option + shortNames := arg[1:] + if len(shortNames) == 0 { + err = ErrInvalidShortOption + } else if len(shortNames) == 1 { + parsed := false + value := "" + if i < len(args)-1 && !strings.HasPrefix(args[i+1], "-") { + value = args[i+1] + i++ + } + + for _, opt := range s { + if opt.ShortName() != shortNames { + continue + } + + if err = opt.Parse(value); err != nil { + return + } + + parsed = true + break + } + + if !parsed { + restArgs = append(restArgs, arg) + } + } else { + for j := 0; j < len(shortNames); j++ { + shortName := shortNames[j] + value := "" + takesValue := false + parsed := false + + for _, opt := range s { + if opt.ShortName() != string(shortName) { + continue + } + + if opt.TakesArg() { + if j > 0 { + err = ErrCannotChainOption + return + } + + takesValue = true + value = shortNames[j+1:] + value = strings.TrimPrefix(value, "=") + if len(value) == 0 && i < len(args)-1 && !strings.HasPrefix(args[i+1], "-") { + value = args[i+1] + i++ + } + j = len(shortNames) + } + + parsed = true + + if err = opt.Parse(value); err != nil { + return + } + + break + } + + if !parsed { + if takesValue { + restArgs = append(restArgs, arg) + break + } else { + restArgs = append(restArgs, fmt.Sprintf("-%c", shortName)) + } + } + } + } + } + + return +} diff --git a/opts/set.go b/opts/set.go index f3daae4..00c1da0 100644 --- a/opts/set.go +++ b/opts/set.go @@ -6,23 +6,6 @@ func NewSet() Set { return Set{} } -func (s *Set) Add(f Option) { - *s = append(*s, f) -} - -func (s Set) Get(name string) (Option, bool) { - for _, f := range s { - if f.Name() == name { - return f, true - } - - if f.ShortName() == name { - return f, true - } - } - return nil, false -} - func (s Set) MaxWidth() int { max := 0 for _, f := range s { @@ -33,7 +16,75 @@ func (s Set) MaxWidth() int { return max } -// TODO: Implement -func (s Set) Parse(args []string) error { - return nil +func (s *Set) Add(f Option) { + *s = append(*s, f) +} + +func (s Set) Get(name string) (Option, bool) { + for _, o := range s { + if o.Name() == name { + return o, true + } + + if o.ShortName() == name { + return o, true + } + } + return nil, false +} + +func (s Set) GetBool(name string) (*BoolOption, bool) { + o, ok := s.Get(name) + if !ok { + return nil, false + } + + b, ok := o.(*BoolOption) + if !ok { + return nil, false + } + + return b, true +} + +func (s Set) GetString(name string) (*StringOption, bool) { + o, ok := s.Get(name) + if !ok { + return nil, false + } + + b, ok := o.(*StringOption) + if !ok { + return nil, false + } + + return b, true +} + +func (s Set) GetInt(name string) (*IntOption, bool) { + o, ok := s.Get(name) + if !ok { + return nil, false + } + + b, ok := o.(*IntOption) + if !ok { + return nil, false + } + + return b, true +} + +func (s Set) GetFloat(name string) (*FloatOption, bool) { + o, ok := s.Get(name) + if !ok { + return nil, false + } + + b, ok := o.(*FloatOption) + if !ok { + return nil, false + } + + return b, true } diff --git a/opts/string.go b/opts/string.go index b5e2b4c..cabecff 100644 --- a/opts/string.go +++ b/opts/string.go @@ -43,6 +43,11 @@ func (o *StringOption) Parse(raw string) error { return nil } +// TakesArg implements Option. +func (*StringOption) TakesArg() bool { + return true +} + func (o *StringOption) Value() string { return o.value }