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 }