github.com/haraldrudell/parl@v0.4.176/pflags/arg-parser.go (about)

     1  /*
     2  © 2020–present Harald Rudell <harald.rudell@gmail.com> (https://haraldrudell.github.io/haraldrudell/)
     3  ISC License
     4  */
     5  
     6  // Package pflags provides declarative options and a string-slice option type.
     7  package pflags
     8  
     9  import (
    10  	"flag"
    11  	"os"
    12  
    13  	"github.com/haraldrudell/parl"
    14  	"github.com/haraldrudell/parl/pstrings"
    15  )
    16  
    17  const (
    18  	// the os.Args value of debug option: “-debug”
    19  	DebugOption = "-" + DebugOptionName
    20  	// the name of debug option “debug”
    21  	DebugOptionName = "debug"
    22  )
    23  
    24  const (
    25  	offFlagFalse = false
    26  	offFlagTrue  = true
    27  )
    28  
    29  // ArgParser invokes [flag.Parse] with off-flags support.
    30  // pflags package compared to flags package:
    31  //   - ability to use declarative options
    32  //   - ability to read default option values from yaml configuration files
    33  //   - support for multiple-strings option value
    34  //   - support for unary off-flags, “-no-flag” options
    35  //   - map or list of visited options
    36  type ArgParser struct {
    37  	optionsList []OptionData
    38  	usage       func()
    39  }
    40  
    41  // NewArgParser returns an options-parser with off-flags support
    42  func NewArgParser(optionsList []OptionData, usage func()) (argParser *ArgParser) {
    43  	return &ArgParser{
    44  		optionsList: optionsList,
    45  		usage:       usage,
    46  	}
    47  }
    48  
    49  // Parse invokes [flag.Parse] after providing optionsList and usage to flag package
    50  //   - -no-flagname flags are inverted before and after
    51  func (a *ArgParser) Parse() {
    52  
    53  	// options have not been parsed yet, so verbose state cannot be determined
    54  	//	- if first option is “-debug”, it’s debug
    55  	if len(os.Args) > 1 && os.Args[1] == DebugOption {
    56  		var _, defaultsMap = OptionValues(a.optionsList)
    57  		parl.Log("option defaults: %v", defaultsMap)
    58  		parl.Log("os.args[1:]: %s", pstrings.QuoteList(os.Args[1:]))
    59  		defer func() {
    60  			var effectiveValueMap, _ = OptionValues(a.optionsList)
    61  			parl.Log("resulting option values: %v", effectiveValueMap)
    62  		}()
    63  	}
    64  
    65  	flag.Usage = a.usage
    66  
    67  	// booleanOffList is a list of pointers to the effective values of off-flags
    68  	//	- on return from [flag.Parse], these values are inverted
    69  	var booleanOffList []*bool
    70  	defer a.parseEnd(&booleanOffList)
    71  
    72  	// provide optionData list to flag package
    73  	omLen := len(a.optionsList)
    74  	for i := 0; i < omLen; i++ {
    75  		option := &a.optionsList[i]
    76  
    77  		// flag package does not support -no-flagname off-flags. pflags does implement off-flags
    78  		//	- an off-flag is a boolean flag with default value true
    79  		//	- — any time the flag occurs on the command-line its value is set to false
    80  		//	- — off-flags typically have names with leading “no”
    81  		//	- — [OptionData.Name] is like “no-stdin”
    82  		//	- — on command-line is provided like “-no-stdin”
    83  		//	- the only option-type allowed by [flags.Parse] to not have an argument is a boolean flag
    84  		//	- — therefore, off-flags must be plain boolean flags
    85  		//	- but boolean flags have default value false, and are set to true on occurrence
    86  		//	- — therefore, identify all boolean flags with default value true
    87  		//	- — prior to invoking [flags.Parse], set their default value to false
    88  		//	- — on occurrence, [flags.Parse] will set their value to true
    89  		//	- — on return from [flags.Parse], invert the off-flags’ effective values
    90  		//	- — if an off-flag did not occur, [flags.Parse] set its effective value to false, and result is true
    91  		//	- — if an off-flag did occur, [flags.Parse] set its effective value to true, and result is false
    92  
    93  		// is this option’s effective value type bool?
    94  		if boolp, ok := option.P.(*bool); ok {
    95  			// is the default value for this boolean flag true?
    96  			if value, ok := option.Value.(bool); ok && value {
    97  
    98  				// this is a flag of type bool with default value true
    99  				//	- this is used for off-flags -no-flagname
   100  				//	- retain a pointer to the off-flag option’s effective value
   101  				booleanOffList = append(booleanOffList, boolp)
   102  
   103  				// in a copy of optionData, set off-option default value to false
   104  				var o = *option
   105  				o.Value = offFlagFalse // have default value false
   106  				option = &o            // use the copy
   107  			}
   108  		}
   109  		option.AddOption()
   110  	}
   111  
   112  	// flag.Parse uses os.Args[1:]
   113  	flag.Parse()
   114  }
   115  
   116  // iterate over the -no-flagname off-flag options
   117  //   - if the value is false, the flag was not provided, result should be true
   118  //   - if the value is true, the flag was provided, the result should be false
   119  func (a *ArgParser) parseEnd(booleanOffList *[]*bool) {
   120  	for _, boolp := range *booleanOffList {
   121  		// invert effective value
   122  		if *boolp {
   123  			*boolp = offFlagFalse
   124  		} else {
   125  			*boolp = offFlagTrue
   126  		}
   127  	}
   128  }