~mna/webparts-flag

e84af31ab0aac85bc4ec57598c0bbfb6f3c23827 — Martin Angers 1 year, 7 months ago
initial commit
6 files changed, 322 insertions(+), 0 deletions(-)

A .gitignore
A .golangci.toml
A flag.go
A flag_test.go
A go.mod
A go.sum
A  => .gitignore +6 -0
@@ 1,6 @@
# environment files (e.g. managed by direnv) and other secrets
/.env*

# output files for different tools, e.g. code coverage
/*.out


A  => .golangci.toml +30 -0
@@ 1,30 @@
[linters]
  disable-all = true
  enable = [
    "deadcode",
    "errcheck",
    "gochecknoinits",
    "gochecknoglobals",
    "gofmt",
    "golint",
    "gosec",
    "gosimple",
    "govet",
    "ineffassign",
    "interfacer",
    "misspell",
    "nakedret",
    "prealloc",
    "staticcheck",
    "structcheck",
    "typecheck",
    "unconvert",
    "unparam",
    "unused",
    "varcheck",
  ]

[issues]
  # regexps of issue texts to exclude
  exclude = [
  ]

A  => flag.go +95 -0
@@ 1,95 @@
// 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 uses the stdlib's flag package internally, and as such shares
// the same behaviour regarding short and long flags.
package flag

import (
	"flag"
	"fmt"
	"io/ioutil"
	"reflect"
	"strings"
	"time"
)

// 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 v has a SetArgs method, it is called with the list
// of non-flag arguments.
//
// 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 Parse(args []string, v interface{}) error {
	// create a FlagSet that is silent and only returns any error
	// it encounters.
	fs := flag.NewFlagSet("", flag.ContinueOnError)
	fs.SetOutput(ioutil.Discard)
	fs.Usage = nil

	durationType := reflect.TypeOf(time.Duration(0))

	// extract the flags from the struct
	val := reflect.ValueOf(v).Elem()
	str := reflect.TypeOf(v).Elem()
	count := val.NumField()
	for i := 0; i < count; i++ {
		fld := val.Field(i)
		typ := str.Field(i)
		names := strings.Split(typ.Tag.Get("flag"), ",")

		for _, nm := range names {
			if nm == "" {
				continue
			}
			switch fld.Kind() {
			case reflect.Bool:
				fs.BoolVar(fld.Addr().Interface().(*bool), nm, fld.Bool(), "")
			case reflect.String:
				fs.StringVar(fld.Addr().Interface().(*string), nm, fld.String(), "")
			case reflect.Int:
				fs.IntVar(fld.Addr().Interface().(*int), nm, int(fld.Int()), "")
			default:
				switch typ.Type {
				case durationType:
					fs.DurationVar(fld.Addr().Interface().(*time.Duration), nm, fld.Interface().(time.Duration), "")
				default:
					panic(fmt.Sprintf("unsupported flag field kind: %s (%s: %s)", fld.Kind(), typ.Name, typ.Type))
				}
			}
		}
	}

	if err := fs.Parse(args); err != nil {
		return err
	}

	if sa, ok := v.(interface{ SetArgs([]string) }); ok {
		args := fs.Args()
		if len(args) == 0 {
			args = nil
		}
		sa.SetArgs(args)
	}
	if sf, ok := v.(interface{ SetFlags(map[string]bool) }); ok {
		set := make(map[string]bool)
		fs.Visit(func(fl *flag.Flag) {
			set[fl.Name] = true
		})
		if len(set) == 0 {
			set = nil
		}
		sf.SetFlags(set)
	}

	return nil
}

A  => flag_test.go +176 -0
@@ 1,176 @@
package flag

import (
	"strings"
	"testing"
	"time"

	"github.com/stretchr/testify/require"
)

type F struct {
	S     string        `flag:"s,string,long-string"`
	I     int           `flag:"i,int"`
	B     bool          `flag:"b"`
	H     bool          `flag:"h,help"`
	T     time.Duration `flag:"t"`
	N     int
	args  []string
	flags map[string]bool
}

func (f *F) SetArgs(args []string) {
	f.args = args
}

func (f *F) SetFlags(flags map[string]bool) {
	f.flags = flags
}

func TestParseFlags(t *testing.T) {
	cases := []struct {
		args []string
		want *F
		err  string
	}{
		{
			want: &F{},
		},
		{
			args: []string{"toto"},
			want: &F{
				args: []string{"toto"},
			},
		},
		{
			args: []string{"-h"},
			want: &F{
				H:     true,
				flags: map[string]bool{"h": true},
			},
		},
		{
			args: []string{"-i", "10", "--int", "20"},
			want: &F{
				I:     20,
				flags: map[string]bool{"i": true, "int": true},
			},
		},
		{
			args: []string{"-i", "10", "--int", "20"},
			want: &F{
				I:     20,
				flags: map[string]bool{"i": true, "int": true},
			},
		},
		{
			args: []string{"-s", "a", "--string", "b", "-long-string", "c"},
			want: &F{
				S:     "c",
				flags: map[string]bool{"s": true, "string": true, "long-string": true},
			},
		},
		{
			args: []string{"-b", "--b", "-b"},
			want: &F{
				B:     true,
				flags: map[string]bool{"b": true},
			},
		},
		{
			args: []string{"-b", "-int", "1", "-string", "a", "arg1", "arg2"},
			want: &F{
				B:     true,
				I:     1,
				S:     "a",
				args:  []string{"arg1", "arg2"},
				flags: map[string]bool{"b": true, "int": true, "string": true},
			},
		},
		{
			args: []string{"-n", "1"},
			want: &F{},
			err:  "not defined: -n",
		},
		{
			args: []string{"-t", "3s"},
			want: &F{
				T:     3 * time.Second,
				flags: map[string]bool{"t": true},
			},
		},
		{
			args: []string{"-t", "nope"},
			want: &F{},
			err:  "invalid value",
		},
	}

	for _, c := range cases {
		t.Run(strings.Join(c.args, " "), func(t *testing.T) {
			var f F
			err := Parse(c.args, &f)

			if c.err != "" {
				require.Error(t, err)
				require.Contains(t, err.Error(), c.err)
				return
			}

			require.NoError(t, err)
			require.Equal(t, c.want, &f)
		})
	}
}

func TestParseNoFlag(t *testing.T) {
	type F struct {
		V int
	}
	f := F{V: 4}
	err := Parse([]string{"x"}, &f)
	require.NoError(t, err)
	require.Equal(t, 4, f.V)
}

type noFlagSetArgs struct {
	args []string
}

func (n *noFlagSetArgs) SetArgs(args []string) {
	n.args = args
}

func TestParseNoFlagSetArgs(t *testing.T) {
	f := noFlagSetArgs{}
	err := Parse([]string{"x"}, &f)
	require.NoError(t, err)
	require.Equal(t, []string{"x"}, f.args)
}

func TestParseArgsError(t *testing.T) {
	type F struct {
		X bool `flag:"x"`
	}
	f := F{}
	err := Parse([]string{"-zz"}, &f)
	require.Error(t, err)
	require.Contains(t, err.Error(), "-zz")
}

func TestParseNotStructPointer(t *testing.T) {
	var i int
	require.Panics(t, func() {
		_ = Parse([]string{"-h"}, i)
	})
}

func TestParseUnsupportedFlagType(t *testing.T) {
	type F struct {
		C *bool `flag:"c"`
	}
	var f F
	require.Panics(t, func() {
		_ = Parse([]string{"-h"}, &f)
	})
}

A  => go.mod +5 -0
@@ 1,5 @@
module git.sr.ht/~mna/webparts-flag

go 1.13

require github.com/stretchr/testify v1.4.0

A  => go.sum +10 -0
@@ 1,10 @@
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/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=
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=