~mna/webparts-flag

306cfcede5b817c470e3564493265d6125d97956 — Martin Angers 4 years ago e84af31
implement parsing with and without env vars
5 files changed, 124 insertions(+), 28 deletions(-)

A README.md
M flag.go
M flag_test.go
M go.mod
M go.sum
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=