pkg.re/essentialkaos/ek.10@v12.41.0+incompatible/options/options.go (about) 1 // Package options provides methods for working with command-line options 2 package options 3 4 // ////////////////////////////////////////////////////////////////////////////////// // 5 // // 6 // Copyright (c) 2022 ESSENTIAL KAOS // 7 // Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0> // 8 // // 9 // ////////////////////////////////////////////////////////////////////////////////// // 10 11 import ( 12 "fmt" 13 "os" 14 "strconv" 15 "strings" 16 ) 17 18 // ////////////////////////////////////////////////////////////////////////////////// // 19 20 // Options types 21 const ( 22 STRING = iota // String option 23 INT // Int/Uint option 24 BOOL // Boolean option 25 FLOAT // Floating number option 26 MIXED // String or boolean option 27 ) 28 29 // Error codes 30 const ( 31 ERROR_UNSUPPORTED = iota 32 ERROR_NO_NAME 33 ERROR_DUPLICATE_LONGNAME 34 ERROR_DUPLICATE_SHORTNAME 35 ERROR_OPTION_IS_NIL 36 ERROR_EMPTY_VALUE 37 ERROR_REQUIRED_NOT_SET 38 ERROR_WRONG_FORMAT 39 ERROR_CONFLICT 40 ERROR_BOUND_NOT_SET 41 ERROR_UNSUPPORTED_VALUE 42 ) 43 44 // ////////////////////////////////////////////////////////////////////////////////// // 45 46 // V is basic option struct 47 type V struct { 48 Type int // option type 49 Max float64 // maximum integer option value 50 Min float64 // minimum integer option value 51 Alias string // list of aliases 52 Conflicts string // list of conflicts options 53 Bound string // list of bound options 54 Mergeble bool // option supports options value merging 55 Required bool // option is required 56 57 set bool // non-exported field 58 59 Value interface{} // default value 60 } 61 62 // Map is map with list of options 63 type Map map[string]*V 64 65 // Options is options struct 66 type Options struct { 67 short map[string]string 68 initialized bool 69 full Map 70 } 71 72 // OptionError is argument parsing error 73 type OptionError struct { 74 Option string 75 BoundOption string 76 Type int 77 } 78 79 // ////////////////////////////////////////////////////////////////////////////////// // 80 81 type optionName struct { 82 Long string 83 Short string 84 } 85 86 // ////////////////////////////////////////////////////////////////////////////////// // 87 88 // global is global options 89 var global *Options 90 91 // ////////////////////////////////////////////////////////////////////////////////// // 92 93 // Add adds a new supported option 94 func (opts *Options) Add(name string, option *V) error { 95 if !opts.initialized { 96 initOptions(opts) 97 } 98 99 optName := parseName(name) 100 101 switch { 102 case option == nil: 103 return OptionError{"--" + optName.Long, "", ERROR_OPTION_IS_NIL} 104 case optName.Long == "": 105 return OptionError{"", "", ERROR_NO_NAME} 106 case opts.full[optName.Long] != nil: 107 return OptionError{"--" + optName.Long, "", ERROR_DUPLICATE_LONGNAME} 108 case optName.Short != "" && opts.short[optName.Short] != "": 109 return OptionError{"-" + optName.Short, "", ERROR_DUPLICATE_SHORTNAME} 110 } 111 112 opts.full[optName.Long] = option 113 114 if optName.Short != "" { 115 opts.short[optName.Short] = optName.Long 116 } 117 118 if option.Alias != "" { 119 aliases := parseOptionsList(option.Alias) 120 121 for _, l := range aliases { 122 opts.full[l.Long] = option 123 124 if l.Short != "" { 125 opts.short[l.Short] = optName.Long 126 } 127 } 128 } 129 130 return nil 131 } 132 133 // AddMap adds supported options as map 134 func (opts *Options) AddMap(optMap Map) []error { 135 var errs []error 136 137 for name, opt := range optMap { 138 err := opts.Add(name, opt) 139 140 if err != nil { 141 errs = append(errs, err) 142 } 143 } 144 145 return errs 146 } 147 148 // GetS returns option value as string 149 func (opts *Options) GetS(name string) string { 150 optName := parseName(name) 151 opt, ok := opts.full[optName.Long] 152 153 switch { 154 case !ok: 155 return "" 156 case opts.full[optName.Long].Value == nil: 157 return "" 158 case opt.Type == INT: 159 return strconv.Itoa(opt.Value.(int)) 160 case opt.Type == FLOAT: 161 return strconv.FormatFloat(opt.Value.(float64), 'f', -1, 64) 162 case opt.Type == BOOL: 163 return strconv.FormatBool(opt.Value.(bool)) 164 default: 165 return opt.Value.(string) 166 } 167 } 168 169 // GetI returns option value as integer 170 func (opts *Options) GetI(name string) int { 171 optName := parseName(name) 172 opt, ok := opts.full[optName.Long] 173 174 switch { 175 case !ok: 176 return 0 177 178 case opts.full[optName.Long].Value == nil: 179 return 0 180 181 case opt.Type == STRING, opt.Type == MIXED: 182 result, err := strconv.Atoi(opt.Value.(string)) 183 if err == nil { 184 return result 185 } 186 return 0 187 188 case opt.Type == FLOAT: 189 return int(opt.Value.(float64)) 190 191 case opt.Type == BOOL: 192 if opt.Value.(bool) { 193 return 1 194 } 195 return 0 196 197 default: 198 return opt.Value.(int) 199 } 200 } 201 202 // GetB returns option value as boolean 203 func (opts *Options) GetB(name string) bool { 204 optName := parseName(name) 205 opt, ok := opts.full[optName.Long] 206 207 switch { 208 case !ok: 209 return false 210 211 case opts.full[optName.Long].Value == nil: 212 return false 213 214 case opt.Type == STRING, opt.Type == MIXED: 215 if opt.Value.(string) == "" { 216 return false 217 } 218 return true 219 220 case opt.Type == FLOAT: 221 if opt.Value.(float64) > 0 { 222 return true 223 } 224 return false 225 226 case opt.Type == INT: 227 if opt.Value.(int) > 0 { 228 return true 229 } 230 return false 231 232 default: 233 return opt.Value.(bool) 234 } 235 } 236 237 // GetF returns option value as floating number 238 func (opts *Options) GetF(name string) float64 { 239 optName := parseName(name) 240 opt, ok := opts.full[optName.Long] 241 242 switch { 243 case !ok: 244 return 0.0 245 246 case opts.full[optName.Long].Value == nil: 247 return 0.0 248 249 case opt.Type == STRING, opt.Type == MIXED: 250 result, err := strconv.ParseFloat(opt.Value.(string), 64) 251 if err == nil { 252 return result 253 } 254 return 0.0 255 256 case opt.Type == INT: 257 return float64(opt.Value.(int)) 258 259 case opt.Type == BOOL: 260 if opt.Value.(bool) { 261 return 1.0 262 } 263 return 0.0 264 265 default: 266 return opt.Value.(float64) 267 } 268 } 269 270 // Has check that option exists and set 271 func (opts *Options) Has(name string) bool { 272 opt, ok := opts.full[parseName(name).Long] 273 274 if !ok { 275 return false 276 } 277 278 if !opt.set { 279 return false 280 } 281 282 return true 283 } 284 285 // Parse parse options 286 func (opts *Options) Parse(rawOpts []string, optMap ...Map) (Arguments, []error) { 287 var errs []error 288 289 if len(optMap) != 0 { 290 for _, m := range optMap { 291 errs = append(errs, opts.AddMap(m)...) 292 } 293 } 294 295 if len(errs) != 0 { 296 return Arguments{}, errs 297 } 298 299 return opts.parseOptions(rawOpts) 300 } 301 302 // ////////////////////////////////////////////////////////////////////////////////// // 303 304 // NewOptions create new options struct 305 func NewOptions() *Options { 306 return &Options{ 307 full: make(Map), 308 short: make(map[string]string), 309 initialized: true, 310 } 311 } 312 313 // Add add new supported option 314 func Add(name string, opt *V) error { 315 if global == nil || !global.initialized { 316 global = NewOptions() 317 } 318 319 return global.Add(name, opt) 320 } 321 322 // AddMap add supported option as map 323 func AddMap(optMap Map) []error { 324 if global == nil || !global.initialized { 325 global = NewOptions() 326 } 327 328 return global.AddMap(optMap) 329 } 330 331 // GetS returns option value as string 332 func GetS(name string) string { 333 if global == nil || !global.initialized { 334 return "" 335 } 336 337 return global.GetS(name) 338 } 339 340 // GetI returns option value as integer 341 func GetI(name string) int { 342 if global == nil || !global.initialized { 343 return 0 344 } 345 346 return global.GetI(name) 347 } 348 349 // GetB returns option value as boolean 350 func GetB(name string) bool { 351 if global == nil || !global.initialized { 352 return false 353 } 354 355 return global.GetB(name) 356 } 357 358 // GetF returns option value as floating number 359 func GetF(name string) float64 { 360 if global == nil || !global.initialized { 361 return 0.0 362 } 363 364 return global.GetF(name) 365 } 366 367 // Has check that option exists and set 368 func Has(name string) bool { 369 if global == nil || !global.initialized { 370 return false 371 } 372 373 return global.Has(name) 374 } 375 376 // Parse parse options 377 func Parse(optMap ...Map) (Arguments, []error) { 378 if global == nil || !global.initialized { 379 global = NewOptions() 380 } 381 382 return global.Parse(os.Args[1:], optMap...) 383 } 384 385 // ParseOptionName parses combined name and returns long and short options 386 func ParseOptionName(name string) (string, string) { 387 a := parseName(name) 388 return a.Long, a.Short 389 } 390 391 // Q merges several options to string 392 func Q(opts ...string) string { 393 return strings.Join(opts, " ") 394 } 395 396 // ////////////////////////////////////////////////////////////////////////////////// // 397 398 // I think it is okay to have such a long and complicated method for parsing data 399 // because it has a lot of logic which can't be separated into different methods 400 // without losing code readability 401 // codebeat:disable[LOC,BLOCK_NESTING,CYCLO] 402 403 func (opts *Options) parseOptions(rawOpts []string) ([]string, []error) { 404 opts.prepare() 405 406 if len(rawOpts) == 0 { 407 return nil, opts.validate() 408 } 409 410 var ( 411 optName string 412 mixedOpt bool 413 arguments Arguments 414 errorList []error 415 ) 416 417 for _, curOpt := range rawOpts { 418 if optName == "" || mixedOpt { 419 var ( 420 curOptName string 421 curOptValue string 422 err error 423 ) 424 425 var curOptLen = len(curOpt) 426 427 switch { 428 case strings.TrimRight(curOpt, "-") == "": 429 arguments = append(arguments, curOpt) 430 continue 431 432 case curOptLen > 2 && curOpt[0:2] == "--": 433 curOptName, curOptValue, err = opts.parseLongOption(curOpt[2:curOptLen]) 434 435 case curOptLen > 1 && curOpt[0:1] == "-": 436 curOptName, curOptValue, err = opts.parseShortOption(curOpt[1:curOptLen]) 437 438 case mixedOpt: 439 errorList = appendError( 440 errorList, 441 updateOption(opts.full[optName], optName, curOpt), 442 ) 443 444 optName, mixedOpt = "", false 445 446 default: 447 arguments = append(arguments, curOpt) 448 continue 449 } 450 451 if err != nil { 452 errorList = append(errorList, err) 453 continue 454 } 455 456 if curOptName != "" && mixedOpt { 457 errorList = appendError( 458 errorList, 459 updateOption(opts.full[optName], optName, "true"), 460 ) 461 462 mixedOpt = false 463 } 464 465 if curOptValue != "" { 466 errorList = appendError( 467 errorList, 468 updateOption(opts.full[curOptName], curOptName, curOptValue), 469 ) 470 } else { 471 switch { 472 case opts.full[curOptName] != nil && opts.full[curOptName].Type == BOOL: 473 errorList = appendError( 474 errorList, 475 updateOption(opts.full[curOptName], curOptName, ""), 476 ) 477 478 case opts.full[curOptName] != nil && opts.full[curOptName].Type == MIXED: 479 optName = curOptName 480 mixedOpt = true 481 482 default: 483 optName = curOptName 484 } 485 } 486 } else { 487 errorList = appendError( 488 errorList, 489 updateOption(opts.full[optName], optName, curOpt), 490 ) 491 492 optName = "" 493 } 494 } 495 496 errorList = append(errorList, opts.validate()...) 497 498 if optName != "" { 499 if opts.full[optName].Type == MIXED { 500 errorList = appendError( 501 errorList, 502 updateOption(opts.full[optName], optName, "true"), 503 ) 504 } else { 505 errorList = append(errorList, OptionError{"--" + optName, "", ERROR_EMPTY_VALUE}) 506 } 507 } 508 509 return arguments, errorList 510 } 511 512 // codebeat:enable[LOC,BLOCK_NESTING,CYCLO] 513 514 func (opts *Options) parseLongOption(opt string) (string, string, error) { 515 if strings.Contains(opt, "=") { 516 optSlice := strings.Split(opt, "=") 517 518 if len(optSlice) <= 1 || optSlice[1] == "" { 519 return "", "", OptionError{"--" + optSlice[0], "", ERROR_WRONG_FORMAT} 520 } 521 522 return optSlice[0], strings.Join(optSlice[1:], "="), nil 523 } 524 525 if opts.full[opt] != nil { 526 return opt, "", nil 527 } 528 529 return "", "", OptionError{"--" + opt, "", ERROR_UNSUPPORTED} 530 } 531 532 func (opts *Options) parseShortOption(opt string) (string, string, error) { 533 if strings.Contains(opt, "=") { 534 optSlice := strings.Split(opt, "=") 535 536 if len(optSlice) <= 1 || optSlice[1] == "" { 537 return "", "", OptionError{"-" + optSlice[0], "", ERROR_WRONG_FORMAT} 538 } 539 540 optName := optSlice[0] 541 542 if opts.short[optName] == "" { 543 return "", "", OptionError{"-" + optName, "", ERROR_UNSUPPORTED} 544 } 545 546 return opts.short[optName], strings.Join(optSlice[1:], "="), nil 547 } 548 549 if opts.short[opt] == "" { 550 return "", "", OptionError{"-" + opt, "", ERROR_UNSUPPORTED} 551 } 552 553 return opts.short[opt], "", nil 554 } 555 556 func (opts *Options) prepare() { 557 for _, v := range opts.full { 558 // String is default type 559 if v.Type == STRING && v.Value != nil { 560 v.Type = guessType(v.Value) 561 } 562 } 563 } 564 565 func (opts *Options) validate() []error { 566 var errorList []error 567 568 for n, v := range opts.full { 569 if !isSupportedType(v.Value) { 570 errorList = append(errorList, OptionError{n, "", ERROR_UNSUPPORTED_VALUE}) 571 } 572 573 if v.Required && v.Value == nil { 574 errorList = append(errorList, OptionError{n, "", ERROR_REQUIRED_NOT_SET}) 575 } 576 577 if v.Conflicts != "" { 578 conflicts := parseOptionsList(v.Conflicts) 579 580 for _, c := range conflicts { 581 if opts.Has(c.Long) && opts.Has(n) { 582 errorList = append(errorList, OptionError{n, c.Long, ERROR_CONFLICT}) 583 } 584 } 585 } 586 587 if v.Bound != "" { 588 bound := parseOptionsList(v.Bound) 589 590 for _, b := range bound { 591 if !opts.Has(b.Long) && opts.Has(n) { 592 errorList = append(errorList, OptionError{n, b.Long, ERROR_BOUND_NOT_SET}) 593 } 594 } 595 } 596 } 597 598 return errorList 599 } 600 601 // ////////////////////////////////////////////////////////////////////////////////// // 602 603 func initOptions(opts *Options) { 604 opts.full = make(Map) 605 opts.short = make(map[string]string) 606 opts.initialized = true 607 } 608 609 func parseName(name string) optionName { 610 na := strings.Split(name, ":") 611 612 if len(na) == 1 { 613 return optionName{na[0], ""} 614 } 615 616 return optionName{na[1], na[0]} 617 } 618 619 func parseOptionsList(list string) []optionName { 620 var result []optionName 621 622 for _, a := range strings.Split(list, " ") { 623 result = append(result, parseName(a)) 624 } 625 626 return result 627 } 628 629 func updateOption(opt *V, name, value string) error { 630 switch opt.Type { 631 case STRING, MIXED: 632 return updateStringOption(opt, value) 633 634 case BOOL: 635 return updateBooleanOption(opt) 636 637 case FLOAT: 638 return updateFloatOption(name, opt, value) 639 640 case INT: 641 return updateIntOption(name, opt, value) 642 } 643 644 return fmt.Errorf("Option --%s has unsupported type", parseName(name).Long) 645 } 646 647 func updateStringOption(opt *V, value string) error { 648 if opt.set && opt.Mergeble { 649 opt.Value = opt.Value.(string) + " " + value 650 } else { 651 opt.Value = value 652 opt.set = true 653 } 654 655 return nil 656 } 657 658 func updateBooleanOption(opt *V) error { 659 opt.Value = true 660 opt.set = true 661 662 return nil 663 } 664 665 func updateFloatOption(name string, opt *V, value string) error { 666 floatValue, err := strconv.ParseFloat(value, 64) 667 668 if err != nil { 669 return OptionError{"--" + name, "", ERROR_WRONG_FORMAT} 670 } 671 672 var resultFloat float64 673 674 if opt.Min != opt.Max { 675 resultFloat = betweenFloat(floatValue, opt.Min, opt.Max) 676 } else { 677 resultFloat = floatValue 678 } 679 680 if opt.set && opt.Mergeble { 681 opt.Value = opt.Value.(float64) + resultFloat 682 } else { 683 opt.Value = resultFloat 684 opt.set = true 685 } 686 687 return nil 688 } 689 690 func updateIntOption(name string, opt *V, value string) error { 691 intValue, err := strconv.Atoi(value) 692 693 if err != nil { 694 return OptionError{"--" + name, "", ERROR_WRONG_FORMAT} 695 } 696 697 var resultInt int 698 699 if opt.Min != opt.Max { 700 resultInt = betweenInt(intValue, int(opt.Min), int(opt.Max)) 701 } else { 702 resultInt = intValue 703 } 704 705 if opt.set && opt.Mergeble { 706 opt.Value = opt.Value.(int) + resultInt 707 } else { 708 opt.Value = resultInt 709 opt.set = true 710 } 711 712 return nil 713 } 714 715 func appendError(errList []error, err error) []error { 716 if err == nil { 717 return errList 718 } 719 720 return append(errList, err) 721 } 722 723 func betweenInt(val, min, max int) int { 724 switch { 725 case val < min: 726 return min 727 case val > max: 728 return max 729 default: 730 return val 731 } 732 } 733 734 func betweenFloat(val, min, max float64) float64 { 735 switch { 736 case val < min: 737 return min 738 case val > max: 739 return max 740 default: 741 return val 742 } 743 } 744 745 func isSupportedType(v interface{}) bool { 746 switch v.(type) { 747 case nil, string, bool, int, float64: 748 return true 749 } 750 751 return false 752 } 753 754 func guessType(v interface{}) int { 755 switch v.(type) { 756 case string: 757 return STRING 758 case bool: 759 return BOOL 760 case int: 761 return INT 762 case float64: 763 return FLOAT 764 } 765 766 return STRING 767 } 768 769 // ////////////////////////////////////////////////////////////////////////////////// // 770 771 func (e OptionError) Error() string { 772 switch e.Type { 773 default: 774 return fmt.Sprintf("Option %s is not supported", e.Option) 775 case ERROR_EMPTY_VALUE: 776 return fmt.Sprintf("Non-boolean option %s is empty", e.Option) 777 case ERROR_REQUIRED_NOT_SET: 778 return fmt.Sprintf("Required option %s is not set", e.Option) 779 case ERROR_WRONG_FORMAT: 780 return fmt.Sprintf("Option %s has wrong format", e.Option) 781 case ERROR_OPTION_IS_NIL: 782 return fmt.Sprintf("Struct for option %s is nil", e.Option) 783 case ERROR_DUPLICATE_LONGNAME, ERROR_DUPLICATE_SHORTNAME: 784 return fmt.Sprintf("Option %s defined 2 or more times", e.Option) 785 case ERROR_NO_NAME: 786 return "Some option does not have a name" 787 case ERROR_CONFLICT: 788 return fmt.Sprintf("Option %s conflicts with option %s", e.Option, e.BoundOption) 789 case ERROR_BOUND_NOT_SET: 790 return fmt.Sprintf("Option %s must be defined with option %s", e.BoundOption, e.Option) 791 case ERROR_UNSUPPORTED_VALUE: 792 return fmt.Sprintf("Option %s contains unsupported default value", e.Option) 793 } 794 } 795 796 // ////////////////////////////////////////////////////////////////////////////////// //