github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/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  	"fmt"
    20  	"sort"
    21  	"strings"
    22  )
    23  
    24  const (
    25  	optNameValDelimChars = " =:"
    26  	whitespaceChars      = " \r\n\t"
    27  
    28  	helpFlag       = "help"
    29  	helpFlagAbbrev = "h"
    30  )
    31  
    32  func ValidatorFromStrList(paramName string, validStrList []string) ValidationFunc {
    33  	errSuffix := " is not a valid option for '" + paramName + "'. valid options are: " + strings.Join(validStrList, "|")
    34  	validStrSet := make(map[string]struct{})
    35  
    36  	for _, str := range validStrList {
    37  		validStrSet[strings.ToLower(str)] = struct{}{}
    38  	}
    39  
    40  	return func(s string) error {
    41  		_, ok := validStrSet[strings.ToLower(s)]
    42  
    43  		if !ok {
    44  			return errors.New(s + errSuffix)
    45  		}
    46  
    47  		return nil
    48  	}
    49  }
    50  
    51  type ArgParser struct {
    52  	Name                 string
    53  	MaxArgs              int
    54  	TooManyArgsErrorFunc func(receivedArgs []string) error
    55  	Supported            []*Option
    56  	nameOrAbbrevToOpt    map[string]*Option
    57  	ArgListHelp          [][2]string
    58  }
    59  
    60  // NewArgParserWithMaxArgs creates a new ArgParser for a named command that limits how many positional arguments it
    61  // will accept. If additional arguments are provided, parsing will return an error with a detailed error message,
    62  // using the provided command name.
    63  func NewArgParserWithMaxArgs(name string, maxArgs int) *ArgParser {
    64  	tooManyArgsErrorGenerator := func(receivedArgs []string) error {
    65  		args := strings.Join(receivedArgs, ", ")
    66  		if maxArgs == 0 {
    67  			return fmt.Errorf("error: %s does not take positional arguments, but found %d: %s", name, len(receivedArgs), args)
    68  		}
    69  		return fmt.Errorf("error: %s has too many positional arguments. Expected at most %d, found %d: %s", name, maxArgs, len(receivedArgs), args)
    70  	}
    71  	var supported []*Option
    72  	nameOrAbbrevToOpt := make(map[string]*Option)
    73  	return &ArgParser{
    74  		Name:                 name,
    75  		MaxArgs:              maxArgs,
    76  		TooManyArgsErrorFunc: tooManyArgsErrorGenerator,
    77  		Supported:            supported,
    78  		nameOrAbbrevToOpt:    nameOrAbbrevToOpt,
    79  	}
    80  }
    81  
    82  // NewArgParserWithVariableArgs creates a new ArgParser for a named command
    83  // that accepts any number of positional arguments.
    84  func NewArgParserWithVariableArgs(name string) *ArgParser {
    85  	return NewArgParserWithMaxArgs(name, -1)
    86  }
    87  
    88  // SupportOption adds support for a new argument with the option given. Options must have a unique name and abbreviated name.
    89  func (ap *ArgParser) SupportOption(opt *Option) {
    90  	name := opt.Name
    91  	abbrev := opt.Abbrev
    92  
    93  	_, nameExist := ap.nameOrAbbrevToOpt[name]
    94  	_, abbrevExist := ap.nameOrAbbrevToOpt[abbrev]
    95  
    96  	if name == "" {
    97  		panic("Name is required")
    98  	} else if name == "help" || abbrev == "help" || name == "h" || abbrev == "h" {
    99  		panic(`"help" and "h" are both reserved`)
   100  	} else if nameExist || abbrevExist {
   101  		panic("There is a bug.  Two supported arguments have the same name or abbreviation")
   102  	} else if name[0] == '-' || (len(abbrev) > 0 && abbrev[0] == '-') {
   103  		panic("There is a bug. Option names, and abbreviations should not start with -")
   104  	} else if strings.IndexAny(name, optNameValDelimChars) != -1 || strings.IndexAny(name, whitespaceChars) != -1 {
   105  		panic("There is a bug.  Option name contains an invalid character")
   106  	}
   107  
   108  	ap.Supported = append(ap.Supported, opt)
   109  	ap.nameOrAbbrevToOpt[name] = opt
   110  
   111  	if abbrev != "" {
   112  		ap.nameOrAbbrevToOpt[abbrev] = opt
   113  	}
   114  }
   115  
   116  // SupportsFlag adds support for a new flag (argument with no value). See SupportOpt for details on params.
   117  func (ap *ArgParser) SupportsFlag(name, abbrev, desc string) *ArgParser {
   118  	opt := &Option{name, abbrev, "", OptionalFlag, desc, nil, false}
   119  	ap.SupportOption(opt)
   120  
   121  	return ap
   122  }
   123  
   124  // SupportsAlias adds support for an alias for an existing option. The alias can be used in place of the original option.
   125  func (ap *ArgParser) SupportsAlias(alias, original string) *ArgParser {
   126  	opt, ok := ap.nameOrAbbrevToOpt[original]
   127  
   128  	if !ok {
   129  		panic(fmt.Sprintf("No option found for %s, this is a bug", original))
   130  	}
   131  
   132  	ap.nameOrAbbrevToOpt[alias] = opt
   133  	return ap
   134  }
   135  
   136  // SupportsString adds support for a new string argument with the description given. See SupportOpt for details on params.
   137  func (ap *ArgParser) SupportsString(name, abbrev, valDesc, desc string) *ArgParser {
   138  	opt := &Option{name, abbrev, valDesc, OptionalValue, desc, nil, false}
   139  	ap.SupportOption(opt)
   140  
   141  	return ap
   142  }
   143  
   144  // SupportsStringList adds support for a new string list argument with the description given. See SupportOpt for details on params.
   145  func (ap *ArgParser) SupportsStringList(name, abbrev, valDesc, desc string) *ArgParser {
   146  	opt := &Option{name, abbrev, valDesc, OptionalValue, desc, nil, true}
   147  	ap.SupportOption(opt)
   148  
   149  	return ap
   150  }
   151  
   152  // SupportsOptionalString adds support for a new string argument with the description given and optional empty value.
   153  func (ap *ArgParser) SupportsOptionalString(name, abbrev, valDesc, desc string) *ArgParser {
   154  	opt := &Option{name, abbrev, valDesc, OptionalEmptyValue, desc, nil, false}
   155  	ap.SupportOption(opt)
   156  
   157  	return ap
   158  }
   159  
   160  // SupportsValidatedString adds support for a new string argument with the description given and defined validation function.
   161  func (ap *ArgParser) SupportsValidatedString(name, abbrev, valDesc, desc string, validator ValidationFunc) *ArgParser {
   162  	opt := &Option{name, abbrev, valDesc, OptionalValue, desc, validator, false}
   163  	ap.SupportOption(opt)
   164  
   165  	return ap
   166  }
   167  
   168  // SupportsUint adds support for a new uint argument with the description given. See SupportOpt for details on params.
   169  func (ap *ArgParser) SupportsUint(name, abbrev, valDesc, desc string) *ArgParser {
   170  	opt := &Option{name, abbrev, valDesc, OptionalValue, desc, isUintStr, false}
   171  	ap.SupportOption(opt)
   172  
   173  	return ap
   174  }
   175  
   176  // SupportsInt adds support for a new int argument with the description given. See SupportOpt for details on params.
   177  func (ap *ArgParser) SupportsInt(name, abbrev, valDesc, desc string) *ArgParser {
   178  	opt := &Option{name, abbrev, valDesc, OptionalValue, desc, isIntStr, false}
   179  	ap.SupportOption(opt)
   180  
   181  	return ap
   182  }
   183  
   184  // modal options in order of descending string length
   185  func (ap *ArgParser) sortedModalOptions() []string {
   186  	smo := make([]string, 0, len(ap.Supported))
   187  	for s, opt := range ap.nameOrAbbrevToOpt {
   188  		if opt.OptType == OptionalFlag && s != "" {
   189  			smo = append(smo, s)
   190  		}
   191  	}
   192  	sort.Slice(smo, func(i, j int) bool { return len(smo[i]) > len(smo[j]) })
   193  	return smo
   194  }
   195  
   196  func (ap *ArgParser) matchModalOptions(arg string) (matches []*Option, rest string) {
   197  	rest = arg
   198  
   199  	// try to match longest options first
   200  	candidateFlagNames := ap.sortedModalOptions()
   201  
   202  	kontinue := true
   203  	for kontinue {
   204  		kontinue = false
   205  
   206  		// stop if we see a value option
   207  		for _, vo := range ap.sortedValueOptions() {
   208  			lv := len(vo)
   209  			isValOpt := len(rest) >= lv && rest[:lv] == vo
   210  			if isValOpt {
   211  				return matches, rest
   212  			}
   213  		}
   214  
   215  		for i, on := range candidateFlagNames {
   216  			lo := len(on)
   217  			isMatch := len(rest) >= lo && rest[:lo] == on
   218  			if isMatch {
   219  				rest = rest[lo:]
   220  				m := ap.nameOrAbbrevToOpt[on]
   221  				matches = append(matches, m)
   222  
   223  				// only match options once
   224  				head := candidateFlagNames[:i]
   225  				var tail []string
   226  				if i+1 < len(candidateFlagNames) {
   227  					tail = candidateFlagNames[i+1:]
   228  				}
   229  				candidateFlagNames = append(head, tail...)
   230  
   231  				kontinue = true
   232  				break
   233  			}
   234  		}
   235  	}
   236  	return matches, rest
   237  }
   238  
   239  func (ap *ArgParser) sortedValueOptions() []string {
   240  	vos := make([]string, 0, len(ap.Supported))
   241  	for s, opt := range ap.nameOrAbbrevToOpt {
   242  		if (opt.OptType == OptionalValue || opt.OptType == OptionalEmptyValue) && s != "" {
   243  			vos = append(vos, s)
   244  		}
   245  	}
   246  	sort.Slice(vos, func(i, j int) bool { return len(vos[i]) > len(vos[j]) })
   247  	return vos
   248  }
   249  
   250  func (ap *ArgParser) matchValueOption(arg string, isLongFormFlag bool) (match *Option, value *string) {
   251  	for _, on := range ap.sortedValueOptions() {
   252  		lo := len(on)
   253  		isMatch := len(arg) >= lo && arg[:lo] == on
   254  		if isMatch {
   255  			v := arg[lo:]
   256  			if len(v) > 0 && !strings.Contains(optNameValDelimChars, v[:1]) { // checks if the value and the param is in the same string
   257  				// we only allow joint param and value for short form flags (ie "-" flags), similar to Git's behavior
   258  				if isLongFormFlag {
   259  					return nil, nil
   260  				}
   261  			}
   262  
   263  			v = strings.TrimLeft(v, optNameValDelimChars)
   264  			if len(v) > 0 {
   265  				value = &v
   266  			}
   267  			match = ap.nameOrAbbrevToOpt[on]
   268  			return match, value
   269  		}
   270  	}
   271  	return nil, nil
   272  }
   273  
   274  func (ap *ArgParser) ParseGlobalArgs(args []string) (apr *ArgParseResults, remaining []string, err error) {
   275  	list := make([]string, 0, 16)
   276  	results := make(map[string]string)
   277  
   278  	i := 0
   279  	for ; i < len(args); i++ {
   280  		arg := args[i]
   281  
   282  		if len(arg) == 0 || arg == "--" {
   283  			continue
   284  		}
   285  
   286  		if arg[0] != '-' {
   287  			// This isn't a flag; assume it's the subcommand. Don't parse the remaining args.
   288  			return &ArgParseResults{results, nil, ap, NO_POSITIONAL_ARGS}, args[i:], nil
   289  		}
   290  
   291  		var err error
   292  		i, list, results, err = ap.parseToken(args, i, list, results)
   293  
   294  		if err != nil {
   295  			return nil, nil, err
   296  		}
   297  	}
   298  
   299  	return nil, nil, errors.New("No valid dolt subcommand found. See 'dolt --help' for usage.")
   300  }
   301  
   302  // Parse parses the string args given using the configuration previously specified with calls to the various Supports*
   303  // methods. Any unrecognized arguments or incorrect types will result in an appropriate error being returned. If the
   304  // universal --help or -h flag is found, an ErrHelp error is returned.
   305  func (ap *ArgParser) Parse(args []string) (*ArgParseResults, error) {
   306  	positionalArgs := make([]string, 0, 16)
   307  	positionalArgsSeparatorIndex := NO_POSITIONAL_ARGS
   308  	namedArgs := make(map[string]string)
   309  	onlyPositionalArgsLeft := false
   310  
   311  	index := 0
   312  	for ; index < len(args); index++ {
   313  		arg := args[index]
   314  
   315  		// empty strings should get passed through like other naked words
   316  		if len(arg) == 0 || arg[0] != '-' || onlyPositionalArgsLeft {
   317  			positionalArgs = append(positionalArgs, arg)
   318  			continue
   319  		}
   320  
   321  		if arg == "--" {
   322  			onlyPositionalArgsLeft = true
   323  			positionalArgsSeparatorIndex = len(positionalArgs)
   324  			continue
   325  		}
   326  
   327  		var err error
   328  		index, positionalArgs, namedArgs, err = ap.parseToken(args, index, positionalArgs, namedArgs)
   329  
   330  		if err != nil {
   331  			return nil, err
   332  		}
   333  	}
   334  
   335  	if index < len(args) {
   336  		copy(positionalArgs, args[index:])
   337  	}
   338  
   339  	if ap.MaxArgs != -1 && len(positionalArgs) > ap.MaxArgs {
   340  		return nil, ap.TooManyArgsErrorFunc(positionalArgs)
   341  	}
   342  
   343  	return &ArgParseResults{namedArgs, positionalArgs, ap, positionalArgsSeparatorIndex}, nil
   344  }
   345  
   346  func (ap *ArgParser) parseToken(args []string, index int, positionalArgs []string, namedArgs map[string]string) (newIndex int, newPositionalArgs []string, newNamedArgs map[string]string, err error) {
   347  	arg := args[index]
   348  
   349  	isLongFormFlag := len(arg) >= 2 && arg[:2] == "--"
   350  
   351  	arg = strings.TrimLeft(arg, "-")
   352  
   353  	if arg == helpFlag || arg == helpFlagAbbrev {
   354  		return 0, nil, nil, ErrHelp
   355  	}
   356  
   357  	modalOpts, rest := ap.matchModalOptions(arg)
   358  
   359  	for _, opt := range modalOpts {
   360  		if _, exists := namedArgs[opt.Name]; exists {
   361  			return 0, nil, nil, errors.New("error: multiple values provided for `" + opt.Name + "'")
   362  		}
   363  
   364  		namedArgs[opt.Name] = ""
   365  	}
   366  
   367  	opt, value := ap.matchValueOption(rest, isLongFormFlag)
   368  
   369  	if opt == nil {
   370  		if rest == "" {
   371  			return index, positionalArgs, namedArgs, nil
   372  		}
   373  
   374  		if len(modalOpts) > 0 {
   375  			// value was attached to modal flag
   376  			// eg: dolt branch -fdmy_branch
   377  			positionalArgs = append(positionalArgs, rest)
   378  			return index, positionalArgs, namedArgs, nil
   379  		}
   380  
   381  		return 0, nil, nil, UnknownArgumentParam{name: arg}
   382  	}
   383  
   384  	if _, exists := namedArgs[opt.Name]; exists {
   385  		//already provided
   386  		return 0, nil, nil, errors.New("error: multiple values provided for `" + opt.Name + "'")
   387  	}
   388  
   389  	if value == nil {
   390  		index++
   391  		valueStr := ""
   392  		if index >= len(args) {
   393  			if opt.OptType != OptionalEmptyValue {
   394  				return 0, nil, nil, errors.New("error: no value for option `" + opt.Name + "'")
   395  			}
   396  		} else {
   397  			if opt.AllowMultipleOptions {
   398  				list := getListValues(args[index:])
   399  				valueStr = strings.Join(list, ",")
   400  				index += len(list) - 1
   401  			} else {
   402  				valueStr = args[index]
   403  			}
   404  		}
   405  		value = &valueStr
   406  	}
   407  
   408  	if opt.Validator != nil {
   409  		err := opt.Validator(*value)
   410  
   411  		if err != nil {
   412  			return 0, nil, nil, err
   413  		}
   414  	}
   415  
   416  	namedArgs[opt.Name] = *value
   417  	return index, positionalArgs, namedArgs, nil
   418  }
   419  
   420  func getListValues(args []string) []string {
   421  	var values []string
   422  
   423  	for _, arg := range args {
   424  		// Stop if another option found
   425  		if arg[0] == '-' || arg == "--" {
   426  			return values
   427  		}
   428  		values = append(values, arg)
   429  	}
   430  
   431  	return values
   432  }