github.com/hasnat/dolt/go@v0.0.0-20210628190320-9eb5d843fbb7/libraries/utils/argparser/parser.go (about)

     1  // Copyright 2019 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package argparser
    16  
    17  import (
    18  	"errors"
    19  	"sort"
    20  	"strings"
    21  )
    22  
    23  const (
    24  	optNameValDelimChars = " =:"
    25  	whitespaceChars      = " \r\n\t"
    26  
    27  	helpFlag       = "help"
    28  	helpFlagAbbrev = "h"
    29  )
    30  
    31  func ValidatorFromStrList(paramName string, validStrList []string) ValidationFunc {
    32  	errSuffix := " is not a valid option for '" + paramName + "'. valid options are: " + strings.Join(validStrList, "|")
    33  	validStrSet := make(map[string]struct{})
    34  
    35  	for _, str := range validStrList {
    36  		validStrSet[strings.ToLower(str)] = struct{}{}
    37  	}
    38  
    39  	return func(s string) error {
    40  		_, ok := validStrSet[strings.ToLower(s)]
    41  
    42  		if !ok {
    43  			return errors.New(s + errSuffix)
    44  		}
    45  
    46  		return nil
    47  	}
    48  }
    49  
    50  type ArgParser struct {
    51  	Supported         []*Option
    52  	NameOrAbbrevToOpt map[string]*Option
    53  	ArgListHelp       [][2]string
    54  }
    55  
    56  func NewArgParser() *ArgParser {
    57  	var supported []*Option
    58  	nameOrAbbrevToOpt := make(map[string]*Option)
    59  	return &ArgParser{supported, nameOrAbbrevToOpt, nil}
    60  }
    61  
    62  // Adds support for a new argument with the option given. Options must have a unique name and abbreviated name.
    63  func (ap *ArgParser) SupportOption(opt *Option) {
    64  	name := opt.Name
    65  	abbrev := opt.Abbrev
    66  
    67  	_, nameExist := ap.NameOrAbbrevToOpt[name]
    68  	_, abbrevExist := ap.NameOrAbbrevToOpt[abbrev]
    69  
    70  	if name == "" {
    71  		panic("Name is required")
    72  	} else if name == "help" || abbrev == "help" || name == "h" || abbrev == "h" {
    73  		panic(`"help" and "h" are both reserved`)
    74  	} else if nameExist || abbrevExist {
    75  		panic("There is a bug.  Two supported arguments have the same name or abbreviation")
    76  	} else if name[0] == '-' || (len(abbrev) > 0 && abbrev[0] == '-') {
    77  		panic("There is a bug. Option names, and abbreviations should not start with -")
    78  	} else if strings.IndexAny(name, optNameValDelimChars) != -1 || strings.IndexAny(name, whitespaceChars) != -1 {
    79  		panic("There is a bug.  Option name contains an invalid character")
    80  	}
    81  
    82  	ap.Supported = append(ap.Supported, opt)
    83  	ap.NameOrAbbrevToOpt[name] = opt
    84  
    85  	if abbrev != "" {
    86  		ap.NameOrAbbrevToOpt[abbrev] = opt
    87  	}
    88  }
    89  
    90  // Adds support for a new flag (argument with no value). See SupportOpt for details on params.
    91  func (ap *ArgParser) SupportsFlag(name, abbrev, desc string) *ArgParser {
    92  	opt := &Option{name, abbrev, "", OptionalFlag, desc, nil}
    93  	ap.SupportOption(opt)
    94  
    95  	return ap
    96  }
    97  
    98  // Adds support for a new string argument with the description given. See SupportOpt for details on params.
    99  func (ap *ArgParser) SupportsString(name, abbrev, valDesc, desc string) *ArgParser {
   100  	opt := &Option{name, abbrev, valDesc, OptionalValue, desc, nil}
   101  	ap.SupportOption(opt)
   102  
   103  	return ap
   104  }
   105  
   106  func (ap *ArgParser) SupportsValidatedString(name, abbrev, valDesc, desc string, validator ValidationFunc) *ArgParser {
   107  	opt := &Option{name, abbrev, valDesc, OptionalValue, desc, validator}
   108  	ap.SupportOption(opt)
   109  
   110  	return ap
   111  }
   112  
   113  // Adds support for a new uint argument with the description given. See SupportOpt for details on params.
   114  func (ap *ArgParser) SupportsUint(name, abbrev, valDesc, desc string) *ArgParser {
   115  	opt := &Option{name, abbrev, valDesc, OptionalValue, desc, isUintStr}
   116  	ap.SupportOption(opt)
   117  
   118  	return ap
   119  }
   120  
   121  // Adds support for a new int argument with the description given. See SupportOpt for details on params.
   122  func (ap *ArgParser) SupportsInt(name, abbrev, valDesc, desc string) *ArgParser {
   123  	opt := &Option{name, abbrev, valDesc, OptionalValue, desc, isIntStr}
   124  	ap.SupportOption(opt)
   125  
   126  	return ap
   127  }
   128  
   129  // modal options in order of descending string length
   130  func (ap *ArgParser) sortedModalOptions() []string {
   131  	smo := make([]string, 0, len(ap.Supported))
   132  	for s, opt := range ap.NameOrAbbrevToOpt {
   133  		if opt.OptType == OptionalFlag && s != "" {
   134  			smo = append(smo, s)
   135  		}
   136  	}
   137  	sort.Slice(smo, func(i, j int) bool { return len(smo[i]) > len(smo[j]) })
   138  	return smo
   139  }
   140  
   141  func (ap *ArgParser) matchModalOptions(arg string) (matches []*Option, rest string) {
   142  	rest = arg
   143  
   144  	// try to match longest options first
   145  	candidateFlagNames := ap.sortedModalOptions()
   146  
   147  	kontinue := true
   148  	for kontinue {
   149  		kontinue = false
   150  
   151  		// stop if we see a value option
   152  		for _, vo := range ap.sortedValueOptions() {
   153  			lv := len(vo)
   154  			isValOpt := len(rest) >= lv && rest[:lv] == vo
   155  			if isValOpt {
   156  				return matches, rest
   157  			}
   158  		}
   159  
   160  		for i, on := range candidateFlagNames {
   161  			lo := len(on)
   162  			isMatch := len(rest) >= lo && rest[:lo] == on
   163  			if isMatch {
   164  				rest = rest[lo:]
   165  				m := ap.NameOrAbbrevToOpt[on]
   166  				matches = append(matches, m)
   167  
   168  				// only match options once
   169  				head := candidateFlagNames[:i]
   170  				var tail []string
   171  				if i+1 < len(candidateFlagNames) {
   172  					tail = candidateFlagNames[i+1:]
   173  				}
   174  				candidateFlagNames = append(head, tail...)
   175  
   176  				kontinue = true
   177  				break
   178  			}
   179  		}
   180  	}
   181  	return matches, rest
   182  }
   183  
   184  func (ap *ArgParser) sortedValueOptions() []string {
   185  	vos := make([]string, 0, len(ap.Supported))
   186  	for s, opt := range ap.NameOrAbbrevToOpt {
   187  		if opt.OptType == OptionalValue && s != "" {
   188  			vos = append(vos, s)
   189  		}
   190  	}
   191  	sort.Slice(vos, func(i, j int) bool { return len(vos[i]) > len(vos[j]) })
   192  	return vos
   193  }
   194  
   195  func (ap *ArgParser) matchValueOption(arg string) (match *Option, value *string) {
   196  	for _, on := range ap.sortedValueOptions() {
   197  		lo := len(on)
   198  		isMatch := len(arg) >= lo && arg[:lo] == on
   199  		if isMatch {
   200  			v := arg[lo:]
   201  			v = strings.TrimLeft(v, optNameValDelimChars)
   202  			if len(v) > 0 {
   203  				value = &v
   204  			}
   205  			match = ap.NameOrAbbrevToOpt[on]
   206  			return match, value
   207  		}
   208  	}
   209  	return nil, nil
   210  }
   211  
   212  // Parses the string args given using the configuration previously specified with calls to the various Supports*
   213  // methods. Any unrecognized arguments or incorrect types will result in an appropriate error being returned. If the
   214  // universal --help or -h flag is found, an ErrHelp error is returned.
   215  func (ap *ArgParser) Parse(args []string) (*ArgParseResults, error) {
   216  	list := make([]string, 0, 16)
   217  	results := make(map[string]string)
   218  
   219  	i := 0
   220  	for ; i < len(args); i++ {
   221  		arg := args[i]
   222  
   223  		if len(arg) == 0 || arg[0] != '-' || arg == "--" { // empty strings should get passed through like other naked words
   224  			list = append(list, arg)
   225  			continue
   226  		}
   227  
   228  		arg = strings.TrimLeft(arg, "-")
   229  
   230  		if arg == helpFlag || arg == helpFlagAbbrev {
   231  			return nil, ErrHelp
   232  		}
   233  
   234  		modalOpts, rest := ap.matchModalOptions(arg)
   235  
   236  		for _, opt := range modalOpts {
   237  			if _, exists := results[opt.Name]; exists {
   238  				return nil, errors.New("error: multiple values provided for `" + opt.Name + "'")
   239  			}
   240  
   241  			results[opt.Name] = ""
   242  		}
   243  
   244  		opt, value := ap.matchValueOption(rest)
   245  
   246  		if opt == nil {
   247  			if rest == "" {
   248  				continue
   249  			}
   250  
   251  			if len(modalOpts) > 0 {
   252  				// value was attached to modal flag
   253  				// eg: dolt branch -fdmy_branch
   254  				list = append(list, rest)
   255  				continue
   256  			}
   257  
   258  			return nil, UnknownArgumentParam{name: arg}
   259  		}
   260  
   261  		if _, exists := results[opt.Name]; exists {
   262  			//already provided
   263  			return nil, errors.New("error: multiple values provided for `" + opt.Name + "'")
   264  		}
   265  
   266  		if value == nil {
   267  			i++
   268  			if i >= len(args) {
   269  				return nil, errors.New("error: no value for option `" + opt.Name + "'")
   270  			}
   271  
   272  			valueStr := args[i]
   273  			value = &valueStr
   274  		}
   275  
   276  		if opt.Validator != nil {
   277  			err := opt.Validator(*value)
   278  
   279  			if err != nil {
   280  				return nil, err
   281  			}
   282  		}
   283  
   284  		results[opt.Name] = *value
   285  	}
   286  
   287  	if i < len(args) {
   288  		copy(list, args[i:])
   289  	}
   290  
   291  	return &ArgParseResults{results, list, ap}, nil
   292  }