A README.md => README.md +13 -0
@@ 0,0 1,13 @@
+# webparts-flag
+
+[![GoDoc](https://godoc.org/git.sr.ht/~mna/webparts-flag?status.svg)](https://godoc.org/git.sr.ht/~mna/webparts-flag)
+
+This repository provides an implementation of the `webparts/flag` standard interface
+using the standard library, a custom flag-to-struct mapping, and the
+`github.com/kelseyhightower/envconfig` package.
+
+## see also
+
+* webparts: https://git.sr.ht/~mna/webparts
+* envconfig: github.com/kelseyhightower/envconfig
+
M flag.go => flag.go +75 -19
@@ 1,38 1,81 @@
-// Package flag implements a command-line flags parser that uses
-// struct tags to configure supported flags and returns any error
-// it encounters, without printing anything automatically.
+// Package flag implements a command-line flags parser that uses struct tags to
+// configure supported flags and returns any error it encounters, without
+// printing anything automatically. It can optionally read flag values from
+// environment variables first, with the command-line flags used to override
+// them.
//
-// It uses the stdlib's flag package internally, and as such shares
+// The struct tag to specify flags is `flag`, while the one to specify
+// environment variables is `envconfig`. See the envconfig package for full
+// details on struct tags configuration and decoding support:
+// https://github.com/kelseyhightower/envconfig.
+//
+// Flag parsing uses the stdlib's flag package internally, and as such shares
// the same behaviour regarding short and long flags.
package flag
import (
- "flag"
+ stdflag "flag"
"fmt"
"io/ioutil"
"reflect"
"strings"
"time"
+
+ "git.sr.ht/~mna/webparts/flag"
+ "github.com/kelseyhightower/envconfig"
)
-// Parse parses args into v, using struct tags to detect flags.
-// The tag must be named "flag" and multiple flags may be set
-// for the same field using a comma-separated list.
-// v must be a pointer to a struct and the flags must be defined
-// on fields with a type of string, int, bool or time.Duration.
+var _ flag.Parser = (*Parser)(nil)
+
+// Parser implements a flag parser.
+type Parser struct {
+ // EnvVars indicates if environment variables are used to read flag values.
+ EnvVars bool
+
+ // EnvPrefix is the prefix to use in front of each flag's environment
+ // variable name. If it is empty, the name of the program (as read from the
+ // args slice at index 0) is used, with dashes replaced with underscores.
+ EnvPrefix string
+}
+
+// Parse parses args into v, using struct tags to detect flags. The tag must
+// be named "flag" and multiple flags may be set for the same field using a
+// comma-separated list. v must be a pointer to a struct and the flags must be
+// defined on fields with a type of string, int, bool or time.Duration.
+// If Parser.EnvVars is true, flag values are initialized from corresponding
+// environment variables first.
//
-// If v has a SetArgs method, it is called with the list
-// of non-flag arguments.
+// After parsing, if v implements a Validate method that returns an error, it
+// is called and any non-nil error is returned as error.
//
-// If v has a SetFlags method, it is called with the set of
-// flags that were set by args (a map[string]bool).
+// If v has a SetArgs method, it is called with the list of non-flag arguments.
//
-// It panics if v is not a pointer to a struct or if a flag is
-// defined with an unsupported type.
-func Parse(args []string, v interface{}) error {
+// If v has a SetFlags method, it is called with the set of flags that were set
+// by args (a map[string]bool).
+//
+// It panics if v is not a pointer to a struct or if a flag is defined with an
+// unsupported type.
+func (p *Parser) Parse(args []string, v interface{}) error {
+ if p.EnvVars {
+ if err := p.parseEnvVars(args, v); err != nil {
+ return err
+ }
+ }
+
+ if err := p.parseFlags(args, v); err != nil {
+ return err
+ }
+
+ if val, ok := v.(interface{ Validate() error }); ok {
+ return val.Validate()
+ }
+ return nil
+}
+
+func (p *Parser) parseFlags(args []string, v interface{}) error {
// create a FlagSet that is silent and only returns any error
// it encounters.
- fs := flag.NewFlagSet("", flag.ContinueOnError)
+ fs := stdflag.NewFlagSet("", stdflag.ContinueOnError)
fs.SetOutput(ioutil.Discard)
fs.Usage = nil
@@ 82,7 125,7 @@ func Parse(args []string, v interface{}) error {
}
if sf, ok := v.(interface{ SetFlags(map[string]bool) }); ok {
set := make(map[string]bool)
- fs.Visit(func(fl *flag.Flag) {
+ fs.Visit(func(fl *stdflag.Flag) {
set[fl.Name] = true
})
if len(set) == 0 {
@@ 93,3 136,16 @@ func Parse(args []string, v interface{}) error {
return nil
}
+
+func (p *Parser) parseEnvVars(args []string, v interface{}) error {
+ prefix := p.EnvPrefix
+
+ if prefix == "" && len(args) > 0 {
+ prefix = prefixFromProgramName(args[0])
+ }
+ return envconfig.Process(prefix, v)
+}
+
+func prefixFromProgramName(name string) string {
+ return strings.ReplaceAll(name, "-", "_")
+}
M flag_test.go => flag_test.go +19 -8
@@ 106,10 106,11 @@ func TestParseFlags(t *testing.T) {
},
}
+ var p Parser
for _, c := range cases {
t.Run(strings.Join(c.args, " "), func(t *testing.T) {
var f F
- err := Parse(c.args, &f)
+ err := p.Parse(c.args, &f)
if c.err != "" {
require.Error(t, err)
@@ 127,8 128,10 @@ func TestParseNoFlag(t *testing.T) {
type F struct {
V int
}
+ var p Parser
+
f := F{V: 4}
- err := Parse([]string{"x"}, &f)
+ err := p.Parse([]string{"x"}, &f)
require.NoError(t, err)
require.Equal(t, 4, f.V)
}
@@ 142,8 145,9 @@ func (n *noFlagSetArgs) SetArgs(args []string) {
}
func TestParseNoFlagSetArgs(t *testing.T) {
+ var p Parser
f := noFlagSetArgs{}
- err := Parse([]string{"x"}, &f)
+ err := p.Parse([]string{"x"}, &f)
require.NoError(t, err)
require.Equal(t, []string{"x"}, f.args)
}
@@ 152,16 156,20 @@ func TestParseArgsError(t *testing.T) {
type F struct {
X bool `flag:"x"`
}
+ var p Parser
f := F{}
- err := Parse([]string{"-zz"}, &f)
+ err := p.Parse([]string{"-zz"}, &f)
require.Error(t, err)
require.Contains(t, err.Error(), "-zz")
}
func TestParseNotStructPointer(t *testing.T) {
- var i int
+ var (
+ i int
+ p Parser
+ )
require.Panics(t, func() {
- _ = Parse([]string{"-h"}, i)
+ _ = p.Parse([]string{"-h"}, i)
})
}
@@ 169,8 177,11 @@ func TestParseUnsupportedFlagType(t *testing.T) {
type F struct {
C *bool `flag:"c"`
}
- var f F
+ var (
+ f F
+ p Parser
+ )
require.Panics(t, func() {
- _ = Parse([]string{"-h"}, &f)
+ _ = p.Parse([]string{"-h"}, &f)
})
}
M go.mod => go.mod +5 -1
@@ 2,4 2,8 @@ module git.sr.ht/~mna/webparts-flag
go 1.13
-require github.com/stretchr/testify v1.4.0
+require (
+ git.sr.ht/~mna/webparts v0.0.0-20191024173037-cf0620d9e43b
+ github.com/kelseyhightower/envconfig v1.4.0
+ github.com/stretchr/testify v1.4.0
+)
M go.sum => go.sum +12 -0
@@ 1,10 1,22 @@
+git.sr.ht/~mna/webparts v0.0.0-20191024173037-cf0620d9e43b h1:VZcf/H2WjldGpmIF7miFr9ZvVY81kAsdJhaVPPJYEXQ=
+git.sr.ht/~mna/webparts v0.0.0-20191024173037-cf0620d9e43b/go.mod h1:I9CmJibFclVrCKUIpZPCOvEMyx58vu6+Ej3rXE1xWzM=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
+github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
+github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20191018212557-ed542cd5b28a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=