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  // ////////////////////////////////////////////////////////////////////////////////// //