├── .github └── workflows │ └── main.yml ├── LICENSE ├── README.md ├── flagutil.go ├── flagutil_test.go ├── go.mod ├── go.sum ├── options.go ├── parse ├── args │ ├── args.go │ └── args_test.go ├── doc.go ├── env │ ├── env.go │ └── env_test.go ├── file │ ├── file.go │ ├── file_test.go │ ├── json │ │ ├── json.go │ │ └── json_test.go │ ├── toml │ │ ├── toml.go │ │ └── toml_test.go │ └── yaml │ │ ├── yaml.go │ │ └── yaml_test.go ├── flagset.go ├── pargs │ ├── posix.go │ └── posix_test.go ├── parse.go ├── parse_test.go ├── prompt │ └── prompt.go └── testutil │ └── testutil.go └── value.go /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [ master ] 5 | pull_request: 6 | branches: [ master ] 7 | jobs: 8 | test: 9 | strategy: 10 | matrix: 11 | os: [ ubuntu-latest, macos-latest, windows-latest ] 12 | go: [ '1.14' ] 13 | runs-on: ${{ matrix.os }} 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | 18 | - name: Setup Go 19 | uses: actions/setup-go@v2 20 | with: 21 | go-version: ${{ matrix.go }} 22 | 23 | - name: Go Env 24 | run: | 25 | go env 26 | 27 | - name: Test 28 | run: | 29 | go test -v -race -cover ./... 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Sergey Kamardin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flagutil 2 | 3 | [![GoDoc][godoc-image]][godoc-url] 4 | [![CI][ci-badge]][ci-url] 5 | 6 | > A library to populate [`flag.FlagSet`][flagSet] from various sources. 7 | 8 | # Features 9 | 10 | - Uses standard `flag` package 11 | - Structured configuration 12 | - Reads values from multiple sources 13 | - Ability to write your own parser 14 | 15 | # Why 16 | 17 | Defining your program parameters and reading their values should be as simple 18 | as possible. There is no reason to use some library or even monstrous framework 19 | instead of standard [flag][flag] package. 20 | 21 | There is [Configuration in Go][article] article, which describes in detail the 22 | reasons of creating this library. 23 | 24 | # Available parsers 25 | 26 | Note that it is very easy to implement your own [Parser][parser]. 27 | 28 | At the moment these parsers are already implemented: 29 | - [Flag][flag-syntax] syntax arguments parser 30 | - [Posix][posix] program arguments syntax parser 31 | - Environment variables parser 32 | - Prompt interactive parser 33 | - File parsers: 34 | - json 35 | - yaml 36 | - toml 37 | 38 | # Custom help message 39 | 40 | It is possible to print custom help message which may include, for example, 41 | names of the environment variables used by env parser. See the 42 | `WithCustomUsage()` parse option. 43 | 44 | Custom usage currently looks like this: 45 | 46 | ``` 47 | Usage of test: 48 | $TEST_FOO, --foo 49 | bool 50 | bool flag description (default false) 51 | 52 | $TEST_BAR, --bar 53 | int 54 | int flag description (default 42) 55 | ``` 56 | 57 | # Usage 58 | 59 | A simple example could be like this: 60 | 61 | ```go 62 | package main 63 | 64 | import ( 65 | "flag" 66 | 67 | "github.com/gobwas/flagutil" 68 | "github.com/gobwas/flagutil/parse/pargs" 69 | "github.com/gobwas/flagutil/parse/file/json" 70 | ) 71 | 72 | func main() { 73 | flags := flag.NewFlagSet("my-app", flag.ExitOnError) 74 | 75 | port := flag.Int(&port, 76 | "port", "port", 77 | "port to bind to", 78 | ) 79 | 80 | // This flag will be required by the file.Parser below. 81 | _ = flags.String( 82 | "config", "/etc/app/config.json", 83 | "path to configuration file", 84 | ) 85 | 86 | flagutil.Parse(flags, 87 | // Use posix options syntax instead of `flag` – just to illustrate that 88 | // it is possible. 89 | flagutil.WithParser(&pargs.Parser{ 90 | Args: os.Args[1:], 91 | }), 92 | 93 | // Then lookup flag values among environment. 94 | flagutil.WithParser(&env.Parser{ 95 | Prefix: "MY_APP_", 96 | }), 97 | 98 | // Finally lookup for "config" flag value and try to interpret its 99 | // value as a path to json configuration file. 100 | flagutil.WithParser( 101 | &file.Parser{ 102 | Lookup: file.LookupFlag(flags, "config"), 103 | Syntax: new(json.Syntax), 104 | }, 105 | // Don't allow to setup "config" flag from file. 106 | flagutil.WithStashName("config"), 107 | ), 108 | ) 109 | 110 | // Work with received values. 111 | } 112 | ``` 113 | 114 | ## Subsets 115 | 116 | `flagutil` provides ability to define so called flag subsets: 117 | 118 | ```go 119 | package main 120 | 121 | import ( 122 | "flag" 123 | 124 | "github.com/gobwas/flagutil" 125 | "github.com/gobwas/flagutil/parse/pargs" 126 | "github.com/gobwas/flagutil/parse/file/json" 127 | ) 128 | 129 | func main() { 130 | flags := flag.NewFlagSet("my-app", flag.ExitOnError) 131 | 132 | port := flag.Int(&port, 133 | "port", "port", 134 | "port to bind to", 135 | ) 136 | 137 | // Define parameters for some third-party library. 138 | var endpoint string 139 | flagutil.Subset(flags, "database", func(sub *flag.FlagSet) { 140 | sub.StringVar(&endpoint, 141 | "endpoint", "localhost", 142 | "database endpoint to connect to" 143 | ) 144 | }) 145 | 146 | flagutil.Parse(flags, 147 | flagutil.WithParser(&pargs.Parser{ 148 | Args: os.Args[1:], 149 | }), 150 | flagutil.WithParser( 151 | &file.Parser{ 152 | Lookup: file.PathLookup("/etc/my-app/config.json"), 153 | Syntax: new(json.Syntax), 154 | }, 155 | ), 156 | ) 157 | 158 | // Work with received values. 159 | } 160 | ``` 161 | 162 | The configuration file may look as follows: 163 | 164 | ```json 165 | { 166 | "port": 4050, 167 | 168 | "database": { 169 | "endpoint": "localhost:5432", 170 | } 171 | } 172 | ``` 173 | 174 | And, if you want to override, say, database endpoint, you can execute your 175 | program as follows: 176 | 177 | ```bash 178 | $ app --database.endpoint 4055 179 | ``` 180 | 181 | ## Allowing name collisions 182 | 183 | It's rare, but still possible, when you want to receive single flag value from 184 | multiple places in code. To avoid panics with "flag redefined" reason you can 185 | (if you _really_ need to) _merge_ two flag values into single by using 186 | `flagutil.Merge()` function: 187 | 188 | ```go 189 | var ( 190 | s0 string 191 | s1 string 192 | ) 193 | flag.StringVar(&s0, 194 | "foo", "default", 195 | "foo flag usage", 196 | ) 197 | flagutil.MergeInto(flag.CommandLine, func(safe *flag.FlagSet) { 198 | safe.StringVar(&s1, 199 | "foo", "", 200 | "foo flag another usage", // This usage will be joined with previous. 201 | ) 202 | }) 203 | 204 | // After parsing, s0 and s1 will be filled with single `-foo` flag value. 205 | // If value is not provided, both s0 and s1 will have its default values (which 206 | // may be _different_). 207 | ``` 208 | 209 | 210 | # Conventions and limitations 211 | 212 | Any structure from parsed configuration is converted into a pairs of a flat key 213 | and a value. Keys are flattened recursively until there is no such flag defined 214 | within `flag.FlagSet`. 215 | 216 | > Keys flattening happens just as two keys concatenation with `.` as a > 217 | > delimiter. 218 | 219 | There are three scenarios when the flag was found: 220 | 221 | 1) If value is a mapping or an object, then its key-value pairs are 222 | concatenated with `:` as a delimiter and are passed to the `flag.Value.Set()` 223 | in appropriate number of calls. 224 | 225 | 2) If value is an array, then its items are passed to the `flag.Value.Set()` in 226 | appropriate number of calls. 227 | 228 | 3) In other way, `flag.Value.Set()` will be called once with value as is. 229 | 230 | > Note that for any type of values the `flag.Value.String()` method is never 231 | > used to access the "real" value – only for defaults when printing help 232 | > message. To provide "real" value implementations must satisfy `flag.Getter` 233 | > interface. 234 | 235 | Suppose you have this json configuration: 236 | 237 | ```json 238 | { 239 | "foo": { 240 | "bar": "1", 241 | "baz": "2" 242 | } 243 | } 244 | ``` 245 | 246 | If you define `foo.bar` flag, you will receive `"1"` in a single call to its 247 | `flag.Value.Set()` method. No surprise here. But if you define `foo` flag, then 248 | its `flag.Value.Set()` will be called twice with `"bar:1"` and `"baz:2"`. 249 | 250 | The same thing happens with slices: 251 | 252 | ```json 253 | { 254 | "foo": [ 255 | "bar", 256 | "baz" 257 | ] 258 | } 259 | ``` 260 | 261 | Your `foo`'s `flag.Value.Set()` will be called twice with `"bar"` and `"baz"`. 262 | 263 | This still allows you to use command line arguments to override or declare 264 | parameter complex values: 265 | 266 | ```bash 267 | $ app --slice 1 --slice 2 --slice 3 --map foo:bar --map bar:baz 268 | ``` 269 | 270 | # Misc 271 | 272 | Creation of this library was greatly inspired by [peterburgon/ff][ff] – and I 273 | wouldn't write `flagutil` if I didn't met some design disagreement with it. 274 | 275 | 276 | [parser]: https://godoc.org/github.com/gobwas/flagutil#Parser 277 | [flag]: https://golang.org/pkg/flag 278 | [flagSet]: https://golang.org/pkg/flag#FlagSet 279 | [flag-syntax]: https://golang.org/pkg/flag/#hdr-Command_line_flag_syntax 280 | [article]: https://gbws.io/articles/configuration-in-go 281 | [godoc-image]: https://godoc.org/github.com/gobwas/flagutil?status.svg 282 | [godoc-url]: https://godoc.org/github.com/gobwas/flagutil 283 | [posix]: https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html 284 | [ff]: https://github.com/peterbourgon/ff 285 | [ci-badge]: https://github.com/gobwas/flagutil/workflows/CI/badge.svg 286 | [ci-url]: https://github.com/gobwas/flagutil/actions?query=workflow%3ACI 287 | -------------------------------------------------------------------------------- /flagutil.go: -------------------------------------------------------------------------------- 1 | package flagutil 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "os" 10 | "reflect" 11 | "strings" 12 | "time" 13 | 14 | "github.com/gobwas/flagutil/parse" 15 | ) 16 | 17 | var SetSeparator = "." 18 | 19 | type Parser interface { 20 | Parse(context.Context, parse.FlagSet) error 21 | } 22 | 23 | type ParserFunc func(context.Context, parse.FlagSet) error 24 | 25 | func (fn ParserFunc) Parse(ctx context.Context, fs parse.FlagSet) error { 26 | return fn(ctx, fs) 27 | } 28 | 29 | type Printer interface { 30 | Name(context.Context, parse.FlagSet) (func(*flag.Flag, func(string)), error) 31 | } 32 | 33 | type PrinterFunc func(context.Context, parse.FlagSet) (func(*flag.Flag, func(string)), error) 34 | 35 | func (fn PrinterFunc) Name(ctx context.Context, fs parse.FlagSet) (func(*flag.Flag, func(string)), error) { 36 | return fn(ctx, fs) 37 | } 38 | 39 | type parser struct { 40 | Parser 41 | stash func(*flag.Flag) bool 42 | ignoreUndefined bool 43 | allowResetSpecified bool 44 | } 45 | 46 | type config struct { 47 | parsers []*parser 48 | parserOptions []ParserOption 49 | customUsage bool 50 | unquoteUsageMode UnquoteUsageMode 51 | } 52 | 53 | func buildConfig(opts []ParseOption) config { 54 | c := config{ 55 | unquoteUsageMode: UnquoteDefault, 56 | } 57 | for _, opt := range opts { 58 | opt.setupParseConfig(&c) 59 | } 60 | for _, opt := range c.parserOptions { 61 | for _, p := range c.parsers { 62 | opt.setupParserConfig(p) 63 | } 64 | } 65 | return c 66 | } 67 | 68 | func Parse(ctx context.Context, flags *flag.FlagSet, opts ...ParseOption) (err error) { 69 | c := buildConfig(opts) 70 | 71 | fs := parse.NewFlagSet(flags) 72 | for _, p := range c.parsers { 73 | parse.NextLevel(fs) 74 | parse.Stash(fs, p.stash) 75 | parse.IgnoreUndefined(fs, p.ignoreUndefined) 76 | parse.AllowResetSpecified(fs, p.allowResetSpecified) 77 | 78 | if err = p.Parse(ctx, fs); err != nil { 79 | if errors.Is(err, flag.ErrHelp) { 80 | _ = printUsageMaybe(ctx, &c, flags) 81 | } 82 | if err != nil { 83 | err = fmt.Errorf("flagutil: parse error: %w", err) 84 | } 85 | switch flags.ErrorHandling() { 86 | case flag.ContinueOnError: 87 | return err 88 | case flag.ExitOnError: 89 | if !errors.Is(err, flag.ErrHelp) { 90 | fmt.Fprintf(flags.Output(), "%v\n", err) 91 | } 92 | os.Exit(2) 93 | case flag.PanicOnError: 94 | panic(err.Error()) 95 | } 96 | } 97 | } 98 | return nil 99 | } 100 | 101 | // PrintDefaults prints parsers aware usage message to flags.Output(). 102 | func PrintDefaults(ctx context.Context, flags *flag.FlagSet, opts ...ParseOption) error { 103 | c := buildConfig(opts) 104 | return printDefaults(ctx, &c, flags) 105 | } 106 | 107 | func printUsageMaybe(ctx context.Context, c *config, flags *flag.FlagSet) error { 108 | if !c.customUsage && flags.Usage != nil { 109 | flags.Usage() 110 | return nil 111 | } 112 | if name := flags.Name(); name == "" { 113 | fmt.Fprintf(flags.Output(), "Usage:\n") 114 | } else { 115 | fmt.Fprintf(flags.Output(), "Usage of %s:\n", name) 116 | } 117 | return printDefaults(ctx, c, flags) 118 | } 119 | 120 | type UnquoteUsageMode uint8 121 | 122 | const ( 123 | UnquoteNothing UnquoteUsageMode = 1 << iota >> 1 124 | UnquoteQuoted 125 | UnquoteInferType 126 | UnquoteClean 127 | 128 | UnquoteDefault UnquoteUsageMode = UnquoteQuoted | UnquoteInferType 129 | ) 130 | 131 | func (m UnquoteUsageMode) String() string { 132 | switch m { 133 | case UnquoteNothing: 134 | return "UnquoteNothing" 135 | case UnquoteQuoted: 136 | return "UnquoteQuoted" 137 | case UnquoteInferType: 138 | return "UnquoteInferType" 139 | case UnquoteClean: 140 | return "UnquoteClean" 141 | case UnquoteDefault: 142 | return "UnquoteDefault" 143 | default: 144 | return "" 145 | } 146 | } 147 | 148 | func (m UnquoteUsageMode) has(x UnquoteUsageMode) bool { 149 | return m&x != 0 150 | } 151 | 152 | func printDefaults(ctx context.Context, c *config, flags *flag.FlagSet) (err error) { 153 | fs := parse.NewFlagSet(flags) 154 | 155 | var hasNameFunc bool 156 | nameFunc := make([]func(*flag.Flag, func(string)), len(c.parsers)) 157 | for i := len(c.parsers) - 1; i >= 0; i-- { 158 | if p, ok := c.parsers[i].Parser.(Printer); ok { 159 | hasNameFunc = true 160 | nameFunc[i], err = p.Name(ctx, fs) 161 | if err != nil { 162 | return 163 | } 164 | } 165 | } 166 | 167 | var buf bytes.Buffer 168 | flags.VisitAll(func(f *flag.Flag) { 169 | n, _ := buf.WriteString(" ") 170 | for i := len(c.parsers) - 1; i >= 0; i-- { 171 | fn := nameFunc[i] 172 | if fn == nil { 173 | continue 174 | } 175 | if stash := c.parsers[i].stash; stash != nil && stash(f) { 176 | continue 177 | } 178 | fn(f, func(name string) { 179 | if buf.Len() > n { 180 | buf.WriteString(", ") 181 | } 182 | buf.WriteString(name) 183 | }) 184 | } 185 | if buf.Len() == n { 186 | // No name has been given. 187 | // Two cases are possible: no Printer implementation among parsers; 188 | // or some parser intentionally filtered out this flag. 189 | if hasNameFunc { 190 | buf.Reset() 191 | return 192 | } 193 | buf.WriteString(f.Name) 194 | } 195 | name, usage := unquoteUsage(c.unquoteUsageMode, f) 196 | if len(name) > 0 { 197 | buf.WriteString("\n \t") 198 | buf.WriteString(name) 199 | } 200 | value := defValue(f) 201 | buf.WriteString("\n \t") 202 | if len(usage) > 0 { 203 | buf.WriteString(strings.ReplaceAll(usage, "\n", "\n \t")) 204 | if len(value) > 0 { 205 | buf.WriteString(" (") 206 | } 207 | } 208 | if len(value) > 0 { 209 | buf.WriteString("default ") 210 | buf.WriteString(defValue(f)) 211 | if len(usage) > 0 { 212 | buf.WriteString(")") 213 | } 214 | } 215 | 216 | buf.WriteByte('\n') 217 | buf.WriteByte('\n') 218 | buf.WriteTo(flags.Output()) 219 | }) 220 | 221 | return nil 222 | } 223 | 224 | func defValue(f *flag.Flag) string { 225 | var x interface{} 226 | g, ok := f.Value.(flag.Getter) 227 | if ok { 228 | x = g.Get() 229 | } 230 | if def := f.DefValue; def != "" { 231 | if _, ok := x.(string); ok { 232 | def = `"` + def + `"` 233 | } 234 | return def 235 | } 236 | v := reflect.ValueOf(x) 237 | repeat: 238 | if !v.IsValid() { 239 | return "" 240 | } 241 | if v.Kind() == reflect.Ptr { 242 | v = v.Elem() 243 | goto repeat 244 | } 245 | switch v.Kind() { 246 | case reflect.String: 247 | return `""` 248 | case 249 | reflect.Slice, 250 | reflect.Array: 251 | return `[]` 252 | case 253 | reflect.Struct, 254 | reflect.Map: 255 | return `{}` 256 | } 257 | return "" 258 | } 259 | 260 | // defValueStd returns default value as it does std lib. 261 | // NOTE: this one is unused. 262 | func defValueStd(f *flag.Flag) string { 263 | t := reflect.TypeOf(f.Value) 264 | var z reflect.Value 265 | if t.Kind() == reflect.Ptr { 266 | z = reflect.New(t.Elem()) 267 | } else { 268 | z = reflect.Zero(t) 269 | } 270 | zero := z.Interface().(flag.Value).String() 271 | if v := f.DefValue; v != zero { 272 | return v 273 | } 274 | return "" 275 | } 276 | 277 | // unquoteUsage is the same as flag.UnquoteUsage() with exception that it does 278 | // not infer type of the flag value. 279 | func unquoteUsage(m UnquoteUsageMode, f *flag.Flag) (name, usage string) { 280 | if m == UnquoteNothing { 281 | return "", f.Usage 282 | } 283 | u := f.Usage 284 | i := strings.IndexByte(u, '`') 285 | if i == -1 { 286 | if m.has(UnquoteInferType) { 287 | return inferType(f), f.Usage 288 | } 289 | return "", u 290 | } 291 | j := strings.IndexByte(u[i+1:], '`') 292 | if j == -1 { 293 | if m.has(UnquoteInferType) { 294 | return inferType(f), f.Usage 295 | } 296 | return "", u 297 | } 298 | j += i + 1 299 | 300 | switch { 301 | case m.has(UnquoteQuoted): 302 | name = u[i+1 : j] 303 | case m.has(UnquoteInferType): 304 | name = inferType(f) 305 | } 306 | 307 | prefix := u[:i] 308 | suffix := u[j+1:] 309 | switch { 310 | case m.has(UnquoteClean): 311 | usage = "" + 312 | strings.TrimRight(prefix, " ") + 313 | " " + 314 | strings.TrimLeft(suffix, " ") 315 | 316 | case m.has(UnquoteQuoted): 317 | usage = prefix + name + suffix 318 | 319 | default: 320 | usage = f.Usage 321 | } 322 | 323 | return 324 | } 325 | 326 | func inferType(f *flag.Flag) string { 327 | if f.Value == nil { 328 | return "?" 329 | } 330 | if isBoolFlag(f) { 331 | return "bool" 332 | } 333 | 334 | var x interface{} 335 | if g, ok := f.Value.(flag.Getter); ok { 336 | x = g.Get() 337 | } else { 338 | x = f.Value 339 | } 340 | v := reflect.ValueOf(x) 341 | 342 | repeat: 343 | if !v.IsValid() { 344 | return "" 345 | } 346 | switch v.Type() { 347 | case reflect.TypeOf(time.Duration(0)): 348 | return "duration" 349 | } 350 | switch v.Kind() { 351 | case 352 | reflect.Interface, 353 | reflect.Ptr: 354 | 355 | v = v.Elem() 356 | goto repeat 357 | 358 | case 359 | reflect.String: 360 | return "string" 361 | case 362 | reflect.Float32, 363 | reflect.Float64: 364 | return "float" 365 | case 366 | reflect.Int, 367 | reflect.Int8, 368 | reflect.Int16, 369 | reflect.Int32, 370 | reflect.Int64: 371 | return "int" 372 | case 373 | reflect.Uint, 374 | reflect.Uint8, 375 | reflect.Uint16, 376 | reflect.Uint32, 377 | reflect.Uint64: 378 | return "uint" 379 | case 380 | reflect.Slice, 381 | reflect.Array: 382 | return "list" 383 | case 384 | reflect.Map: 385 | return "object" 386 | } 387 | return "" 388 | } 389 | 390 | // Subset registers new flag subset with given prefix within given flag 391 | // superset. It calls setup function to let caller register needed flags within 392 | // created subset. 393 | func Subset(super *flag.FlagSet, prefix string, setup func(sub *flag.FlagSet)) (err error) { 394 | sub := flag.NewFlagSet(prefix, 0) 395 | setup(sub) 396 | sub.VisitAll(func(f *flag.Flag) { 397 | name := prefix + "." + f.Name 398 | if super.Lookup(name) != nil { 399 | if err == nil { 400 | err = fmt.Errorf( 401 | "flag %q already exists in a super set", 402 | name, 403 | ) 404 | // TODO: should we panic here if super has PanicOnError? 405 | } 406 | return 407 | } 408 | super.Var(f.Value, name, f.Usage) 409 | }) 410 | return 411 | } 412 | 413 | func isBoolFlag(f *flag.Flag) bool { 414 | return isBoolValue(f.Value) 415 | } 416 | func isBoolValue(v flag.Value) bool { 417 | x, ok := v.(interface { 418 | IsBoolFlag() bool 419 | }) 420 | return ok && x.IsBoolFlag() 421 | } 422 | 423 | type joinVar struct { 424 | flags []*flag.Flag 425 | } 426 | 427 | // MergeUsage specifies way of joining two different flag usage strings. 428 | var MergeUsage = func(name string, usage0, usage1 string) string { 429 | return usage0 + " / " + usage1 430 | } 431 | 432 | // MergeInto merges new flagset into superset and resolves any name collisions. 433 | // It calls setup function to let caller register needed flags within subset 434 | // before they are merged into the superset. 435 | // 436 | // If name of the flag defined in the subset already present in a superset, 437 | // then subset flag is merged into superset's one. That is, flag will remain in 438 | // the superset, but setting its value will make both parameters filled with 439 | // received value. 440 | // 441 | // Description of each flag (if differ) is joined by MergeUsage(). 442 | // 443 | // Note that default values (and initial values of where flag.Value points to) 444 | // are kept untouched and may differ if no value is set during parsing phase. 445 | func MergeInto(super *flag.FlagSet, setup func(*flag.FlagSet)) { 446 | fs := flag.NewFlagSet("", flag.ContinueOnError) 447 | setup(fs) 448 | fs.VisitAll(func(next *flag.Flag) { 449 | prev := super.Lookup(next.Name) 450 | if prev == nil { 451 | super.Var(next.Value, next.Name, next.Usage) 452 | return 453 | } 454 | *prev = *CombineFlags(prev, next) 455 | }) 456 | } 457 | 458 | // Copy defines all flags from src with dst. 459 | // It panics on any flag name collision. 460 | func Copy(dst, src *flag.FlagSet) { 461 | src.VisitAll(func(f *flag.Flag) { 462 | dst.Var(f.Value, f.Name, f.Usage) 463 | }) 464 | } 465 | 466 | // CombineSets combines given sets into a third one. 467 | // Every collided flags are combined into third one in a way that setting value 468 | // to it sets value of both original flags. 469 | func CombineSets(fs0, fs1 *flag.FlagSet) *flag.FlagSet { 470 | // TODO: join Name(). 471 | super := flag.NewFlagSet("", flag.ContinueOnError) 472 | fs0.VisitAll(func(f0 *flag.Flag) { 473 | var v flag.Value 474 | f1 := fs1.Lookup(f0.Name) 475 | if f1 != nil { 476 | // Same flag exists in fs1 flag set. 477 | f0 = CombineFlags(f0, f1) 478 | } 479 | v = OverrideSet(f0.Value, func(value string) (err error) { 480 | err = fs0.Set(f0.Name, value) 481 | if err != nil { 482 | return 483 | } 484 | if f1 == nil { 485 | return 486 | } 487 | err = fs1.Set(f1.Name, value) 488 | if err != nil { 489 | return 490 | } 491 | return nil 492 | }) 493 | super.Var(v, f0.Name, f0.Usage) 494 | }) 495 | fs1.VisitAll(func(f1 *flag.Flag) { 496 | if super.Lookup(f1.Name) != nil { 497 | // Already combined. 498 | return 499 | } 500 | v := OverrideSet(f1.Value, func(value string) error { 501 | return fs1.Set(f1.Name, value) 502 | }) 503 | super.Var(v, f1.Name, f1.Usage) 504 | }) 505 | return super 506 | } 507 | 508 | // CombineFlags combines given flags into a third one. Setting value of 509 | // returned flag will cause both given flags change their values as well. 510 | // However, flag sets of both flags will not be aware that the flags were set. 511 | // 512 | // Description of each flag (if differ) is joined by MergeUsage(). 513 | func CombineFlags(f0, f1 *flag.Flag) *flag.Flag { 514 | if f0.Name != f1.Name { 515 | panic(fmt.Sprintf( 516 | "flagutil: can't combine flags with different names: %q vs %q", 517 | f0.Name, f1.Name, 518 | )) 519 | } 520 | r := flag.Flag{ 521 | Name: f0.Name, 522 | Value: valuePair{f0.Value, f1.Value}, 523 | Usage: mergeUsage(f0.Name, f0.Usage, f1.Usage), 524 | } 525 | // This is how flag.FlagSet() does it in its Var() method. 526 | r.DefValue = r.Value.String() 527 | return &r 528 | } 529 | 530 | // SetActual makes flag look like it has been set within flag set. 531 | // If flag set doesn't has flag with given SetActual() does nothing. 532 | // Original value of found flag remains untouched, so it is safe to use with 533 | // flags that accumulate values of multiple Set() calls. 534 | func SetActual(fs *flag.FlagSet, name string) { 535 | f := fs.Lookup(name) 536 | if f == nil { 537 | return 538 | } 539 | orig := f.Value 540 | defer func() { 541 | f.Value = orig 542 | }() 543 | var didSet bool 544 | f.Value = value{ 545 | doSet: func(s string) error { 546 | didSet = s == "dummy" 547 | return nil 548 | }, 549 | } 550 | fs.Set(name, "dummy") 551 | if !didSet { 552 | panic("flagutil: set actual didn't work well") 553 | } 554 | } 555 | 556 | // LinkFlag links dst to be updated when src value is set. 557 | // It panics if any of the given names doesn't exist in fs. 558 | // 559 | // Note that it caches the both src and dst flag.Value pointers internally, so 560 | // it is possible to link src to dst and dst to src without infinite recursion. 561 | // However, if any of the src or dst flag value get overwritten after 562 | // LinkFlag() call, created link will not work properly anymore. 563 | func LinkFlag(fs *flag.FlagSet, src, dst string) { 564 | srcFlag := fs.Lookup(src) 565 | if srcFlag == nil { 566 | panic(fmt.Sprintf( 567 | "flagutil: link flag: source flag %q must exist", 568 | src, 569 | )) 570 | } 571 | dstFlag := fs.Lookup(dst) 572 | if dstFlag == nil { 573 | panic(fmt.Sprintf( 574 | "flagutil: link flag: destination flag %q must exist", 575 | dst, 576 | )) 577 | } 578 | var ( 579 | srcValue = srcFlag.Value 580 | dstValue = dstFlag.Value 581 | ) 582 | srcFlag.Value = OverrideSet(srcFlag.Value, func(s string) error { 583 | err := srcValue.Set(s) 584 | if err != nil { 585 | return err 586 | } 587 | return dstValue.Set(s) 588 | }) 589 | } 590 | 591 | func mergeUsage(name, s0, s1 string) string { 592 | switch { 593 | case s0 == "": 594 | return s1 595 | case s1 == "": 596 | return s0 597 | case s0 == s1: 598 | return s0 599 | default: 600 | return MergeUsage(name, s0, s1) 601 | } 602 | } 603 | -------------------------------------------------------------------------------- /flagutil_test.go: -------------------------------------------------------------------------------- 1 | package flagutil 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "math/rand" 9 | "regexp" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/google/go-cmp/cmp" 14 | 15 | "github.com/gobwas/flagutil/parse" 16 | ) 17 | 18 | func TestSetActual(t *testing.T) { 19 | fs := flag.NewFlagSet(t.Name(), flag.PanicOnError) 20 | fs.String("flag", "default", "usage") 21 | 22 | f := fs.Lookup("flag") 23 | f.Value = OverrideSet(f.Value, func(string) error { 24 | t.Fatalf("unexpected value change") 25 | return nil 26 | }) 27 | 28 | mustNotBeActual(t, fs, "flag") 29 | SetActual(fs, "flag") 30 | mustBeActual(t, fs, "flag") 31 | } 32 | 33 | type fullParser struct { 34 | Parser 35 | Printer 36 | } 37 | 38 | func TestPrintUsage(t *testing.T) { 39 | var buf bytes.Buffer 40 | fs := flag.NewFlagSet("test", flag.PanicOnError) 41 | fs.SetOutput(&buf) 42 | var ( 43 | foo string 44 | bar bool 45 | baz int 46 | ) 47 | fs.StringVar(&foo, "foo", "", "`custom` description here") 48 | fs.BoolVar(&bar, "bar", bar, "description here") 49 | fs.IntVar(&baz, "baz", baz, "description here") 50 | 51 | PrintDefaults(context.Background(), fs, 52 | WithParser( 53 | &fullParser{ 54 | Parser: nil, 55 | Printer: PrinterFunc(func(_ context.Context, fs parse.FlagSet) (func(*flag.Flag, func(string)), error) { 56 | return func(f *flag.Flag, it func(string)) { 57 | it("MUST-IGNORE-" + strings.ToUpper(f.Name)) 58 | }, nil 59 | }), 60 | }, 61 | WithStashPrefix("f"), 62 | WithStashName("bar"), 63 | ), 64 | WithParser( 65 | &fullParser{ 66 | Parser: nil, 67 | Printer: PrinterFunc(func(_ context.Context, fs parse.FlagSet) (func(*flag.Flag, func(string)), error) { 68 | return func(f *flag.Flag, it func(string)) { 69 | it("-" + string(f.Name[0])) 70 | it("-" + f.Name) 71 | }, nil 72 | }), 73 | }, 74 | WithStashName("foo"), 75 | ), 76 | WithParser(&fullParser{ 77 | Parser: nil, 78 | Printer: PrinterFunc(func(_ context.Context, fs parse.FlagSet) (func(*flag.Flag, func(string)), error) { 79 | return func(f *flag.Flag, it func(string)) { 80 | it("$" + strings.ToUpper(f.Name)) 81 | }, nil 82 | }), 83 | }), 84 | WithStashRegexp(regexp.MustCompile(".*baz.*")), 85 | ) 86 | exp := "" + 87 | " $BAR, -b, -bar\n" + 88 | " \tbool\n" + 89 | " \tdescription here (default false)\n" + 90 | "\n" + 91 | " $FOO\n" + // -foo is ignored. 92 | " \tcustom\n" + 93 | " \tcustom description here (default \"\")\n" + 94 | "\n" 95 | if act := buf.String(); act != exp { 96 | t.Error(cmp.Diff(exp, act)) 97 | } 98 | } 99 | 100 | func TestUnquoteUsage(t *testing.T) { 101 | type expMode map[UnquoteUsageMode][2]string 102 | for _, test := range []struct { 103 | name string 104 | flag *flag.Flag 105 | modes expMode 106 | }{ 107 | { 108 | flag: &flag.Flag{ 109 | Usage: "foo `bar` baz", 110 | }, 111 | modes: expMode{ 112 | UnquoteNothing: [2]string{ 113 | "", "foo `bar` baz", 114 | }, 115 | UnquoteQuoted: [2]string{ 116 | "bar", "foo bar baz", 117 | }, 118 | UnquoteClean: [2]string{ 119 | "", "foo baz", 120 | }, 121 | }, 122 | }, 123 | { 124 | flag: stringFlag("", "", "some kind of `hello` message"), 125 | modes: expMode{ 126 | UnquoteDefault: [2]string{ 127 | "hello", "some kind of hello message", 128 | }, 129 | UnquoteInferType: [2]string{ 130 | "string", "some kind of `hello` message", 131 | }, 132 | UnquoteInferType | UnquoteClean: [2]string{ 133 | "string", "some kind of message", 134 | }, 135 | }, 136 | }, 137 | { 138 | flag: stringFlag("", "", "no quoted info"), 139 | modes: expMode{ 140 | UnquoteQuoted: [2]string{ 141 | "", "no quoted info", 142 | }, 143 | UnquoteInferType: [2]string{ 144 | "string", "no quoted info", 145 | }, 146 | }, 147 | }, 148 | } { 149 | t.Run(test.name, func(t *testing.T) { 150 | for mode, exp := range test.modes { 151 | t.Run(mode.String(), func(t *testing.T) { 152 | actName, actUsage := unquoteUsage(mode, test.flag) 153 | if expName := exp[0]; actName != expName { 154 | t.Errorf("unexpected name:\n%s", cmp.Diff(expName, actName)) 155 | } 156 | if expUsage := exp[1]; actUsage != expUsage { 157 | t.Errorf("unexpected usage:\n%s", cmp.Diff(expUsage, actUsage)) 158 | } 159 | }) 160 | } 161 | }) 162 | } 163 | } 164 | 165 | func ExampleMerge() { 166 | fs := flag.NewFlagSet("superset", flag.PanicOnError) 167 | var ( 168 | s0 string 169 | s1 string 170 | ) 171 | // Setup flag in a superset. 172 | fs.StringVar(&s0, 173 | "foo", "42", 174 | "some flag usage here", 175 | ) 176 | // Now we need to setup same flag (probably from some different place). 177 | // Setting it up again in a superset will cause error. 178 | MergeInto(fs, func(sub *flag.FlagSet) { 179 | // Notice that default value of this flag is different. 180 | // However, it will be discarded in favour of default value from superset. 181 | sub.StringVar(&s1, 182 | "foo", "84", 183 | "another flag usage here", 184 | ) 185 | }) 186 | 187 | fmt.Println(s0) 188 | fmt.Println(s1) 189 | 190 | fs.Set("foo", "34") 191 | fmt.Println(s0) 192 | fmt.Println(s1) 193 | 194 | flag := fs.Lookup("foo") 195 | fmt.Println(flag.Usage) 196 | 197 | // Output: 198 | // 42 199 | // 84 200 | // 34 201 | // 34 202 | // some flag usage here / another flag usage here 203 | } 204 | 205 | func ExampleMerge_different_types() { 206 | fs := flag.NewFlagSet("superset", flag.PanicOnError) 207 | var ( 208 | s string 209 | i int 210 | ) 211 | fs.StringVar(&s, 212 | "foo", "42", 213 | "some flag usage here", 214 | ) 215 | MergeInto(fs, func(sub *flag.FlagSet) { 216 | sub.IntVar(&i, 217 | "foo", 84, 218 | "another flag usage here", 219 | ) 220 | }) 221 | fs.Set("foo", "34") 222 | fmt.Println(s) 223 | fmt.Println(i) 224 | // Output: 225 | // 34 226 | // 34 227 | } 228 | 229 | func TestMerge(t *testing.T) { 230 | fs := flag.NewFlagSet(t.Name(), flag.PanicOnError) 231 | var ( 232 | s0 string 233 | s1 string 234 | s2 string 235 | ) 236 | fs.StringVar(&s0, 237 | "foo", "bar", 238 | "superset usage", 239 | ) 240 | MergeInto(fs, func(fs *flag.FlagSet) { 241 | fs.StringVar(&s1, "foo", "baz", "subset1 usage") 242 | }) 243 | MergeInto(fs, func(fs *flag.FlagSet) { 244 | fs.StringVar(&s2, "foo", "baq", "subset2 usage") 245 | }) 246 | if s0 == s1 || s1 == s2 { 247 | t.Fatalf("strings are equal: %q vs %q vs %q", s0, s1, s2) 248 | } 249 | if err := fs.Set("foo", "42"); err != nil { 250 | t.Fatal(err) 251 | } 252 | if s0 != "42" { 253 | t.Fatalf("unexpected value after Set(): %q", s0) 254 | } 255 | if s0 != s1 || s1 != s2 { 256 | t.Fatalf("strings are not equal: %q vs %q vs %q", s0, s1, s2) 257 | } 258 | 259 | f := fs.Lookup("foo") 260 | if s := f.Value.String(); s != s0 { 261 | t.Fatalf("String() is %q; want %q", s, s0) 262 | } 263 | if act, exp := f.Usage, "superset usage / subset1 usage / subset2 usage"; act != exp { 264 | t.Fatalf("unexpected usage: %q; want %q", act, exp) 265 | } 266 | } 267 | 268 | func TestCombineSets(t *testing.T) { 269 | var ( 270 | nameInBoth = "both" 271 | nameInFirst = "first" 272 | nameInSecond = "second" 273 | nameUnknown = "whoa" 274 | ) 275 | var ( 276 | fs0 = flag.NewFlagSet("FlagSet#0", flag.ContinueOnError) 277 | fs1 = flag.NewFlagSet("FlagSet#1", flag.ContinueOnError) 278 | ) 279 | fs0.String(nameInFirst, "first-default", "") 280 | fs0.String(nameInBoth, "both-default-0", "") 281 | fs1.String(nameInBoth, "both-default-1", "") 282 | fs1.String(nameInSecond, "second-default", "") 283 | 284 | fs := CombineSets(fs0, fs1) 285 | 286 | mustNotBeDefined(t, fs, nameUnknown) 287 | mustBeEqualTo(t, fs, nameInFirst, "first-default") 288 | mustBeEqualTo(t, fs, nameInSecond, "second-default") 289 | mustBeEqualTo(t, fs, nameInBoth, "") 290 | 291 | mustNotSet(t, fs, nameUnknown, "want error") 292 | 293 | mustSet(t, fs, nameInFirst, "first") 294 | mustBeEqualTo(t, fs, nameInFirst, "first") 295 | mustBeEqualTo(t, fs0, nameInFirst, "first") 296 | 297 | mustSet(t, fs, nameInSecond, "second") 298 | mustBeEqualTo(t, fs, nameInSecond, "second") 299 | mustBeEqualTo(t, fs1, nameInSecond, "second") 300 | 301 | mustSet(t, fs, nameInBoth, "both") 302 | mustBeEqualTo(t, fs, nameInBoth, "both") 303 | mustBeEqualTo(t, fs0, nameInBoth, "both") 304 | mustBeEqualTo(t, fs1, nameInBoth, "both") 305 | } 306 | 307 | func mustNotSet(t *testing.T, fs *flag.FlagSet, name, value string) { 308 | if err := fs.Set(name, value); err == nil { 309 | t.Fatalf( 310 | "want error on setting flag %q value to %q: %v", 311 | name, value, err, 312 | ) 313 | } 314 | } 315 | 316 | func mustSet(t *testing.T, fs *flag.FlagSet, name, value string) { 317 | if err := fs.Set(name, value); err != nil { 318 | t.Fatalf("can't set flag %q value to %q: %v", name, value, err) 319 | } 320 | } 321 | 322 | func mustBeEqualTo(t *testing.T, fs *flag.FlagSet, name, value string) { 323 | mustBeDefined(t, fs, name) 324 | if act, exp := fs.Lookup(name).Value.String(), value; act != exp { 325 | t.Fatalf("flag %q value is %q; want %q", name, act, exp) 326 | } 327 | } 328 | 329 | func mustNotBeDefined(t *testing.T, fs *flag.FlagSet, name string) { 330 | if fs.Lookup(name) != nil { 331 | t.Fatalf("want flag %q to not be present in set", name) 332 | } 333 | } 334 | 335 | func mustBeDefined(t *testing.T, fs *flag.FlagSet, name string) { 336 | if fs.Lookup(name) == nil { 337 | t.Fatalf("want flag %q to be present in set", name) 338 | } 339 | } 340 | 341 | func mustBeActual(t *testing.T, fs *flag.FlagSet, name string) { 342 | if !isActual(fs, name) { 343 | t.Fatalf("want flag %q to be actual in set", name) 344 | } 345 | } 346 | 347 | func mustNotBeActual(t *testing.T, fs *flag.FlagSet, name string) { 348 | if isActual(fs, name) { 349 | t.Fatalf("want flag %q to not be actual in set", name) 350 | } 351 | } 352 | 353 | func isActual(fs *flag.FlagSet, name string) (actual bool) { 354 | fs.Visit(func(f *flag.Flag) { 355 | if f.Name == name { 356 | actual = true 357 | } 358 | }) 359 | return 360 | } 361 | 362 | func TestCombineFlags(t *testing.T) { 363 | for _, test := range []struct { 364 | name string 365 | flags [2]*flag.Flag 366 | exp *flag.Flag 367 | panic bool 368 | }{ 369 | { 370 | name: "different names", 371 | flags: [2]*flag.Flag{ 372 | stringFlag("foo", "def", "desc#0"), 373 | stringFlag("bar", "def", "desc#1"), 374 | }, 375 | panic: true, 376 | }, 377 | { 378 | name: "different default values", 379 | flags: [2]*flag.Flag{ 380 | stringFlag("foo", "def#0", "desc#0"), 381 | stringFlag("foo", "def#1", "desc#1"), 382 | }, 383 | exp: stringFlag("foo", "", "desc#0 / desc#1"), 384 | }, 385 | { 386 | name: "basic", 387 | flags: [2]*flag.Flag{ 388 | stringFlag("foo", "def", "desc#0"), 389 | stringFlag("foo", "def", "desc#1"), 390 | }, 391 | exp: stringFlag("foo", "def", "desc#0 / desc#1"), 392 | }, 393 | { 394 | name: "basic", 395 | flags: [2]*flag.Flag{ 396 | stringFlag("foo", "def", "desc#0"), 397 | stringFlag("foo", "", "desc#1"), 398 | }, 399 | exp: stringFlag("foo", "", "desc#0 / desc#1"), 400 | }, 401 | } { 402 | t.Run(test.name, func(t *testing.T) { 403 | type flagOrPanic struct { 404 | flag *flag.Flag 405 | panic interface{} 406 | } 407 | done := make(chan flagOrPanic) 408 | go func() { 409 | defer func() { 410 | if p := recover(); p != nil { 411 | done <- flagOrPanic{ 412 | panic: p, 413 | } 414 | } 415 | }() 416 | done <- flagOrPanic{ 417 | flag: CombineFlags(test.flags[0], test.flags[1]), 418 | } 419 | }() 420 | x := <-done 421 | if !test.panic && x.panic != nil { 422 | t.Fatalf("panic() recovered: %s", x.panic) 423 | } 424 | if test.panic { 425 | if x.panic == nil { 426 | t.Fatalf("want panic; got nothing") 427 | } 428 | return 429 | } 430 | opts := []cmp.Option{ 431 | cmp.Transformer("Value", func(v flag.Value) string { 432 | return v.String() 433 | }), 434 | } 435 | if act, exp := x.flag, test.exp; !cmp.Equal(act, exp, opts...) { 436 | t.Errorf("unexpected flag:\n%s", cmp.Diff(exp, act, opts...)) 437 | } 438 | exp := fmt.Sprintf("%x", rand.Int63()) 439 | if err := x.flag.Value.Set(exp); err != nil { 440 | t.Fatalf("unexpected Set() error: %v", err) 441 | } 442 | for _, f := range test.flags { 443 | assertEquals(t, f, exp) 444 | } 445 | }) 446 | } 447 | } 448 | 449 | func TestLinkFlag(t *testing.T) { 450 | for _, test := range []struct { 451 | name string 452 | flags [2]*flag.Flag 453 | links [2]string 454 | }{ 455 | { 456 | name: "basic", 457 | flags: [2]*flag.Flag{ 458 | stringFlag("foo", "def#0", "desc#0"), 459 | stringFlag("bar", "def#1", "desc#1"), 460 | }, 461 | links: [2]string{"foo", "bar"}, 462 | }, 463 | } { 464 | t.Run(test.name, func(t *testing.T) { 465 | fs := flag.NewFlagSet("", flag.PanicOnError) 466 | for _, f := range test.flags { 467 | if f != nil { 468 | fs.Var(f.Value, f.Name, f.Usage) 469 | } 470 | } 471 | LinkFlag(fs, test.links[0], test.links[1]) 472 | 473 | // First, test that setting for src flag affects dst flag. 474 | exp := fmt.Sprintf("%x", rand.Int63()) 475 | fs.Set(test.links[0], exp) 476 | for _, n := range test.links { 477 | if f := fs.Lookup(n); f != nil { 478 | assertEquals(t, f, exp) 479 | } 480 | } 481 | // Second, test that setting dst flag doesn't affect src flag. 482 | nonExp := fmt.Sprintf("%x", rand.Int63()) 483 | fs.Set(test.flags[1].Name, nonExp) 484 | assertEquals(t, test.flags[0], exp) // Still the same. 485 | assertEquals(t, test.flags[1], nonExp) // Updated. 486 | }) 487 | } 488 | } 489 | 490 | func assertEquals(t *testing.T, f *flag.Flag, exp string) { 491 | if act := f.Value.String(); act != exp { 492 | t.Errorf( 493 | "unexpected flag %q value: %s; want %s", 494 | f.Name, act, exp, 495 | ) 496 | } 497 | } 498 | 499 | func stringFlag(name, def, desc string) *flag.Flag { 500 | fs := flag.NewFlagSet("", flag.PanicOnError) 501 | fs.String(name, def, desc) 502 | f := fs.Lookup(name) 503 | return f 504 | } 505 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gobwas/flagutil 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/BurntSushi/toml v0.3.1 7 | github.com/gobwas/prompt v0.2.2 8 | github.com/google/go-cmp v0.4.0 9 | gopkg.in/yaml.v2 v2.2.8 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/gobwas/prompt v0.2.2 h1:ri/sgOnLzXAwm2nUI7/jM0kJ24MVMob/01+n5OaZmJ8= 4 | github.com/gobwas/prompt v0.2.2/go.mod h1:UVO2T+b2GvmPMwT07n3gxtzepiAu6N3GP177DjDN1l8= 5 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 6 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 7 | golang.org/x/sys v0.0.0-20200610111108-226ff32320da h1:bGb80FudwxpeucJUjPYJXuJ8Hk91vNtfvrymzwiei38= 8 | golang.org/x/sys v0.0.0-20200610111108-226ff32320da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 10 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 12 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 13 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 14 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 15 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package flagutil 2 | 3 | import ( 4 | "flag" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | type ParseOption interface { 10 | setupParseConfig(*config) 11 | } 12 | 13 | type ParserOption interface { 14 | setupParserConfig(*parser) 15 | } 16 | 17 | func WithParseOptions(opts ...ParseOption) ParseOptionFunc { 18 | return ParseOptionFunc(func(c *config) { 19 | for _, opt := range opts { 20 | opt.setupParseConfig(c) 21 | } 22 | }) 23 | } 24 | 25 | type ParseOptionFunc func(*config) 26 | 27 | func (fn ParseOptionFunc) setupParseConfig(c *config) { fn(c) } 28 | 29 | type ParserOptionFunc func(*parser) 30 | 31 | func (fn ParserOptionFunc) setupParserConfig(p *parser) { fn(p) } 32 | 33 | type ParseOrParserOptionFunc func(*config, *parser) 34 | 35 | func (fn ParseOrParserOptionFunc) setupParseConfig(c *config) { fn(c, nil) } 36 | func (fn ParseOrParserOptionFunc) setupParserConfig(p *parser) { fn(nil, p) } 37 | 38 | func stashFunc(check func(*flag.Flag) bool) (opt ParseOrParserOptionFunc) { 39 | return ParseOrParserOptionFunc(func(c *config, p *parser) { 40 | if c != nil { 41 | c.parserOptions = append(c.parserOptions, opt) 42 | return 43 | } 44 | prev := p.stash 45 | p.stash = func(f *flag.Flag) bool { 46 | if prev != nil && prev(f) { 47 | return true 48 | } 49 | return check(f) 50 | } 51 | }) 52 | } 53 | 54 | func WithStashName(name string) ParseOrParserOptionFunc { 55 | return stashFunc(func(f *flag.Flag) bool { 56 | return f.Name == name 57 | }) 58 | } 59 | 60 | func WithStashPrefix(prefix string) ParseOrParserOptionFunc { 61 | return stashFunc(func(f *flag.Flag) bool { 62 | return strings.HasPrefix(f.Name, prefix) 63 | }) 64 | } 65 | 66 | func WithStashRegexp(re *regexp.Regexp) ParseOrParserOptionFunc { 67 | return stashFunc(func(f *flag.Flag) bool { 68 | return re.MatchString(f.Name) 69 | }) 70 | } 71 | 72 | func WithResetSpecified() ParserOptionFunc { 73 | return ParserOptionFunc(func(p *parser) { 74 | p.allowResetSpecified = true 75 | }) 76 | } 77 | 78 | // WithParser returns a parse option and makes p to be used during Parse(). 79 | func WithParser(p Parser, opts ...ParserOption) ParseOptionFunc { 80 | x := &parser{ 81 | Parser: p, 82 | } 83 | for _, opt := range opts { 84 | opt.setupParserConfig(x) 85 | } 86 | return ParseOptionFunc(func(c *config) { 87 | c.parsers = append(c.parsers, x) 88 | }) 89 | } 90 | 91 | // WithIgnoreUndefined makes Parse() to not fail on setting undefined flag. 92 | func WithIgnoreUndefined() (opt ParseOrParserOptionFunc) { 93 | return ParseOrParserOptionFunc(func(c *config, p *parser) { 94 | switch { 95 | case c != nil: 96 | c.parserOptions = append(c.parserOptions, opt) 97 | case p != nil: 98 | p.ignoreUndefined = true 99 | } 100 | }) 101 | } 102 | 103 | func WithAllowResetSpecified() (opt ParseOrParserOptionFunc) { 104 | return ParseOrParserOptionFunc(func(c *config, p *parser) { 105 | switch { 106 | case c != nil: 107 | c.parserOptions = append(c.parserOptions, opt) 108 | case p != nil: 109 | p.allowResetSpecified = true 110 | } 111 | }) 112 | } 113 | 114 | // WithCustomUsage makes Parse() to ignore flag.FlagSet.Usage field when 115 | // receiving flag.ErrHelp error from some parser and print results of 116 | // flagutil.PrintDefaults() instead. 117 | func WithCustomUsage() ParseOptionFunc { 118 | return ParseOptionFunc(func(c *config) { 119 | c.customUsage = true 120 | }) 121 | } 122 | 123 | func WithUnquoteUsageMode(m UnquoteUsageMode) ParseOptionFunc { 124 | return ParseOptionFunc(func(c *config) { 125 | c.unquoteUsageMode = m 126 | }) 127 | } 128 | -------------------------------------------------------------------------------- /parse/args/args.go: -------------------------------------------------------------------------------- 1 | package args 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/gobwas/flagutil/parse" 10 | ) 11 | 12 | type Parser struct { 13 | Args []string 14 | 15 | fs parse.FlagSet 16 | pos int 17 | name string 18 | value string 19 | err error 20 | } 21 | 22 | func (p *Parser) Parse(_ context.Context, fs parse.FlagSet) error { 23 | p.reset(fs) 24 | for p.next() { 25 | if fs.Lookup(p.name) == nil && (p.name == "help" || p.name == "h") { 26 | return flag.ErrHelp 27 | } 28 | if err := fs.Set(p.name, p.value); err != nil { 29 | return err 30 | } 31 | } 32 | return p.err 33 | } 34 | 35 | func (p *Parser) NonFlagArgs() []string { 36 | if p.pos < len(p.Args) { 37 | return p.Args[p.pos:] 38 | } 39 | return nil 40 | } 41 | 42 | func (p *Parser) Name(_ context.Context, fs parse.FlagSet) (func(*flag.Flag, func(string)), error) { 43 | return func(f *flag.Flag, it func(string)) { 44 | it("-" + f.Name) 45 | }, nil 46 | } 47 | 48 | func (p *Parser) reset(fs parse.FlagSet) { 49 | p.fs = fs 50 | p.pos = 0 51 | p.err = nil 52 | } 53 | 54 | func (p *Parser) next() bool { 55 | if p.err != nil { 56 | return false 57 | } 58 | if p.pos >= len(p.Args) { 59 | return false 60 | } 61 | s := p.Args[p.pos] 62 | if len(s) < 2 || s[0] != '-' { 63 | return false 64 | } 65 | p.pos++ 66 | 67 | minuses := 1 68 | if s[1] == '-' { 69 | minuses = 2 70 | if len(s) == minuses { // "--" terminates all flags. 71 | return false 72 | } 73 | } 74 | name := s[minuses:] 75 | if name[0] == '-' || name[0] == '=' { 76 | p.fail("bad flag syntax: %s", s) 77 | return false 78 | } 79 | 80 | name, value, hasValue := split(name, '=') 81 | if !hasValue && p.pos < len(p.Args) { 82 | value = p.Args[p.pos] 83 | if len(value) == 0 || value[0] != '-' { 84 | // NOTE: this is NOT the same behaviour as for flag.Parse(). 85 | // flag.Parse() works well if we pass `-flag=true`, but not 86 | // if we pass `-flag true`. 87 | if p.isBoolFlag(name) { 88 | p.fail(""+ 89 | "ambiguous boolean flag -%[1]s value: can't guess whether "+ 90 | "the %[2]q is the flag value or the non-flag argument "+ 91 | "(consider using `-%[1]s=%[2]s` or `-%[1]s -- %[2]s`)", 92 | name, value, 93 | ) 94 | return false 95 | } 96 | hasValue = true 97 | p.pos++ 98 | } 99 | } 100 | if !hasValue && p.isBoolFlag(name) { 101 | value = "true" 102 | hasValue = true 103 | } 104 | if !hasValue { 105 | p.fail("flag needs an argument: -%s", name) 106 | return false 107 | } 108 | 109 | p.name = name 110 | p.value = value 111 | return true 112 | } 113 | 114 | func (p *Parser) isBoolFlag(name string) bool { 115 | f := p.fs.Lookup(name) 116 | if f == nil && (name == "help" || name == "h") { 117 | // Special case for help message request. 118 | return true 119 | } 120 | if f == nil { 121 | return false 122 | } 123 | return isBoolFlag(f) 124 | } 125 | 126 | func (p *Parser) fail(f string, args ...interface{}) { 127 | p.err = fmt.Errorf("args: %s", fmt.Sprintf(f, args...)) 128 | } 129 | 130 | func split(s string, sep byte) (a, b string, ok bool) { 131 | i := strings.IndexByte(s, sep) 132 | if i == -1 { 133 | return s, "", false 134 | } 135 | return s[:i], s[i+1:], true 136 | } 137 | 138 | func isBoolFlag(f *flag.Flag) bool { 139 | x, ok := f.Value.(interface { 140 | IsBoolFlag() bool 141 | }) 142 | return ok && x.IsBoolFlag() 143 | } 144 | -------------------------------------------------------------------------------- /parse/args/args_test.go: -------------------------------------------------------------------------------- 1 | package args 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | 9 | "github.com/gobwas/flagutil" 10 | "github.com/gobwas/flagutil/parse" 11 | "github.com/gobwas/flagutil/parse/testutil" 12 | ) 13 | 14 | var _ flagutil.Printer = new(Parser) 15 | 16 | func TestFlagsParseArgs(t *testing.T) { 17 | for _, test := range []struct { 18 | name string 19 | flags map[string]bool 20 | args []string 21 | expPairs [][2]string 22 | expArgs []string 23 | err bool 24 | }{ 25 | { 26 | name: "basic", 27 | flags: map[string]bool{ 28 | "a": true, 29 | "b": true, 30 | "c": true, 31 | "foo": false, 32 | "bar": false, 33 | }, 34 | args: []string{ 35 | "-a", 36 | "-b=true", 37 | "-c=false", 38 | "-foo", "value", 39 | "--bar", "value", 40 | "arg1", "arg2", "arg3", 41 | }, 42 | expPairs: [][2]string{ 43 | {"a", "true"}, 44 | {"b", "true"}, 45 | {"c", "false"}, 46 | {"foo", "value"}, 47 | {"bar", "value"}, 48 | }, 49 | expArgs: []string{ 50 | "arg1", "arg2", "arg3", 51 | }, 52 | }, 53 | { 54 | name: "flags termination", 55 | flags: map[string]bool{ 56 | "param": false, 57 | }, 58 | args: []string{ 59 | "-param", "value", 60 | "--", 61 | "arg1", "arg2", "arg3", 62 | }, 63 | expPairs: [][2]string{ 64 | {"param", "value"}, 65 | }, 66 | expArgs: []string{ 67 | "arg1", "arg2", "arg3", 68 | }, 69 | }, 70 | { 71 | name: "empty arg", 72 | flags: map[string]bool{ 73 | "param": false, 74 | }, 75 | args: []string{ 76 | "-param", 77 | "", 78 | }, 79 | expPairs: [][2]string{ 80 | {"param", ""}, 81 | }, 82 | }, 83 | { 84 | name: "empty bool arg", 85 | flags: map[string]bool{ 86 | "param": true, 87 | }, 88 | args: []string{ 89 | "-param", 90 | "", 91 | }, 92 | err: true, 93 | }, 94 | { 95 | name: "last bool arg", 96 | flags: map[string]bool{ 97 | "param1": false, 98 | "param2": true, 99 | }, 100 | args: []string{ 101 | "-param1", "value", 102 | "-param2", 103 | }, 104 | expPairs: [][2]string{ 105 | {"param1", "value"}, 106 | {"param2", "true"}, 107 | }, 108 | }, 109 | { 110 | name: "basic error", 111 | flags: map[string]bool{ 112 | "param": false, 113 | }, 114 | args: []string{ 115 | "-param", 116 | }, 117 | err: true, 118 | }, 119 | { 120 | name: "basic error", 121 | flags: map[string]bool{ 122 | "param": false, 123 | }, 124 | args: []string{ 125 | "--param", 126 | }, 127 | err: true, 128 | }, 129 | } { 130 | t.Run(test.name, func(t *testing.T) { 131 | fs := testutil.StubFlagSet{ 132 | IgnoreUndefined: true, 133 | } 134 | for name, isBool := range test.flags { 135 | if isBool { 136 | fs.AddBoolFlag(name, false) 137 | } else { 138 | fs.AddFlag(name, "") 139 | } 140 | } 141 | p := Parser{ 142 | Args: test.args, 143 | } 144 | err := p.Parse(context.Background(), &fs) 145 | if !test.err && err != nil { 146 | t.Fatalf("unexpected error: %v", err) 147 | } 148 | if test.err && err == nil { 149 | t.Fatalf("want error; got nothing") 150 | } 151 | if test.err { 152 | return 153 | } 154 | if exp, act := test.expPairs, fs.Pairs(); !cmp.Equal(act, exp) { 155 | t.Errorf( 156 | "unexpected set pairs:\n%s", 157 | cmp.Diff(exp, act), 158 | ) 159 | } 160 | if exp, act := test.expArgs, p.NonFlagArgs(); !cmp.Equal(act, exp) { 161 | t.Errorf( 162 | "unexpected non-flag arguments:\n%s", 163 | cmp.Diff(exp, act), 164 | ) 165 | } 166 | }) 167 | } 168 | } 169 | 170 | func TestArgs(t *testing.T) { 171 | testutil.TestParser(t, func(values testutil.Values, fs parse.FlagSet) error { 172 | p := Parser{ 173 | Args: marshal(values), 174 | } 175 | return p.Parse(context.Background(), fs) 176 | }) 177 | } 178 | 179 | func marshal(values testutil.Values) (args []string) { 180 | parse.Setup(values, parse.VisitorFunc{ 181 | SetFunc: func(name, value string) error { 182 | if value == "false" { 183 | args = append(args, "-"+name+"=false") 184 | return nil 185 | } 186 | args = append(args, "-"+name) 187 | if value != "true" { 188 | args = append(args, value) 189 | } 190 | return nil 191 | }, 192 | HasFunc: func(string) bool { 193 | return false 194 | }, 195 | }) 196 | return args 197 | } 198 | -------------------------------------------------------------------------------- /parse/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package parse provides tools for common flag parsing tasks. 3 | */ 4 | package parse 5 | -------------------------------------------------------------------------------- /parse/env/env.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "os" 7 | "strings" 8 | "sync" 9 | 10 | "github.com/gobwas/flagutil" 11 | "github.com/gobwas/flagutil/parse" 12 | ) 13 | 14 | var DefaultReplace = map[string]string{ 15 | "-": "_", 16 | } 17 | 18 | var DefaultSetSeparator = "__" 19 | 20 | type Parser struct { 21 | Prefix string 22 | SetSeparator string 23 | ListSeparator string 24 | Replace map[string]string 25 | 26 | LookupEnvFunc func(string) (string, bool) 27 | 28 | once sync.Once 29 | replacer *strings.Replacer 30 | } 31 | 32 | func (p *Parser) init() { 33 | p.once.Do(func() { 34 | separator := p.SetSeparator 35 | if separator == "" { 36 | separator = DefaultSetSeparator 37 | } 38 | replace := p.Replace 39 | if replace == nil { 40 | replace = DefaultReplace 41 | } 42 | p.replacer = makeReplacer(separator, replace) 43 | }) 44 | } 45 | 46 | func makeReplacer(sep string, repl map[string]string) *strings.Replacer { 47 | var oldnew []string 48 | oldnew = append(oldnew, 49 | flagutil.SetSeparator, sep, 50 | ) 51 | for old, new := range repl { 52 | oldnew = append(oldnew, 53 | old, new, 54 | ) 55 | } 56 | return strings.NewReplacer(oldnew...) 57 | } 58 | 59 | func (p *Parser) Parse(_ context.Context, fs parse.FlagSet) (err error) { 60 | p.init() 61 | 62 | set := func(f *flag.Flag, s string) { 63 | e := f.Value.Set(s) 64 | if e != nil && err == nil { 65 | err = e 66 | } 67 | } 68 | fs.VisitUnspecified(func(f *flag.Flag) { 69 | name := p.name(f) 70 | value, has := p.lookupEnv(name) 71 | if !has { 72 | return 73 | } 74 | if sep := p.ListSeparator; sep != "" { 75 | for _, v := range strings.Split(value, p.ListSeparator) { 76 | set(f, v) 77 | } 78 | } else { 79 | set(f, value) 80 | } 81 | }) 82 | 83 | return err 84 | } 85 | 86 | func (p *Parser) Name(_ context.Context, fs parse.FlagSet) (func(*flag.Flag, func(string)), error) { 87 | p.init() 88 | return func(f *flag.Flag, it func(string)) { 89 | it("$" + p.name(f)) 90 | }, nil 91 | } 92 | 93 | func (p *Parser) name(f *flag.Flag) string { 94 | name := p.Prefix + strings.ToUpper(f.Name) 95 | name = p.replacer.Replace(name) 96 | return name 97 | } 98 | 99 | func (p *Parser) lookupEnv(name string) (value string, has bool) { 100 | if f := p.LookupEnvFunc; f != nil { 101 | return f(name) 102 | } 103 | return os.LookupEnv(name) 104 | } 105 | -------------------------------------------------------------------------------- /parse/env/env_test.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/google/go-cmp/cmp" 10 | 11 | "github.com/gobwas/flagutil" 12 | "github.com/gobwas/flagutil/parse" 13 | "github.com/gobwas/flagutil/parse/testutil" 14 | ) 15 | 16 | var _ flagutil.Printer = new(Parser) 17 | 18 | func TestEnvParser(t *testing.T) { 19 | for _, test := range []struct { 20 | name string 21 | parser *Parser 22 | flags []string 23 | env map[string]string 24 | exp [][2]string 25 | }{ 26 | { 27 | name: "basic", 28 | parser: &Parser{ 29 | Prefix: "F_", 30 | }, 31 | flags: []string{ 32 | "foo", 33 | "bar.baz", 34 | "xxx", 35 | }, 36 | env: map[string]string{ 37 | "F_FOO": "bar", 38 | "F_BAR__BAZ": "qux", 39 | }, 40 | exp: [][2]string{ 41 | {"foo", "bar"}, 42 | {"bar.baz", "qux"}, 43 | }, 44 | }, 45 | } { 46 | t.Run(test.name, func(t *testing.T) { 47 | var fs testutil.StubFlagSet 48 | for _, name := range test.flags { 49 | fs.AddFlag(name, "") 50 | } 51 | 52 | p := test.parser 53 | p.LookupEnvFunc = func(name string) (value string, has bool) { 54 | value, has = test.env[name] 55 | return 56 | } 57 | if err := p.Parse(context.Background(), &fs); err != nil { 58 | t.Fatal(err) 59 | } 60 | if exp, act := test.exp, fs.Pairs(); !cmp.Equal(act, exp) { 61 | t.Errorf( 62 | "unexpected set pairs:\n%s", 63 | cmp.Diff(exp, act), 64 | ) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func TestEnv(t *testing.T) { 71 | testutil.TestParser(t, func(values testutil.Values, fs parse.FlagSet) error { 72 | env := marshal(values) 73 | p := Parser{ 74 | LookupEnvFunc: func(name string) (value string, has bool) { 75 | value, has = env[name] 76 | return 77 | }, 78 | ListSeparator: ";", 79 | } 80 | return p.Parse(context.Background(), fs) 81 | }) 82 | } 83 | 84 | func marshal(values testutil.Values) map[string]string { 85 | var ( 86 | env = make(map[string]string) 87 | replacer = makeReplacer(DefaultSetSeparator, DefaultReplace) 88 | ) 89 | parse.Setup(values, parse.VisitorFunc{ 90 | SetFunc: func(name, value string) error { 91 | name = strings.ToUpper(replacer.Replace(name)) 92 | prev, has := env[name] 93 | if has { 94 | value = prev + ";" + value 95 | } 96 | env[name] = value 97 | return nil 98 | }, 99 | HasFunc: func(string) bool { 100 | return false 101 | }, 102 | }) 103 | fmt.Println("marshal", env) 104 | return env 105 | } 106 | -------------------------------------------------------------------------------- /parse/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "os" 11 | 12 | "github.com/gobwas/flagutil/parse" 13 | ) 14 | 15 | // Syntax is an interface capable to parse file syntax. 16 | type Syntax interface { 17 | Unmarshal([]byte) (map[string]interface{}, error) 18 | } 19 | 20 | // Lookup is an interface to search for syntax source. 21 | type Lookup interface { 22 | Lookup() (io.ReadCloser, error) 23 | } 24 | 25 | // ErrNoFile is an returned by Lookup implementation to report that lookup 26 | // didn't find any file to parse. 27 | var ErrNoFile = fmt.Errorf("file: no file") 28 | 29 | // LookupFunc is an adapter that allows the use of ordinar functions as Lookup. 30 | type LookupFunc func() (io.ReadCloser, error) 31 | 32 | // Lookup implements Lookup interface. 33 | func (f LookupFunc) Lookup() (io.ReadCloser, error) { 34 | return f() 35 | } 36 | 37 | // MultiLookup holds Lookup implementations and their order. 38 | type MultiLookup []Lookup 39 | 40 | // Lookup implements Lookup interface. 41 | func (ls MultiLookup) Lookup() (io.ReadCloser, error) { 42 | for _, l := range ls { 43 | rc, err := l.Lookup() 44 | if err == ErrNoFile { 45 | continue 46 | } 47 | if err != nil { 48 | return nil, err 49 | } 50 | return rc, nil 51 | } 52 | return nil, ErrNoFile 53 | } 54 | 55 | // FlagLookup search for flag with equal name and interprets it as filename to 56 | // open. 57 | type FlagLookup struct { 58 | FlagSet *flag.FlagSet 59 | Name string 60 | } 61 | 62 | // LookupFlag is a shortcut to build up a FlagLookup structure. 63 | func LookupFlag(fs *flag.FlagSet, name string) *FlagLookup { 64 | return &FlagLookup{ 65 | FlagSet: fs, 66 | Name: name, 67 | } 68 | } 69 | 70 | // Lookup implements Lookup interface. 71 | func (f *FlagLookup) Lookup() (io.ReadCloser, error) { 72 | flag := f.FlagSet.Lookup(f.Name) 73 | if flag == nil { 74 | return nil, ErrNoFile 75 | } 76 | path := flag.Value.String() 77 | if path == "" { 78 | return nil, ErrNoFile 79 | } 80 | return os.Open(path) 81 | } 82 | 83 | // PathLookup prepares source search on a path. 84 | // If path is not exits it doesn't fail. 85 | type PathLookup string 86 | 87 | // Lookup implements Lookup interface. 88 | func (p PathLookup) Lookup() (io.ReadCloser, error) { 89 | info, err := os.Stat(string(p)) 90 | if os.IsNotExist(err) { 91 | return nil, ErrNoFile 92 | } 93 | if err != nil { 94 | return nil, err 95 | } 96 | if info.IsDir() { 97 | return nil, fmt.Errorf( 98 | "file: can't parse %s since its dir", 99 | p, 100 | ) 101 | } 102 | return os.Open(string(p)) 103 | } 104 | 105 | // BytesLookup succeeds source lookup with itself. 106 | type BytesLookup []byte 107 | 108 | // Lookup implements Lookup interface. 109 | func (b BytesLookup) Lookup() (io.ReadCloser, error) { 110 | return ioutil.NopCloser(bytes.NewReader(b)), nil 111 | } 112 | 113 | // Parser contains options of parsing source and filling flag values. 114 | type Parser struct { 115 | // Lookup contains logic of how configuration source must be opened. 116 | // Lookup must not be nil. 117 | Lookup Lookup 118 | 119 | // Requires makes Parser to fail if Lookup doesn't return any source. 120 | Required bool 121 | 122 | // Syntax contains logic of parsing source. 123 | Syntax Syntax 124 | } 125 | 126 | // Parse implements flagutil.Parser interface. 127 | func (p *Parser) Parse(_ context.Context, fs parse.FlagSet) error { 128 | bts, err := p.readSource() 129 | if err == ErrNoFile { 130 | if p.Required { 131 | err = fmt.Errorf("file: source not found") 132 | } else { 133 | err = nil 134 | } 135 | } 136 | if err != nil { 137 | return err 138 | } 139 | if len(bts) == 0 { 140 | return nil 141 | } 142 | x, err := p.Syntax.Unmarshal(bts) 143 | if err != nil { 144 | return fmt.Errorf("file: syntax error: %v", err) 145 | } 146 | return parse.Setup(x, parse.VisitorFunc{ 147 | SetFunc: func(name, value string) error { 148 | return fs.Set(name, value) 149 | }, 150 | HasFunc: func(name string) bool { 151 | return fs.Lookup(name) != nil 152 | }, 153 | }) 154 | } 155 | 156 | func (p *Parser) readSource() ([]byte, error) { 157 | src, err := p.Lookup.Lookup() 158 | if err != nil { 159 | return nil, err 160 | } 161 | defer src.Close() 162 | return ioutil.ReadAll(src) 163 | } 164 | -------------------------------------------------------------------------------- /parse/file/file_test.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "flag" 7 | "io/ioutil" 8 | "os" 9 | "testing" 10 | ) 11 | 12 | var ( 13 | _ Lookup = MultiLookup{} 14 | _ Lookup = &FlagLookup{} 15 | _ Lookup = PathLookup("") 16 | _ Lookup = BytesLookup{} 17 | ) 18 | 19 | func TestPathLookupDir(t *testing.T) { 20 | dir, err := ioutil.TempDir("", "") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | defer os.Remove(dir) 25 | lookup := PathLookup(dir) 26 | if _, err := lookup.Lookup(); err == nil { 27 | t.Fatal("want error; got nil") 28 | } 29 | } 30 | 31 | func TestPathLookup(t *testing.T) { 32 | file, exp, err := tempFile() 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | defer os.Remove(file.Name()) 37 | 38 | lookup := PathLookup(file.Name()) 39 | rc, err := lookup.Lookup() 40 | if err != nil { 41 | t.Fatalf("unexpected error: %v", err) 42 | } 43 | defer rc.Close() 44 | 45 | act, err := ioutil.ReadAll(rc) 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | if !bytes.Equal(act, exp) { 50 | t.Fatalf("unexpected file contents") 51 | } 52 | } 53 | 54 | func TestFlagLookup(t *testing.T) { 55 | file, exp, err := tempFile() 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | defer os.Remove(file.Name()) 60 | 61 | fs := flag.NewFlagSet(t.Name(), flag.PanicOnError) 62 | fs.String("config", file.Name(), "") 63 | 64 | lookup := LookupFlag(fs, "config") 65 | 66 | rc, err := lookup.Lookup() 67 | if err != nil { 68 | t.Fatalf("unexpected error: %v", err) 69 | } 70 | defer rc.Close() 71 | 72 | act, err := ioutil.ReadAll(rc) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | if !bytes.Equal(act, exp) { 77 | t.Fatalf("unexpected file contents") 78 | } 79 | } 80 | 81 | func tempFile() (file *os.File, content []byte, err error) { 82 | file, err = ioutil.TempFile("", "") 83 | if err != nil { 84 | return nil, nil, err 85 | } 86 | defer func() { 87 | if err != nil { 88 | file.Close() 89 | os.Remove(file.Name()) 90 | } 91 | }() 92 | 93 | content = make([]byte, 512) 94 | n, err := rand.Read(content) 95 | if err != nil { 96 | return nil, nil, err 97 | } 98 | content = content[:n] 99 | 100 | if _, err := file.Write(content); err != nil { 101 | return nil, nil, err 102 | } 103 | if err := file.Close(); err != nil { 104 | return nil, nil, err 105 | } 106 | 107 | return file, content, nil 108 | } 109 | -------------------------------------------------------------------------------- /parse/file/json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import "encoding/json" 4 | 5 | type Syntax struct { 6 | } 7 | 8 | func (s *Syntax) Unmarshal(p []byte) (m map[string]interface{}, err error) { 9 | err = json.Unmarshal(p, &m) 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /parse/file/json/json_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/gobwas/flagutil/parse" 9 | "github.com/gobwas/flagutil/parse/file" 10 | "github.com/gobwas/flagutil/parse/testutil" 11 | ) 12 | 13 | func TestJSON(t *testing.T) { 14 | testutil.TestParser(t, func(values testutil.Values, fs parse.FlagSet) error { 15 | p := file.Parser{ 16 | Lookup: file.BytesLookup(marshal(values)), 17 | Syntax: new(Syntax), 18 | } 19 | return p.Parse(context.Background(), fs) 20 | }) 21 | } 22 | 23 | func marshal(values testutil.Values) []byte { 24 | bts, err := json.Marshal(values) 25 | if err != nil { 26 | panic(err) 27 | } 28 | return bts 29 | } 30 | -------------------------------------------------------------------------------- /parse/file/toml/toml.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import "github.com/BurntSushi/toml" 4 | 5 | type Syntax struct { 6 | } 7 | 8 | func (s *Syntax) Unmarshal(p []byte) (m map[string]interface{}, err error) { 9 | err = toml.Unmarshal(p, &m) 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /parse/file/toml/toml_test.go: -------------------------------------------------------------------------------- 1 | package toml 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | 8 | "github.com/BurntSushi/toml" 9 | 10 | "github.com/gobwas/flagutil/parse" 11 | "github.com/gobwas/flagutil/parse/file" 12 | "github.com/gobwas/flagutil/parse/testutil" 13 | ) 14 | 15 | func TestTOML(t *testing.T) { 16 | testutil.TestParser(t, func(values testutil.Values, fs parse.FlagSet) error { 17 | p := file.Parser{ 18 | Lookup: file.BytesLookup(marshal(values)), 19 | Syntax: new(Syntax), 20 | } 21 | return p.Parse(context.Background(), fs) 22 | }) 23 | } 24 | 25 | func marshal(values testutil.Values) []byte { 26 | var buf bytes.Buffer 27 | err := toml.NewEncoder(&buf).Encode(values) 28 | if err != nil { 29 | panic(err) 30 | } 31 | return buf.Bytes() 32 | } 33 | -------------------------------------------------------------------------------- /parse/file/yaml/yaml.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import yaml "gopkg.in/yaml.v2" 4 | 5 | type Syntax struct { 6 | } 7 | 8 | func (s *Syntax) Unmarshal(p []byte) (m map[string]interface{}, err error) { 9 | err = yaml.Unmarshal(p, &m) 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /parse/file/yaml/yaml_test.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | yaml "gopkg.in/yaml.v2" 8 | 9 | "github.com/gobwas/flagutil/parse" 10 | "github.com/gobwas/flagutil/parse/file" 11 | "github.com/gobwas/flagutil/parse/testutil" 12 | ) 13 | 14 | func TestYAML(t *testing.T) { 15 | testutil.TestParser(t, func(values testutil.Values, fs parse.FlagSet) error { 16 | p := file.Parser{ 17 | Lookup: file.BytesLookup(marshal(values)), 18 | Syntax: new(Syntax), 19 | } 20 | return p.Parse(context.Background(), fs) 21 | }) 22 | } 23 | 24 | func marshal(values testutil.Values) []byte { 25 | bts, err := yaml.Marshal(values) 26 | if err != nil { 27 | panic(err) 28 | } 29 | return bts 30 | } 31 | -------------------------------------------------------------------------------- /parse/flagset.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | ) 7 | 8 | type FlagGetter interface { 9 | Lookup(name string) *flag.Flag 10 | VisitAll(func(*flag.Flag)) 11 | VisitUnspecified(func(*flag.Flag)) 12 | } 13 | 14 | type FlagSetter interface { 15 | Set(name, value string) error 16 | } 17 | 18 | type FlagSet interface { 19 | FlagGetter 20 | FlagSetter 21 | } 22 | 23 | type FlagSetOption func(*flagSet) 24 | 25 | func WithIgnoreUndefined(v bool) FlagSetOption { 26 | return func(fs *flagSet) { 27 | fs.ignoreUndefined = v 28 | } 29 | } 30 | 31 | func NextLevel(fs FlagSet) { 32 | fset := fs.(*flagSet) 33 | fset.stash = nil 34 | fset.update() 35 | } 36 | 37 | func Stash(fs FlagSet, fn func(*flag.Flag) bool) { 38 | fset := fs.(*flagSet) 39 | fset.stash = fn 40 | } 41 | 42 | func IgnoreUndefined(fs FlagSet, ignore bool) { 43 | fset := fs.(*flagSet) 44 | fset.ignoreUndefined = ignore 45 | } 46 | 47 | func AllowResetSpecified(fs FlagSet, allow bool) { 48 | fset := fs.(*flagSet) 49 | fset.allowResetSpecified = allow 50 | } 51 | 52 | type flagSet struct { 53 | dest *flag.FlagSet 54 | ignoreUndefined bool 55 | allowResetSpecified bool 56 | specified map[string]bool 57 | stash func(*flag.Flag) bool 58 | } 59 | 60 | func NewFlagSet(flags *flag.FlagSet, opts ...FlagSetOption) FlagSet { 61 | fs := &flagSet{ 62 | dest: flags, 63 | specified: make(map[string]bool), 64 | } 65 | for _, opt := range opts { 66 | opt(fs) 67 | } 68 | fs.update() 69 | return fs 70 | } 71 | 72 | func (fs *flagSet) Set(name, value string) error { 73 | if fs.specified[name] && !fs.allowResetSpecified { 74 | return nil 75 | } 76 | f := fs.dest.Lookup(name) 77 | if f != nil && fs.stashed(f) { 78 | f = nil 79 | } 80 | defined := f != nil 81 | if !defined && fs.ignoreUndefined { 82 | return nil 83 | } 84 | if !defined { 85 | return fmt.Errorf("flag provided but not defined: %q", name) 86 | } 87 | err := fs.dest.Set(name, value) 88 | if err != nil { 89 | err = fmt.Errorf("set %q: %w", name, err) 90 | } 91 | return err 92 | } 93 | 94 | func (fs *flagSet) stashed(f *flag.Flag) bool { 95 | stash := fs.stash 96 | return stash != nil && stash(f) 97 | } 98 | 99 | func (fs *flagSet) update() { 100 | fs.dest.Visit(func(f *flag.Flag) { 101 | fs.specified[f.Name] = true 102 | }) 103 | } 104 | 105 | func (fs *flagSet) VisitUnspecified(fn func(*flag.Flag)) { 106 | fs.dest.VisitAll(func(f *flag.Flag) { 107 | if !fs.specified[f.Name] && !fs.stashed(f) { 108 | fn(fs.clone(f)) 109 | } 110 | }) 111 | } 112 | 113 | func (fs *flagSet) VisitAll(fn func(*flag.Flag)) { 114 | fs.dest.VisitAll(func(f *flag.Flag) { 115 | if !fs.stashed(f) { 116 | fn(fs.clone(f)) 117 | } 118 | }) 119 | } 120 | 121 | func (fs *flagSet) Lookup(name string) *flag.Flag { 122 | f := fs.dest.Lookup(name) 123 | if f == nil || fs.stashed(f) { 124 | return nil 125 | } 126 | return fs.clone(f) 127 | } 128 | 129 | func (fs *flagSet) clone(f *flag.Flag) *flag.Flag { 130 | cp := *f 131 | cp.Value = value{ 132 | Value: f.Value, 133 | fs: fs, 134 | name: f.Name, 135 | } 136 | return &cp 137 | } 138 | 139 | type value struct { 140 | flag.Value 141 | fs *flagSet 142 | name string 143 | } 144 | 145 | func (v value) Set(s string) error { 146 | return v.fs.Set(v.name, s) 147 | } 148 | 149 | func (v value) IsBoolFlag() bool { 150 | x, ok := v.Value.(interface { 151 | IsBoolFlag() bool 152 | }) 153 | return ok && x.IsBoolFlag() 154 | } 155 | -------------------------------------------------------------------------------- /parse/pargs/posix.go: -------------------------------------------------------------------------------- 1 | // Package pargs implements POSIX program argument syntax conventions. 2 | // 3 | // See https://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html 4 | package pargs 5 | 6 | import ( 7 | "context" 8 | "flag" 9 | "fmt" 10 | "strings" 11 | 12 | "github.com/gobwas/flagutil" 13 | "github.com/gobwas/flagutil/parse" 14 | ) 15 | 16 | type Parser struct { 17 | Args []string 18 | 19 | // Shorthand specifies whether parser should try to provide shorthand 20 | // version (e.g. just first letter of name) of each top level flag. 21 | Shorthand bool 22 | 23 | // ShorthandFunc allows user to define custom way of picking shorthand 24 | // version of flag with given name. 25 | // Shorthand field must be true when setting ShorthandFunc. 26 | // Returning empty string means that no shorthand is possible for given 27 | // name. 28 | ShorthandFunc func(string) string 29 | 30 | pos int 31 | err error 32 | mult bool 33 | name string 34 | value string 35 | fs parse.FlagSet 36 | alias map[string]string 37 | } 38 | 39 | func (p *Parser) Parse(_ context.Context, fs parse.FlagSet) (err error) { 40 | p.reset(fs) 41 | 42 | for p.next() { 43 | p.pairs(func(name, value string) bool { 44 | name = p.resolve(name) 45 | 46 | _, isHelp := lookup(fs, name) 47 | if isHelp { 48 | err = flag.ErrHelp 49 | return false 50 | } 51 | 52 | err = fs.Set(name, value) 53 | 54 | return err == nil 55 | }) 56 | if err != nil { 57 | return err 58 | } 59 | } 60 | return p.err 61 | } 62 | 63 | func (p *Parser) NonOptionArgs() []string { 64 | if p.pos < len(p.Args) { 65 | return p.Args[p.pos:] 66 | } 67 | return nil 68 | } 69 | 70 | func (p *Parser) resolve(name string) string { 71 | if s, has := p.alias[name]; has { 72 | name = s 73 | } 74 | return name 75 | } 76 | 77 | func (p *Parser) Name(_ context.Context, fs parse.FlagSet) (func(*flag.Flag, func(string)), error) { 78 | short := p.shorthands(fs) 79 | return func(f *flag.Flag, it func(string)) { 80 | if p.Shorthand { 81 | s := p.shorthand(f) 82 | if _, has := short[s]; has { 83 | it("-" + s) 84 | } 85 | } 86 | var prefix string 87 | if len(f.Name) == 1 { 88 | prefix = "-" 89 | } else { 90 | prefix = "--" 91 | } 92 | it(prefix + f.Name) 93 | }, nil 94 | } 95 | 96 | func (p *Parser) shorthand(f *flag.Flag) string { 97 | if fn := p.ShorthandFunc; fn != nil { 98 | return fn(f.Name) 99 | } 100 | if !isTopSet(f) { 101 | // Not a topmost flag set. 102 | return "" 103 | } 104 | return string(f.Name[0]) 105 | } 106 | 107 | func (p *Parser) pairs(fn func(name, value string) bool) { 108 | if p.mult { 109 | for i := range p.name { 110 | if !fn(p.name[i:i+1], p.value) { 111 | return 112 | } 113 | } 114 | return 115 | } 116 | 117 | fn(p.name, p.value) 118 | } 119 | 120 | func (p *Parser) reset(fs parse.FlagSet) { 121 | p.pos = 0 122 | p.err = nil 123 | p.mult = false 124 | p.name = "" 125 | p.value = "" 126 | p.fs = fs 127 | if p.Shorthand { 128 | p.alias = p.shorthands(fs) 129 | } 130 | } 131 | 132 | func lookup(fs parse.FlagSet, name string) (f *flag.Flag, isHelp bool) { 133 | f = fs.Lookup(name) 134 | isHelp = f == nil && (name == "h" || name == "help") 135 | return 136 | } 137 | 138 | func (p *Parser) isBoolFlag(name string) bool { 139 | name = p.resolve(name) 140 | f, isHelp := lookup(p.fs, name) 141 | if isHelp { 142 | return true 143 | } 144 | if f == nil { 145 | return false 146 | } 147 | return isBoolFlag(f) 148 | } 149 | 150 | func (p *Parser) next() bool { 151 | if p.err != nil { 152 | return false 153 | } 154 | if p.pos >= len(p.Args) { 155 | return false 156 | } 157 | s := p.Args[p.pos] 158 | if len(s) < 2 || s[0] != '-' { 159 | return false 160 | } 161 | p.mult = false 162 | p.pos++ 163 | var short bool 164 | if s[1] == '-' { 165 | if len(s) == 2 { 166 | // "--" terminates all options. 167 | return false 168 | } 169 | s = s[2:] 170 | } else { 171 | short = true 172 | s = s[1:] 173 | } 174 | name, value, hasValue := split(s, '=') 175 | if !hasValue && p.pos < len(p.Args) { 176 | value = p.Args[p.pos] 177 | if len(value) == 0 || value[0] != '-' { 178 | if p.isBoolFlag(name) { 179 | dash := "--" 180 | if short { 181 | dash = "-" 182 | } 183 | p.fail(""+ 184 | "ambiguous boolean flag %[1]s%[2]s value: can't guess whether "+ 185 | "the %[3]q is the flag value or the non-flag argument "+ 186 | "(consider using `%[1]s%[2]s=%[3]s` or `%[1]s%[2]s -- %[3]s`)", 187 | dash, name, value, 188 | ) 189 | return false 190 | } 191 | hasValue = true 192 | p.pos++ 193 | } 194 | } 195 | if short { 196 | if hasValue && len(name) > 1 { // -abc=foo, -abc foo 197 | p.fail("invalid short option syntax for %q", name) 198 | return false 199 | } 200 | if !hasValue { // [-o, -abc] or [-ofoo] 201 | if !p.isBoolFlag(name[:1]) { // -ofoo 202 | if len(name) == 1 { 203 | p.fail("argument is required for option %q", name) 204 | return false 205 | } 206 | value = name[1:] 207 | name = name[:1] 208 | } else { 209 | p.mult = true 210 | value = "true" 211 | } 212 | } 213 | } else { 214 | if !hasValue { 215 | if !p.isBoolFlag(name) { 216 | p.fail("argument is required for option %q", name) 217 | return false 218 | } 219 | value = "true" 220 | } 221 | } 222 | if !isValidName(name, short) { 223 | p.fail("invalid option name: %q", name) 224 | return false 225 | } 226 | 227 | p.name = name 228 | p.value = value 229 | 230 | return true 231 | } 232 | 233 | func (p *Parser) shorthands(fs parse.FlagSet) map[string]string { 234 | short := make(map[string]string) 235 | // Need to provide all shorthand aliases to not fail on meeting some 236 | // shorthand version of already provided flag. 237 | fs.VisitAll(func(f *flag.Flag) { 238 | s := p.shorthand(f) 239 | if s == "" { 240 | return 241 | } 242 | if _, has := short[s]; has { 243 | // Mark this shorthand name as ambiguous. 244 | short[s] = "" 245 | } else { 246 | short[s] = f.Name 247 | } 248 | }) 249 | for s, n := range short { 250 | if n == "" { 251 | delete(short, s) 252 | } 253 | if fs.Lookup(s) != nil { 254 | delete(short, s) 255 | } 256 | } 257 | return short 258 | } 259 | 260 | func (p *Parser) fail(f string, args ...interface{}) { 261 | p.err = fmt.Errorf("pargs: %s", fmt.Sprintf(f, args...)) 262 | } 263 | 264 | func split(s string, sep byte) (a, b string, ok bool) { 265 | i := strings.IndexByte(s, sep) 266 | if i == -1 { 267 | return s, "", false 268 | } 269 | return s[:i], s[i+1:], true 270 | } 271 | 272 | func isValidName(s string, short bool) bool { 273 | if len(s) == 0 { 274 | return false 275 | } 276 | for i := 0; i < len(s); i++ { 277 | c := s[i] 278 | if !isLetter(c) && !isDigit(c) && (short || !isSpecial(c)) { 279 | return false 280 | } 281 | } 282 | return true 283 | } 284 | 285 | var special = [...]bool{ 286 | '.': true, 287 | '_': true, 288 | '-': true, 289 | } 290 | 291 | func isSpecial(c byte) bool { 292 | return special[c] 293 | } 294 | 295 | func isDigit(c byte) bool { 296 | return '0' <= c && c <= '9' 297 | } 298 | 299 | func isLetter(c byte) bool { 300 | return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') 301 | } 302 | 303 | func isBoolFlag(f *flag.Flag) bool { 304 | x, ok := f.Value.(interface { 305 | IsBoolFlag() bool 306 | }) 307 | return ok && x.IsBoolFlag() 308 | } 309 | 310 | func isTopSet(f *flag.Flag) bool { 311 | return strings.Index(f.Name, flagutil.SetSeparator) == -1 312 | } 313 | -------------------------------------------------------------------------------- /parse/pargs/posix_test.go: -------------------------------------------------------------------------------- 1 | package pargs 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | 9 | "github.com/gobwas/flagutil" 10 | "github.com/gobwas/flagutil/parse" 11 | "github.com/gobwas/flagutil/parse/testutil" 12 | ) 13 | 14 | var _ flagutil.Printer = new(Parser) 15 | 16 | func TestPosixParse(t *testing.T) { 17 | for _, test := range []struct { 18 | name string 19 | args []string 20 | expPairs [][2]string 21 | expArgs []string 22 | err bool 23 | flags map[string]bool 24 | shorthand bool 25 | }{ 26 | { 27 | name: "short basic", 28 | args: []string{ 29 | "-a", 30 | "-bcd", 31 | "-efoo", 32 | "-f=bar", 33 | "-g", "baz", 34 | }, 35 | flags: map[string]bool{ 36 | "a": true, 37 | "b": true, 38 | "c": true, 39 | "d": true, 40 | "e": false, 41 | "f": false, 42 | "g": false, 43 | }, 44 | expPairs: [][2]string{ 45 | {"a", "true"}, 46 | {"b", "true"}, 47 | {"c", "true"}, 48 | {"d", "true"}, 49 | 50 | {"e", "foo"}, 51 | {"f", "bar"}, 52 | {"g", "baz"}, 53 | }, 54 | }, 55 | { 56 | name: "long basic", 57 | args: []string{ 58 | "--a", 59 | "--foo", 60 | "--bar=baz", 61 | "--opt", "val", 62 | }, 63 | flags: map[string]bool{ 64 | "a": true, 65 | "foo": true, 66 | "bar": false, 67 | "opt": false, 68 | }, 69 | expPairs: [][2]string{ 70 | {"a", "true"}, 71 | {"foo", "true"}, 72 | {"bar", "baz"}, 73 | {"opt", "val"}, 74 | }, 75 | }, 76 | { 77 | name: "short booleans", 78 | args: []string{ 79 | "-t=true", 80 | "-f=false", 81 | }, 82 | flags: map[string]bool{ 83 | "t": true, 84 | "f": true, 85 | }, 86 | expPairs: [][2]string{ 87 | {"t", "true"}, 88 | {"f", "false"}, 89 | }, 90 | }, 91 | { 92 | name: "short ambiguous booleans", 93 | args: []string{ 94 | "-t", "true", 95 | "-f", "false", 96 | }, 97 | flags: map[string]bool{ 98 | "t": true, 99 | "f": true, 100 | }, 101 | err: true, 102 | }, 103 | { 104 | name: "long booleans", 105 | args: []string{ 106 | "--foo=true", 107 | "--bar=false", 108 | }, 109 | flags: map[string]bool{ 110 | "foo": true, 111 | "bar": true, 112 | }, 113 | expPairs: [][2]string{ 114 | {"foo", "true"}, 115 | {"bar", "false"}, 116 | }, 117 | }, 118 | { 119 | name: "empty parameter", 120 | args: []string{ 121 | "--foo", "", 122 | }, 123 | flags: map[string]bool{ 124 | "foo": false, 125 | }, 126 | expPairs: [][2]string{ 127 | {"foo", ""}, 128 | }, 129 | }, 130 | { 131 | name: "empty boolean", 132 | args: []string{ 133 | "--foo", "", 134 | }, 135 | flags: map[string]bool{ 136 | "foo": true, 137 | }, 138 | err: true, 139 | }, 140 | { 141 | name: "last bool parameter", 142 | flags: map[string]bool{ 143 | "param1": false, 144 | "param2": true, 145 | }, 146 | args: []string{ 147 | "--param1", "value", 148 | "--param2", 149 | }, 150 | expPairs: [][2]string{ 151 | {"param1", "value"}, 152 | {"param2", "true"}, 153 | }, 154 | }, 155 | { 156 | name: "long ambiguous booleans", 157 | args: []string{ 158 | "--foo", "true", 159 | "--bar", "false", 160 | }, 161 | flags: map[string]bool{ 162 | "foo": true, 163 | "bar": true, 164 | }, 165 | err: true, 166 | }, 167 | { 168 | name: "non-boolean without argument", 169 | args: []string{ 170 | "--param", 171 | }, 172 | flags: map[string]bool{ 173 | "param": false, 174 | }, 175 | err: true, 176 | }, 177 | { 178 | name: "invalid name", 179 | args: []string{ 180 | "--=foo", 181 | }, 182 | err: true, 183 | }, 184 | { 185 | name: "short ambiguous", 186 | args: []string{ 187 | "-abc=foo", 188 | }, 189 | err: true, 190 | }, 191 | 192 | { 193 | name: "non-flag arguments basic", 194 | args: []string{ 195 | "-a", 196 | "--param", "value", 197 | "arg1", "arg2", "arg3", 198 | }, 199 | flags: map[string]bool{ 200 | "a": true, 201 | "param": false, 202 | }, 203 | expPairs: [][2]string{ 204 | {"a", "true"}, 205 | {"param", "value"}, 206 | }, 207 | expArgs: []string{ 208 | "arg1", "arg2", "arg3", 209 | }, 210 | }, 211 | { 212 | name: "non-flag arguments basic with dash-dash", 213 | args: []string{ 214 | "-a", 215 | "--param", "value", 216 | "--", 217 | "arg1", "arg2", "arg3", 218 | }, 219 | flags: map[string]bool{ 220 | "a": true, 221 | "param": false, 222 | }, 223 | expPairs: [][2]string{ 224 | {"a", "true"}, 225 | {"param", "value"}, 226 | }, 227 | expArgs: []string{ 228 | "arg1", "arg2", "arg3", 229 | }, 230 | }, 231 | 232 | { 233 | name: "shorthand basic", 234 | shorthand: true, 235 | flags: map[string]bool{ 236 | "shorthand": false, 237 | }, 238 | args: []string{ 239 | "-s=foo", 240 | }, 241 | expPairs: [][2]string{ 242 | {"shorthand", "foo"}, 243 | }, 244 | }, 245 | { 246 | name: "shorthand ambiguous", 247 | shorthand: true, 248 | flags: map[string]bool{ 249 | "some-foo": false, 250 | "some-bar": false, 251 | }, 252 | args: []string{ 253 | "-s=foo", 254 | }, 255 | err: true, 256 | }, 257 | { 258 | name: "shorthand collision", 259 | shorthand: true, 260 | flags: map[string]bool{ 261 | "some-foo": false, 262 | "s": false, 263 | }, 264 | args: []string{ 265 | "-s=foo", 266 | }, 267 | expPairs: [][2]string{ 268 | {"s", "foo"}, 269 | }, 270 | }, 271 | { 272 | name: "shorthand only top", 273 | shorthand: true, 274 | flags: map[string]bool{ 275 | "some.foo": false, 276 | }, 277 | args: []string{ 278 | "-s=foo", 279 | }, 280 | err: true, 281 | }, 282 | 283 | { 284 | name: "non-existing-short-single", 285 | flags: map[string]bool{}, 286 | args: []string{"-w"}, 287 | err: true, 288 | }, 289 | { 290 | name: "non-existing-short-multi", 291 | flags: map[string]bool{}, 292 | args: []string{"-www"}, 293 | err: true, 294 | }, 295 | { 296 | name: "non-existing-long", 297 | flags: map[string]bool{}, 298 | args: []string{"--www"}, 299 | err: true, 300 | }, 301 | } { 302 | t.Run(test.name, func(t *testing.T) { 303 | var fs testutil.StubFlagSet 304 | for name, isBool := range test.flags { 305 | if isBool { 306 | fs.AddBoolFlag(name, false) 307 | } else { 308 | fs.AddFlag(name, "") 309 | } 310 | } 311 | p := Parser{ 312 | Args: test.args, 313 | Shorthand: test.shorthand, 314 | } 315 | err := p.Parse(context.Background(), &fs) 316 | if !test.err && err != nil { 317 | t.Fatalf("unexpected error: %v", err) 318 | } 319 | if test.err && err == nil { 320 | t.Fatalf("want error; got nothing") 321 | } 322 | if test.err { 323 | return 324 | } 325 | if exp, act := test.expPairs, fs.Pairs(); !cmp.Equal(act, exp) { 326 | t.Errorf( 327 | "unexpected set pairs:\n%s", 328 | cmp.Diff(exp, act), 329 | ) 330 | } 331 | if exp, act := test.expArgs, p.NonOptionArgs(); !cmp.Equal(act, exp) { 332 | t.Errorf( 333 | "unexpected non-flag arguments:\n%s", 334 | cmp.Diff(exp, act), 335 | ) 336 | } 337 | }) 338 | } 339 | } 340 | 341 | func TestPosix(t *testing.T) { 342 | testutil.TestParser(t, func(values testutil.Values, fs parse.FlagSet) error { 343 | p := Parser{ 344 | Args: marshal(values), 345 | } 346 | return p.Parse(context.Background(), fs) 347 | }) 348 | } 349 | 350 | func marshal(values testutil.Values) (args []string) { 351 | parse.Setup(values, parse.VisitorFunc{ 352 | SetFunc: func(name, value string) error { 353 | if len(name) == 1 { 354 | args = append(args, "-"+name) 355 | } else { 356 | args = append(args, "--"+name) 357 | } 358 | if value != "true" { 359 | args = append(args, value) 360 | } 361 | return nil 362 | }, 363 | HasFunc: func(string) bool { 364 | return false 365 | }, 366 | }) 367 | return args 368 | } 369 | -------------------------------------------------------------------------------- /parse/parse.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | ) 8 | 9 | var SetSeparator = "." 10 | 11 | type SetupFunc func(name, value string) error 12 | 13 | type Visitor interface { 14 | Set(name, value string) error 15 | Has(name string) bool 16 | } 17 | 18 | type VisitorFunc struct { 19 | SetFunc func(name, value string) error 20 | HasFunc func(name string) bool 21 | } 22 | 23 | func (v VisitorFunc) Set(name, value string) error { 24 | return v.SetFunc(name, value) 25 | } 26 | 27 | func (v VisitorFunc) Has(name string) bool { 28 | return v.HasFunc(name) 29 | } 30 | 31 | func Setup(x interface{}, v Visitor) error { 32 | return setup(v, "", x) 33 | } 34 | 35 | func setup(v Visitor, key string, value interface{}) error { 36 | var ( 37 | typ = reflect.TypeOf(value) 38 | val = reflect.ValueOf(value) 39 | ) 40 | switch typ.Kind() { 41 | case reflect.Map: 42 | iter := val.MapRange() 43 | if v.Has(key) { 44 | for iter.Next() { 45 | ks, err := stringify(iter.Key().Interface()) 46 | if err != nil { 47 | return err 48 | } 49 | vs, err := stringify(iter.Value().Interface()) 50 | if err != nil { 51 | return err 52 | } 53 | if err := setup(v, key, ks+":"+vs); err != nil { 54 | return err 55 | } 56 | } 57 | } else { 58 | for iter.Next() { 59 | ks, err := stringify(iter.Key().Interface()) 60 | if err != nil { 61 | return err 62 | } 63 | if err := setup(v, Join(key, ks), iter.Value().Interface()); err != nil { 64 | return err 65 | } 66 | } 67 | } 68 | 69 | case reflect.Slice: 70 | for i := 0; i < val.Len(); i++ { 71 | vs, err := stringify(val.Index(i).Interface()) 72 | if err != nil { 73 | return err 74 | } 75 | if err := setup(v, key, vs); err != nil { 76 | return err 77 | } 78 | } 79 | 80 | default: 81 | str, err := stringify(value) 82 | if err != nil { 83 | return err 84 | } 85 | if str == "" { 86 | return fmt.Errorf("can't use empty key as flag name") 87 | } 88 | if err := v.Set(key, str); err != nil { 89 | return fmt.Errorf( 90 | "set %q (%T) as flag %q value error: %v", 91 | str, value, key, err, 92 | ) 93 | } 94 | } 95 | return nil 96 | } 97 | 98 | func Join(a, b string) string { 99 | if a == "" { 100 | return b 101 | } 102 | return a + "." + b 103 | } 104 | 105 | func stringify(x interface{}) (string, error) { 106 | switch v := x.(type) { 107 | case 108 | bool, 109 | int, 110 | int8, 111 | int16, 112 | int32, 113 | int64, 114 | uint, 115 | uint8, 116 | uint16, 117 | uint32, 118 | uint64, 119 | uintptr: 120 | 121 | return fmt.Sprintf("%v", v), nil 122 | 123 | case float64: 124 | return strconv.FormatFloat(v, 'f', -1, 64), nil 125 | 126 | case float32: 127 | return strconv.FormatFloat(float64(v), 'f', -1, 32), nil 128 | 129 | case string: 130 | return v, nil 131 | 132 | default: 133 | return "", fmt.Errorf("can't stringify %[1]v (%[1]T)", v) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /parse/parse_test.go: -------------------------------------------------------------------------------- 1 | package parse 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/google/go-cmp/cmp" 7 | ) 8 | 9 | func TestSetup(t *testing.T) { 10 | for _, test := range []struct { 11 | name string 12 | pairs [][2]string 13 | input interface{} 14 | has map[string]bool 15 | err bool 16 | }{ 17 | { 18 | name: "basic", 19 | input: map[string]string{ 20 | "foo": "bar", 21 | }, 22 | pairs: [][2]string{ 23 | {"foo", "bar"}, 24 | }, 25 | }, 26 | { 27 | name: "slice", 28 | input: map[string]interface{}{ 29 | "foo": []string{ 30 | "a", "b", "c", 31 | }, 32 | }, 33 | pairs: [][2]string{ 34 | {"foo", "a"}, 35 | {"foo", "b"}, 36 | {"foo", "c"}, 37 | }, 38 | }, 39 | { 40 | name: "nested", 41 | input: map[string]interface{}{ 42 | "foo": map[string]string{ 43 | "bar": "baz", 44 | }, 45 | }, 46 | pairs: [][2]string{ 47 | {"foo.bar", "baz"}, 48 | }, 49 | }, 50 | { 51 | name: "custom mapping", 52 | input: map[int]int{ 53 | 1: 2, 54 | }, 55 | pairs: [][2]string{ 56 | {"1", "2"}, 57 | }, 58 | }, 59 | { 60 | name: "mapping", 61 | input: map[string]interface{}{ 62 | "foo": map[string]interface{}{ 63 | "bar": map[string]string{ 64 | "baz": "yes", 65 | }, 66 | }, 67 | }, 68 | has: map[string]bool{ 69 | "foo.bar": true, 70 | }, 71 | pairs: [][2]string{ 72 | {"foo.bar", "baz:yes"}, 73 | }, 74 | }, 75 | { 76 | name: "restrictions", 77 | input: map[string]interface{}{ 78 | "slice": []interface{}{ 79 | map[string]string{ 80 | "foo": "bar", 81 | }, 82 | }, 83 | }, 84 | err: true, 85 | }, 86 | } { 87 | t.Run(test.name, func(t *testing.T) { 88 | var act [][2]string 89 | err := Setup(test.input, VisitorFunc{ 90 | SetFunc: func(name, value string) error { 91 | act = append(act, [2]string{name, value}) 92 | return nil 93 | }, 94 | HasFunc: func(name string) bool { 95 | return test.has[name] 96 | }, 97 | }) 98 | if test.err && err == nil { 99 | t.Fatalf("want error; got nothing") 100 | } 101 | if !test.err && err != nil { 102 | t.Fatalf("unexpected error: %v", err) 103 | } 104 | if exp := test.pairs; !cmp.Equal(act, exp) { 105 | t.Fatalf("unexpected pairs:\n%s", cmp.Diff(exp, act)) 106 | } 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /parse/prompt/prompt.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/gobwas/flagutil/parse" 9 | "github.com/gobwas/prompt" 10 | ) 11 | 12 | type FlagInfo struct { 13 | Message string 14 | Options []string 15 | Multiple bool 16 | Boolean bool 17 | } 18 | 19 | type FlagInfoMapper interface { 20 | FlagInfo(context.Context, *flag.Flag) (FlagInfo, error) 21 | } 22 | 23 | type FlagInfoFunc func(context.Context, *flag.Flag) (FlagInfo, error) 24 | 25 | func (fn FlagInfoFunc) FlagInfo(ctx context.Context, f *flag.Flag) (FlagInfo, error) { 26 | return fn(ctx, f) 27 | } 28 | 29 | type FlagInfoMap map[string]FlagInfo 30 | 31 | func (m FlagInfoMap) FlagInfo(_ context.Context, f *flag.Flag) (FlagInfo, error) { 32 | return m[f.Name], nil 33 | } 34 | 35 | var DefaultMessage = func(f *flag.Flag, c FlagInfo) string { 36 | m := "Specify " + f.Name + " value" 37 | if c.Multiple { 38 | m += "(s)" 39 | } 40 | m += " (" + f.Usage + ")" 41 | return m 42 | } 43 | 44 | type Parser struct { 45 | Retry bool 46 | FlagInfo FlagInfoMapper 47 | Message func(*flag.Flag, FlagInfo) string 48 | } 49 | 50 | func (p *Parser) Parse(ctx context.Context, fs parse.FlagSet) (err error) { 51 | fs.VisitUnspecified(func(f *flag.Flag) { 52 | if err != nil { 53 | return 54 | } 55 | repeat: 56 | var set []string 57 | set, err = p.values(ctx, f) 58 | if err != nil { 59 | return 60 | } 61 | for i, s := range set { 62 | err = fs.Set(f.Name, s) 63 | if err != nil && p.Retry && i == 0 { 64 | // i == 0 is required to not leave partially configured flags. 65 | fmt.Println(err) 66 | goto repeat 67 | } 68 | if err != nil { 69 | break 70 | } 71 | } 72 | }) 73 | return err 74 | } 75 | 76 | func (p *Parser) values(ctx context.Context, f *flag.Flag) ([]string, error) { 77 | cfg, err := p.info(ctx, f) 78 | if err != nil { 79 | return nil, err 80 | } 81 | switch { 82 | case cfg.Options != nil: 83 | return p.opt(ctx, f, cfg) 84 | 85 | case cfg.Boolean || isBoolFlag(f): 86 | return p.confirm(ctx, f, cfg) 87 | 88 | default: 89 | return p.readLine(ctx, f, cfg) 90 | } 91 | } 92 | 93 | func (p *Parser) info(ctx context.Context, f *flag.Flag) (_ FlagInfo, err error) { 94 | if x := p.FlagInfo; x != nil { 95 | return x.FlagInfo(ctx, f) 96 | } 97 | return 98 | } 99 | 100 | func (p *Parser) message(f *flag.Flag, c FlagInfo) string { 101 | if msg := c.Message; msg != "" { 102 | return msg 103 | } 104 | if fn := p.Message; fn != nil { 105 | return fn(f, c) 106 | } 107 | return DefaultMessage(f, c) 108 | } 109 | 110 | func (p *Parser) opt(ctx context.Context, f *flag.Flag, c FlagInfo) (set []string, err error) { 111 | s := prompt.Select{ 112 | Message: p.message(f, c), 113 | Options: c.Options, 114 | } 115 | var xs []int 116 | if c.Multiple { 117 | xs, err = s.Multiple(ctx) 118 | } else { 119 | xs = []int{-1} 120 | xs[0], err = s.Single(ctx) 121 | } 122 | if err != nil { 123 | return nil, err 124 | } 125 | set = make([]string, len(xs)) 126 | for i, x := range xs { 127 | set[i] = c.Options[x] 128 | } 129 | return set, nil 130 | } 131 | 132 | func (p *Parser) confirm(ctx context.Context, f *flag.Flag, c FlagInfo) (set []string, err error) { 133 | q := prompt.Question{ 134 | Message: p.message(f, c), 135 | Strict: true, 136 | Mode: prompt.QuestionSuffix, 137 | } 138 | v, err := q.Confirm(ctx) 139 | if err != nil { 140 | return nil, err 141 | } 142 | return []string{fmt.Sprintf("%t", v)}, nil 143 | } 144 | 145 | func (p *Parser) readLine(ctx context.Context, f *flag.Flag, c FlagInfo) (set []string, err error) { 146 | pt := prompt.Prompt{ 147 | Message: p.message(f, c) + " ", 148 | Default: f.Value.String(), 149 | } 150 | line, err := pt.ReadLine(ctx) 151 | if err != nil { 152 | return nil, err 153 | } 154 | return []string{line}, nil 155 | } 156 | 157 | func isBoolFlag(f *flag.Flag) bool { 158 | x, ok := f.Value.(interface { 159 | IsBoolFlag() bool 160 | }) 161 | return ok && x.IsBoolFlag() 162 | } 163 | -------------------------------------------------------------------------------- /parse/testutil/testutil.go: -------------------------------------------------------------------------------- 1 | package testutil 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | 12 | "github.com/gobwas/flagutil/parse" 13 | ) 14 | 15 | type stubValue struct { 16 | s *StubFlagSet 17 | name string 18 | value string 19 | isBool bool 20 | } 21 | 22 | func (v *stubValue) Set(value string) error { 23 | v.value = value 24 | return v.s.Set(v.name, value) 25 | } 26 | func (v *stubValue) String() string { 27 | return v.value 28 | } 29 | func (v *stubValue) IsBoolFlag() bool { 30 | return v.isBool 31 | } 32 | 33 | type StubFlagSet struct { 34 | IgnoreUndefined bool 35 | 36 | flags map[string]flag.Value 37 | order []string 38 | pairs [][2]string 39 | } 40 | 41 | func (s *StubFlagSet) init() { 42 | if s.flags == nil { 43 | s.flags = make(map[string]flag.Value) 44 | } 45 | } 46 | 47 | func (s *StubFlagSet) AddFlag(name, value string) { 48 | s.addFlag(name, value, false) 49 | } 50 | 51 | func (s *StubFlagSet) AddBoolFlag(name string, value bool) { 52 | s.addFlag(name, fmt.Sprintf("%t", value), true) 53 | } 54 | 55 | func (s *StubFlagSet) addFlag(name, value string, isBool bool) { 56 | s.init() 57 | s.flags[name] = &stubValue{ 58 | s: s, 59 | name: name, 60 | value: value, 61 | isBool: isBool, 62 | } 63 | s.order = append(s.order, name) 64 | } 65 | 66 | func (s *StubFlagSet) Pairs() [][2]string { 67 | return s.pairs 68 | } 69 | 70 | func (s *StubFlagSet) Lookup(name string) *flag.Flag { 71 | val, has := s.flags[name] 72 | if !has { 73 | return nil 74 | } 75 | return &flag.Flag{ 76 | Name: name, 77 | Value: val, 78 | } 79 | } 80 | 81 | func (s *StubFlagSet) VisitAll(fn func(*flag.Flag)) { 82 | for _, name := range s.order { 83 | fn(&flag.Flag{ 84 | Name: name, 85 | Value: s.flags[name], 86 | }) 87 | } 88 | } 89 | 90 | func (s *StubFlagSet) VisitUnspecified(fn func(*flag.Flag)) { 91 | for _, name := range s.order { 92 | fn(&flag.Flag{ 93 | Name: name, 94 | Value: s.flags[name], 95 | }) 96 | } 97 | } 98 | 99 | func (s *StubFlagSet) Set(name, value string) error { 100 | _, has := s.flags[name] 101 | switch { 102 | case !has && s.IgnoreUndefined: 103 | return nil 104 | case !has: 105 | return fmt.Errorf("no such flag %q", name) 106 | default: 107 | s.pairs = append(s.pairs, [2]string{name, value}) 108 | } 109 | return nil 110 | } 111 | 112 | type Values map[string]interface{} 113 | 114 | func TestParser(t *testing.T, parseFunc func(Values, parse.FlagSet) error) { 115 | for _, test := range []struct { 116 | name string 117 | input Values 118 | setup Values 119 | }{ 120 | { 121 | name: "basic", 122 | input: Values{ 123 | "string": "flagutil", 124 | "int": 42, 125 | "float": 3.14, 126 | "bool": true, 127 | "b": true, 128 | "duration": time.Second, 129 | "subset": Values{ 130 | "foo": "bar", 131 | }, 132 | "list": []string{ 133 | "a", "b", "c", 134 | }, 135 | }, 136 | }, 137 | { 138 | name: "override", 139 | setup: Values{ 140 | "bar": "baz", 141 | }, 142 | input: Values{ 143 | "foo": "bar", 144 | "bar": "foo", 145 | }, 146 | }, 147 | } { 148 | t.Run(test.name, func(t *testing.T) { 149 | fs := flag.NewFlagSet(test.name, flag.ContinueOnError) 150 | fetch, input, exp, err := declare(fs, "", test.input, test.setup) 151 | if err != nil { 152 | panic(err) 153 | } 154 | 155 | err = parseFunc(input, parse.NewFlagSet(fs)) 156 | if err != nil { 157 | t.Fatalf("unexpected error: %v", err) 158 | } 159 | act := fetch() 160 | if !cmp.Equal(act, exp) { 161 | t.Fatalf( 162 | "unexpected parsed values:\n%s", 163 | cmp.Diff(exp, act), 164 | ) 165 | } 166 | }) 167 | } 168 | } 169 | 170 | func declare( 171 | fs *flag.FlagSet, prefix string, 172 | values, setup Values, 173 | ) ( 174 | fetch func() Values, 175 | input, exp Values, 176 | err error, 177 | ) { 178 | res := make(Values) 179 | exp = make(Values) 180 | input = make(Values) 181 | 182 | var funcs []func() 183 | appendFetch := func(f func()) { 184 | funcs = append(funcs, f) 185 | } 186 | defer func() { 187 | if err != nil { 188 | return 189 | } 190 | fetch = func() Values { 191 | for _, f := range funcs { 192 | f() 193 | } 194 | return res 195 | } 196 | }() 197 | 198 | for name, value := range values { 199 | var ( 200 | name = name 201 | key = join(prefix, name) 202 | ) 203 | 204 | switch v := value.(type) { 205 | case Values: 206 | s, _ := setup[name].(Values) 207 | f, in, e, err := declare(fs, join(prefix, name), v, s) 208 | if err != nil { 209 | return nil, nil, nil, err 210 | } 211 | appendFetch(func() { 212 | res[name] = f() 213 | }) 214 | exp[name] = e 215 | input[name] = in 216 | continue 217 | 218 | case []string: 219 | s := stringSlice{} 220 | fs.Var(&s, key, "") 221 | appendFetch(func() { 222 | res[name] = []string(s) 223 | }) 224 | 225 | case string: 226 | p := new(string) 227 | fs.StringVar(p, key, "", "") 228 | appendFetch(func() { 229 | res[name] = *p 230 | }) 231 | 232 | case time.Duration: 233 | p := new(time.Duration) 234 | fs.DurationVar(p, key, 0, "") 235 | appendFetch(func() { 236 | res[name] = *p 237 | }) 238 | input[name] = v.String() 239 | 240 | case float64: 241 | p := new(float64) 242 | fs.Float64Var(p, key, 0, "") 243 | appendFetch(func() { 244 | res[name] = *p 245 | }) 246 | 247 | case int: 248 | p := new(int) 249 | fs.IntVar(p, key, 0, "") 250 | appendFetch(func() { 251 | res[name] = *p 252 | }) 253 | 254 | case bool: 255 | p := new(bool) 256 | fs.BoolVar(p, key, false, "") 257 | appendFetch(func() { 258 | res[name] = *p 259 | }) 260 | 261 | default: 262 | return nil, nil, nil, fmt.Errorf("unexpected value type: %T", v) 263 | } 264 | if x, has := setup[name]; has { 265 | if err := fs.Set(key, fmt.Sprintf("%v", x)); err != nil { 266 | panic(err) 267 | } 268 | exp[name] = x 269 | } else { 270 | exp[name] = value 271 | } 272 | if _, has := input[name]; !has { 273 | input[name] = value 274 | } 275 | } 276 | 277 | return 278 | } 279 | 280 | type stringSlice []string 281 | 282 | func (s *stringSlice) Set(x string) error { 283 | *s = append(*s, x) 284 | return nil 285 | } 286 | 287 | func (s stringSlice) String() string { 288 | return strings.Join(s, ",") 289 | } 290 | 291 | func join(a, b string) string { 292 | if a == "" { 293 | return b 294 | } 295 | return a + "." + b 296 | } 297 | -------------------------------------------------------------------------------- /value.go: -------------------------------------------------------------------------------- 1 | package flagutil 2 | 3 | import ( 4 | "flag" 5 | "reflect" 6 | ) 7 | 8 | // OverrideSet returns a wrapper around v which Set() method is replaced by f. 9 | func OverrideSet(v flag.Value, f func(string) error) flag.Value { 10 | return value{ 11 | value: v, 12 | doSet: f, 13 | } 14 | } 15 | 16 | type flagSetPair [2]*flag.FlagSet 17 | 18 | func (p flagSetPair) Set(name, s string) error { 19 | for i := 0; i < len(p); i++ { 20 | if p[i] == nil { 21 | continue 22 | } 23 | if err := p[i].Set(name, s); err != nil { 24 | return err 25 | } 26 | } 27 | return nil 28 | } 29 | 30 | type value struct { 31 | value flag.Value 32 | doSet func(string) error 33 | doGet func() interface{} 34 | doString func() string 35 | doIsBoolFlag func() bool 36 | } 37 | 38 | func (v value) Set(s string) error { 39 | if fn := v.doSet; fn != nil { 40 | return fn(s) 41 | } 42 | if v := v.value; v != nil { 43 | return v.Set(s) 44 | } 45 | return nil 46 | } 47 | func (v value) Get() interface{} { 48 | if fn := v.doGet; fn != nil { 49 | return fn() 50 | } 51 | if g, ok := v.value.(flag.Getter); ok { 52 | return g.Get() 53 | } 54 | return nil 55 | } 56 | func (v value) String() string { 57 | if fn := v.doString; fn != nil { 58 | return fn() 59 | } 60 | if v := v.value; v != nil { 61 | return v.String() 62 | } 63 | return "" 64 | } 65 | func (v value) IsBoolFlag() bool { 66 | if fn := v.doIsBoolFlag; fn != nil { 67 | return fn() 68 | } 69 | if b, ok := v.value.(interface { 70 | IsBoolFlag() bool 71 | }); ok { 72 | return b.IsBoolFlag() 73 | } 74 | return false 75 | } 76 | 77 | type valuePair [2]flag.Value 78 | 79 | func (p valuePair) Set(val string) error { 80 | for _, v := range p { 81 | if err := v.Set(val); err != nil { 82 | return err 83 | } 84 | } 85 | return nil 86 | } 87 | 88 | func (p valuePair) Get() interface{} { 89 | var ( 90 | v0 interface{} 91 | v1 interface{} 92 | ) 93 | if g0, ok := p[0].(flag.Getter); ok { 94 | v0 = g0.Get() 95 | } 96 | if g1, ok := p[1].(flag.Getter); ok { 97 | v1 = g1.Get() 98 | } 99 | if !reflect.DeepEqual(v0, v1) { 100 | return nil 101 | } 102 | return v0 103 | } 104 | 105 | func (p valuePair) String() string { 106 | if p.isZero() { 107 | return "" 108 | } 109 | s0 := p[0].String() 110 | s1 := p[1].String() 111 | if s0 != s1 { 112 | return "" 113 | } 114 | return s0 115 | } 116 | 117 | func (p valuePair) IsBoolFlag() bool { 118 | if isBoolValue(p[0]) && isBoolValue(p[1]) { 119 | return true 120 | } 121 | return false 122 | } 123 | 124 | func (p valuePair) isZero() bool { 125 | return p == valuePair{} 126 | } 127 | --------------------------------------------------------------------------------