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 }