...

Source file src/github.com/peterbourgon/ff/v3/parse.go

Documentation: github.com/peterbourgon/ff/v3

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

View as plain text