code.cestus.io/tools/fabricator@v0.4.3/pkg/ff/parse.go (about)

     1  package ff
     2  
     3  import (
     4  	"bufio"
     5  	"flag"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"strings"
    10  )
    11  
    12  // FlagSet interface defines required minimum set of functions required for
    13  // parsing parameters.
    14  //
    15  // The standard library flag.FlagSet implements this interface. FromPflag
    16  // function is also provided in this package which can be used to adapt
    17  // pflag.FlagSet to FlagSet.
    18  type FlagSet interface {
    19  	Parse(arguments []string) error
    20  	Visit(fn func(*flag.Flag))
    21  	VisitAll(fn func(*flag.Flag))
    22  	Set(name, value string) error
    23  	Lookup(name string) *flag.Flag
    24  }
    25  
    26  // Parse the flags in the flag set from the provided (presumably commandline)
    27  // args. Additional options may be provided to parse from a config file and/or
    28  // environment variables in that priority order.
    29  func Parse(fs FlagSet, args []string, options ...Option) error {
    30  	var c Context
    31  	for _, option := range options {
    32  		option(&c)
    33  	}
    34  
    35  	// First priority: commandline flags (explicit user preference).
    36  	if err := fs.Parse(args); err != nil {
    37  		return fmt.Errorf("error parsing commandline args: %w", err)
    38  	}
    39  
    40  	provided := map[string]bool{}
    41  	fs.Visit(func(f *flag.Flag) {
    42  		provided[f.Name] = true
    43  	})
    44  
    45  	// Second priority: environment variables (session).
    46  	if parseEnv := c.envVarPrefix != "" || c.envVarNoPrefix; parseEnv {
    47  		var visitErr error
    48  		fs.VisitAll(func(f *flag.Flag) {
    49  			if visitErr != nil {
    50  				return
    51  			}
    52  
    53  			if provided[f.Name] {
    54  				return
    55  			}
    56  
    57  			var key string
    58  			key = strings.ToUpper(f.Name)
    59  			key = envVarReplacer.Replace(key)
    60  			key = maybePrefix(key, c.envVarNoPrefix, c.envVarPrefix)
    61  
    62  			value := os.Getenv(key)
    63  			if value == "" {
    64  				return
    65  			}
    66  
    67  			for _, v := range maybeSplit(value, c.envVarSplit) {
    68  				if err := fs.Set(f.Name, v); err != nil {
    69  					visitErr = fmt.Errorf("error setting flag %q from env var %q: %w", f.Name, key, err)
    70  					return
    71  				}
    72  			}
    73  		})
    74  		if visitErr != nil {
    75  			return fmt.Errorf("error parsing env vars: %w", visitErr)
    76  		}
    77  	}
    78  
    79  	fs.Visit(func(f *flag.Flag) {
    80  		provided[f.Name] = true
    81  	})
    82  
    83  	// Third priority: config file (host).
    84  	if c.configFile == "" && c.configFileFlagName != "" {
    85  		if f := fs.Lookup(c.configFileFlagName); f != nil {
    86  			c.configFile = f.Value.String()
    87  		}
    88  	}
    89  
    90  	if parseConfig := c.configFile != "" && c.configFileParser != nil; parseConfig {
    91  		f, err := os.Open(c.configFile)
    92  		switch {
    93  		case err == nil:
    94  			defer f.Close()
    95  			if err := c.configFileParser(f, func(name, value string) error {
    96  				if provided[name] {
    97  					return nil
    98  				}
    99  
   100  				defined := fs.Lookup(name) != nil
   101  				switch {
   102  				case !defined && c.ignoreUndefined:
   103  					return nil
   104  				case !defined && !c.ignoreUndefined:
   105  					return fmt.Errorf("config file flag %q not defined in flag set", name)
   106  				}
   107  
   108  				if err := fs.Set(name, value); err != nil {
   109  					return fmt.Errorf("error setting flag %q from config file: %w", name, err)
   110  				}
   111  
   112  				return nil
   113  			}); err != nil {
   114  				return err
   115  			}
   116  
   117  		case os.IsNotExist(err) && c.allowMissingConfigFile:
   118  			// no problem
   119  
   120  		default:
   121  			return err
   122  		}
   123  	}
   124  
   125  	fs.Visit(func(f *flag.Flag) {
   126  		provided[f.Name] = true
   127  	})
   128  
   129  	return nil
   130  }
   131  
   132  // Context contains private fields used during parsing.
   133  type Context struct {
   134  	configFile             string
   135  	configFileFlagName     string
   136  	configFileParser       ConfigFileParser
   137  	allowMissingConfigFile bool
   138  	envVarPrefix           string
   139  	envVarNoPrefix         bool
   140  	envVarSplit            string
   141  	ignoreUndefined        bool
   142  }
   143  
   144  // Option controls some aspect of Parse behavior.
   145  type Option func(*Context)
   146  
   147  // WithConfigFile tells Parse to read the provided filename as a config file.
   148  // Requires WithConfigFileParser, and overrides WithConfigFileFlag.
   149  // Because config files should generally be user-specifiable, this option
   150  // should be rarely used. Prefer WithConfigFileFlag.
   151  func WithConfigFile(filename string) Option {
   152  	return func(c *Context) {
   153  		c.configFile = filename
   154  	}
   155  }
   156  
   157  // WithConfigFileFlag tells Parse to treat the flag with the given name as a
   158  // config file. Requires WithConfigFileParser, and is overridden by
   159  // WithConfigFile.
   160  //
   161  // To specify a default config file, provide it as the default value of the
   162  // corresponding flag -- and consider also using the WithAllowMissingConfigFile
   163  // option.
   164  func WithConfigFileFlag(flagname string) Option {
   165  	return func(c *Context) {
   166  		c.configFileFlagName = flagname
   167  	}
   168  }
   169  
   170  // WithConfigFileParser tells Parse how to interpret the config file provided
   171  // via WithConfigFile or WithConfigFileFlag.
   172  func WithConfigFileParser(p ConfigFileParser) Option {
   173  	return func(c *Context) {
   174  		c.configFileParser = p
   175  	}
   176  }
   177  
   178  // WithAllowMissingConfigFile tells Parse to permit the case where a config file
   179  // is specified but doesn't exist. By default, missing config files result in an
   180  // error.
   181  func WithAllowMissingConfigFile(allow bool) Option {
   182  	return func(c *Context) {
   183  		c.allowMissingConfigFile = allow
   184  	}
   185  }
   186  
   187  // WithEnvVarPrefix tells Parse to try to set flags from environment variables
   188  // with the given prefix. Flag names are matched to environment variables with
   189  // the given prefix, followed by an underscore, followed by the capitalized flag
   190  // names, with separator characters like periods or hyphens replaced with
   191  // underscores. By default, flags are not set from environment variables at all.
   192  func WithEnvVarPrefix(prefix string) Option {
   193  	return func(c *Context) {
   194  		c.envVarPrefix = prefix
   195  	}
   196  }
   197  
   198  // WithEnvVarNoPrefix tells Parse to try to set flags from environment variables
   199  // without any specific prefix. Flag names are matched to environment variables
   200  // by capitalizing the flag name, and replacing separator characters like
   201  // periods or hyphens with underscores. By default, flags are not set from
   202  // environment variables at all.
   203  func WithEnvVarNoPrefix() Option {
   204  	return func(c *Context) {
   205  		c.envVarNoPrefix = true
   206  	}
   207  }
   208  
   209  // WithEnvVarSplit tells Parse to split environment variables on the given
   210  // delimiter, and to make a call to Set on the corresponding flag with each
   211  // split token.
   212  func WithEnvVarSplit(delimiter string) Option {
   213  	return func(c *Context) {
   214  		c.envVarSplit = delimiter
   215  	}
   216  }
   217  
   218  // WithIgnoreUndefined tells Parse to ignore undefined flags that it encounters
   219  // in config files. By default, if Parse encounters an undefined flag in a
   220  // config file, it will return an error. Note that this setting does not apply
   221  // to undefined flags passed as arguments.
   222  func WithIgnoreUndefined(ignore bool) Option {
   223  	return func(c *Context) {
   224  		c.ignoreUndefined = ignore
   225  	}
   226  }
   227  
   228  // ConfigFileParser interprets the config file represented by the reader
   229  // and calls the set function for each parsed flag pair.
   230  type ConfigFileParser func(r io.Reader, set func(name, value string) error) error
   231  
   232  // PlainParser is a parser for config files in an extremely simple format. Each
   233  // line is tokenized as a single key/value pair. The first whitespace-delimited
   234  // token in the line is interpreted as the flag name, and all remaining tokens
   235  // are interpreted as the value. Any leading hyphens on the flag name are
   236  // ignored.
   237  func PlainParser(r io.Reader, set func(name, value string) error) error {
   238  	s := bufio.NewScanner(r)
   239  	for s.Scan() {
   240  		line := strings.TrimSpace(s.Text())
   241  		if line == "" {
   242  			continue // skip empties
   243  		}
   244  
   245  		if line[0] == '#' {
   246  			continue // skip comments
   247  		}
   248  
   249  		var (
   250  			name  string
   251  			value string
   252  			index = strings.IndexRune(line, ' ')
   253  		)
   254  		if index < 0 {
   255  			name, value = line, "true" // boolean option
   256  		} else {
   257  			name, value = line[:index], strings.TrimSpace(line[index:])
   258  		}
   259  
   260  		if i := strings.Index(value, " #"); i >= 0 {
   261  			value = strings.TrimSpace(value[:i])
   262  		}
   263  
   264  		if err := set(name, value); err != nil {
   265  			return fmt.Errorf("PlainParser set: %w", err)
   266  		}
   267  	}
   268  	return nil
   269  }
   270  
   271  var envVarReplacer = strings.NewReplacer(
   272  	"-", "_",
   273  	".", "_",
   274  	"/", "_",
   275  )
   276  
   277  func maybePrefix(key string, noPrefix bool, prefix string) string {
   278  	if noPrefix {
   279  		return key
   280  	}
   281  	return strings.ToUpper(prefix) + "_" + key
   282  }
   283  
   284  func maybeSplit(value, split string) []string {
   285  	if split == "" {
   286  		return []string{value}
   287  	}
   288  	return strings.Split(value, split)
   289  }