Files
dolt/go/libraries/utils/argparser/parser.go
Andy Arthur a17914549d ugh parsing
2020-05-21 16:56:27 -05:00

293 lines
7.7 KiB
Go

// Copyright 2019 Liquidata, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package argparser
import (
"errors"
"sort"
"strings"
)
const (
optNameValDelimChars = " =:"
whitespaceChars = " \r\n\t"
helpFlag = "help"
helpFlagAbbrev = "h"
)
func ValidatorFromStrList(paramName string, validStrList []string) ValidationFunc {
errSuffix := " is not a valid option for '" + paramName + "'. valid options are: " + strings.Join(validStrList, "|")
validStrSet := make(map[string]struct{})
for _, str := range validStrList {
validStrSet[strings.ToLower(str)] = struct{}{}
}
return func(s string) error {
_, ok := validStrSet[strings.ToLower(s)]
if !ok {
return errors.New(s + errSuffix)
}
return nil
}
}
type ArgParser struct {
Supported []*Option
NameOrAbbrevToOpt map[string]*Option
ArgListHelp [][2]string
}
func NewArgParser() *ArgParser {
var supported []*Option
nameOrAbbrevToOpt := make(map[string]*Option)
return &ArgParser{supported, nameOrAbbrevToOpt, nil}
}
// Adds support for a new argument with the option given. Options must have a unique name and abbreviated name.
func (ap *ArgParser) SupportOption(opt *Option) {
name := opt.Name
abbrev := opt.Abbrev
_, nameExist := ap.NameOrAbbrevToOpt[name]
_, abbrevExist := ap.NameOrAbbrevToOpt[abbrev]
if name == "" {
panic("Name is required")
} else if name == "help" || abbrev == "help" || name == "h" || abbrev == "h" {
panic(`"help" and "h" are both reserved`)
} else if nameExist || abbrevExist {
panic("There is a bug. Two supported arguments have the same name or abbreviation")
} else if name[0] == '-' || (len(abbrev) > 0 && abbrev[0] == '-') {
panic("There is a bug. Option names, and abbreviations should not start with -")
} else if strings.IndexAny(name, optNameValDelimChars) != -1 || strings.IndexAny(name, whitespaceChars) != -1 {
panic("There is a bug. Option name contains an invalid character")
}
ap.Supported = append(ap.Supported, opt)
ap.NameOrAbbrevToOpt[name] = opt
if abbrev != "" {
ap.NameOrAbbrevToOpt[abbrev] = opt
}
}
// Adds support for a new flag (argument with no value). See SupportOpt for details on params.
func (ap *ArgParser) SupportsFlag(name, abbrev, desc string) *ArgParser {
opt := &Option{name, abbrev, "", OptionalFlag, desc, nil}
ap.SupportOption(opt)
return ap
}
// Adds support for a new string argument with the description given. See SupportOpt for details on params.
func (ap *ArgParser) SupportsString(name, abbrev, valDesc, desc string) *ArgParser {
opt := &Option{name, abbrev, valDesc, OptionalValue, desc, nil}
ap.SupportOption(opt)
return ap
}
func (ap *ArgParser) SupportsValidatedString(name, abbrev, valDesc, desc string, validator ValidationFunc) *ArgParser {
opt := &Option{name, abbrev, valDesc, OptionalValue, desc, validator}
ap.SupportOption(opt)
return ap
}
// Adds support for a new uint argument with the description given. See SupportOpt for details on params.
func (ap *ArgParser) SupportsUint(name, abbrev, valDesc, desc string) *ArgParser {
opt := &Option{name, abbrev, valDesc, OptionalValue, desc, isUintStr}
ap.SupportOption(opt)
return ap
}
// Adds support for a new int argument with the description given. See SupportOpt for details on params.
func (ap *ArgParser) SupportsInt(name, abbrev, valDesc, desc string) *ArgParser {
opt := &Option{name, abbrev, valDesc, OptionalValue, desc, isIntStr}
ap.SupportOption(opt)
return ap
}
// modal options in order of descending string length
func (ap *ArgParser) sortedModalOptions() []string {
smo := make([]string, 0, len(ap.Supported))
for s, opt := range ap.NameOrAbbrevToOpt {
if opt.OptType == OptionalFlag && s != "" {
smo = append(smo, s)
}
}
sort.Slice(smo, func(i, j int) bool { return len(smo[i]) > len(smo[j]) })
return smo
}
func (ap *ArgParser) matchModalOptions(arg string) (matches []*Option, rest string) {
rest = arg
// try to match longest options first
candidateFlagNames := ap.sortedModalOptions()
kontinue := true
for kontinue {
kontinue = false
// stop if we see a value option
for _, vo := range ap.sortedValueOptions() {
lv := len(vo)
isValOpt := len(rest) >= lv && rest[:lv] == vo
if isValOpt {
return matches, rest
}
}
for i, on := range candidateFlagNames {
lo := len(on)
isMatch := len(rest) >= lo && rest[:lo] == on
if isMatch {
rest = rest[lo:]
m := ap.NameOrAbbrevToOpt[on]
matches = append(matches, m)
// only match options once
head := candidateFlagNames[:i]
var tail []string
if i+1 < len(candidateFlagNames) {
tail = candidateFlagNames[i+1:]
}
candidateFlagNames = append(head, tail...)
kontinue = true
break
}
}
}
return matches, rest
}
func (ap *ArgParser) sortedValueOptions() []string {
vos := make([]string, 0, len(ap.Supported))
for s, opt := range ap.NameOrAbbrevToOpt {
if opt.OptType == OptionalValue && s != "" {
vos = append(vos, s)
}
}
sort.Slice(vos, func(i, j int) bool { return len(vos[i]) > len(vos[j]) })
return vos
}
func (ap *ArgParser) matchValueOption(arg string) (match *Option, value *string) {
for _, on := range ap.sortedValueOptions() {
lo := len(on)
isMatch := len(arg) >= lo && arg[:lo] == on
if isMatch {
v := arg[lo:]
v = strings.TrimLeft(v, optNameValDelimChars)
if len(v) > 0 {
value = &v
}
match = ap.NameOrAbbrevToOpt[on]
return match, value
}
}
return nil, nil
}
// Parses the string args given using the configuration previously specified with calls to the various Supports*
// methods. Any unrecognized arguments or incorrect types will result in an appropriate error being returned. If the
// universal --help or -h flag is found, an ErrHelp error is returned.
func (ap *ArgParser) Parse(args []string) (*ArgParseResults, error) {
list := make([]string, 0, 16)
results := make(map[string]string)
i := 0
for ; i < len(args); i++ {
arg := args[i]
if len(arg) == 0 || arg[0] != '-' || arg == "--" { // empty strings should get passed through like other naked words
list = append(list, arg)
continue
}
arg = strings.TrimLeft(arg, "-")
if arg == helpFlag || arg == helpFlagAbbrev {
return nil, ErrHelp
}
modalOpts, rest := ap.matchModalOptions(arg)
for _, opt := range modalOpts {
if _, exists := results[opt.Name]; exists {
return nil, errors.New("error: multiple values provided for `" + opt.Name + "'")
}
results[opt.Name] = ""
}
opt, value := ap.matchValueOption(rest)
if opt == nil {
if rest == "" {
continue
}
if len(modalOpts) > 0 {
// value was attached to modal flag
// eg: dolt branch -fdmy_branch
list = append(list, rest)
continue
}
return nil, UnknownArgumentParam{name: arg}
}
if _, exists := results[opt.Name]; exists {
//already provided
return nil, errors.New("error: multiple values provided for `" + opt.Name + "'")
}
if value == nil {
i++
if i >= len(args) {
return nil, errors.New("error: no value for option `" + opt.Name + "'")
}
valueStr := args[i]
value = &valueStr
}
if opt.Validator != nil {
err := opt.Validator(*value)
if err != nil {
return nil, err
}
}
results[opt.Name] = *value
}
if i < len(args) {
copy(list, args[i:])
}
return &ArgParseResults{results, list, ap}, nil
}