├── LICENSE ├── README.md ├── arg.go ├── arg_test.go ├── arity.go ├── builtin.go ├── bytes.go ├── cases_test.go ├── doc.go ├── errors.go ├── excess.go ├── go.mod ├── go.sum ├── marshalers.go ├── misc.go ├── parseopt.go ├── parser.go ├── tagflag.go ├── tagflag_test.go └── usage.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Matt Joiner 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 | # tagflag 2 | 3 | [![GoDoc](https://godoc.org/github.com/anacrolix/tagflag?status.svg)](https://godoc.org/github.com/anacrolix/tagflag) 4 | 5 | See the thorough package documentation on [GoDoc](https://godoc.org/github.com/anacrolix/tagflag). I know you want to see this in the README, but just go to the GoDoc. 6 | 7 | ## Similar projects 8 | 9 | * https://github.com/jaffee/commandeer 10 | * https://github.com/octago/sflags 11 | -------------------------------------------------------------------------------- /arg.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | type arg struct { 9 | arity arity 10 | name string 11 | help string 12 | value reflect.Value 13 | } 14 | 15 | func (me arg) hasZeroValue() bool { 16 | return reflect.DeepEqual( 17 | reflect.Zero(me.value.Type()).Interface(), 18 | me.value.Interface()) 19 | } 20 | 21 | func (me arg) marshal(s string, explicitValue bool) error { 22 | m := valueMarshaler(me.value.Type()) 23 | if m.RequiresExplicitValue() && !explicitValue { 24 | return userError{fmt.Sprintf("explicit value required (%s%s=VALUE)", flagPrefix, me.name)} 25 | } 26 | return m.Marshal(me.value, s) 27 | } 28 | -------------------------------------------------------------------------------- /arg_test.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "net" 5 | "reflect" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestEqualZeroArgValue(t *testing.T) { 12 | a := arg{value: reflect.ValueOf(net.IP(nil))} 13 | assert.True(t, a.hasZeroValue()) 14 | b := arg{value: reflect.ValueOf(net.ParseIP("127.0.0.1"))} 15 | assert.False(t, b.hasZeroValue()) 16 | } 17 | -------------------------------------------------------------------------------- /arity.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | const infArity = 1000 9 | 10 | type arity struct { 11 | min, max int 12 | } 13 | 14 | func fieldArity(v reflect.Value, sf reflect.StructField) (arity arity) { 15 | arity.min = 1 16 | arity.max = 1 17 | if v.Kind() == reflect.Slice { 18 | arity.max = infArity 19 | } 20 | if sf.Tag.Get("arity") != "" { 21 | switch sf.Tag.Get("arity") { 22 | case "?": 23 | arity.min = 0 24 | case "*": 25 | arity.min = 0 26 | arity.max = infArity 27 | case "+": 28 | arity.max = infArity 29 | default: 30 | panic(fmt.Sprintf("unhandled arity tag: %q", sf.Tag.Get("arity"))) 31 | } 32 | } 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /builtin.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net" 7 | "net/url" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "golang.org/x/xerrors" 14 | ) 15 | 16 | var builtinMarshalers = map[reflect.Type]marshaler{} 17 | 18 | // Convenience function to allow adding marshalers using typed functions. 19 | // marshalFunc is of type func(arg string) T or func(arg string) (T, error), 20 | // where T is the type the function can marshal. 21 | func addBuiltinDynamicMarshaler(marshalFunc interface{}, explicitValueRequired bool) { 22 | marshalFuncValue := reflect.ValueOf(marshalFunc) 23 | marshalType := marshalFuncValue.Type().Out(0) 24 | builtinMarshalers[marshalType] = dynamicMarshaler{ 25 | marshal: func(marshalValue reflect.Value, arg string) error { 26 | out := marshalFuncValue.Call([]reflect.Value{reflect.ValueOf(arg)}) 27 | marshalValue.Set(out[0]) 28 | if len(out) > 1 { 29 | i := out[1].Interface() 30 | if i != nil { 31 | return i.(error) 32 | } 33 | } 34 | return nil 35 | }, 36 | explicitValueRequired: explicitValueRequired, 37 | } 38 | } 39 | 40 | func init() { 41 | // These are some simple builtin types that are nice to be handled without 42 | // wrappers that implement Marshaler. Note that if they return pointer 43 | // types, those must be used in the flag struct, because there's no way to 44 | // know that nothing depends on the address returned. 45 | addBuiltinDynamicMarshaler(func(urlStr string) (*url.URL, error) { 46 | return url.Parse(urlStr) 47 | }, false) 48 | // Empty strings for this type are valid, so we enforce that the value is 49 | // explicit (=), so that the user knows what they're getting into. 50 | addBuiltinDynamicMarshaler(func(s string) (*net.TCPAddr, error) { 51 | if s == "" { 52 | return nil, nil 53 | } 54 | var ret net.TCPAddr 55 | retAlt, err := parseIpPortZone(s) 56 | if err != nil { 57 | return nil, err 58 | } 59 | ret = net.TCPAddr(retAlt) 60 | return &ret, err 61 | }, true) 62 | addBuiltinDynamicMarshaler(func(s string) (time.Duration, error) { 63 | return time.ParseDuration(s) 64 | }, false) 65 | addBuiltinDynamicMarshaler(func(s string) (ip net.IP, err error) { 66 | ip = net.ParseIP(s) 67 | if ip == nil { 68 | err = fmt.Errorf("failed to parse IP") 69 | } 70 | return 71 | }, false) 72 | } 73 | 74 | func parseIpAddr(host string) (ret net.IPAddr, err error) { 75 | ss := strings.SplitN(host, "%", 2) 76 | ret.IP = net.ParseIP(ss[0]) 77 | if ret.IP == nil && ss[0] != "" { 78 | err = errors.New("error parsing IP") 79 | return 80 | } 81 | if len(ss) >= 2 { 82 | ret.Zone = ss[1] 83 | } 84 | return 85 | } 86 | 87 | type ipPortZone struct { 88 | IP net.IP 89 | Port int 90 | Zone string 91 | } 92 | 93 | func parseIpPortZone(hostport string) (ret ipPortZone, err error) { 94 | host, port, err := net.SplitHostPort(hostport) 95 | if err != nil { 96 | return 97 | } 98 | portInt64, err := strconv.ParseInt(port, 10, 0) 99 | if err != nil { 100 | return 101 | } 102 | ret.Port = int(portInt64) 103 | ipAddr, err := parseIpAddr(host) 104 | if err != nil { 105 | err = xerrors.Errorf("parsing host %q: %w", host, err) 106 | return 107 | } 108 | ret.IP = ipAddr.IP 109 | ret.Zone = ipAddr.Zone 110 | return 111 | } 112 | -------------------------------------------------------------------------------- /bytes.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "encoding" 5 | 6 | "github.com/dustin/go-humanize" 7 | ) 8 | 9 | // A nice builtin type that will marshal human readable byte quantities to 10 | // int64. For example 100GB. See https://godoc.org/github.com/dustin/go-humanize. 11 | type Bytes int64 12 | 13 | var ( 14 | _ Marshaler = (*Bytes)(nil) 15 | _ encoding.TextUnmarshaler = (*Bytes)(nil) 16 | ) 17 | 18 | func (me *Bytes) Marshal(s string) (err error) { 19 | ui64, err := humanize.ParseBytes(s) 20 | if err != nil { 21 | return 22 | } 23 | *me = Bytes(ui64) 24 | return 25 | } 26 | 27 | func (me *Bytes) UnmarshalText(text []byte) error { 28 | return me.Marshal(string(text)) 29 | } 30 | 31 | func (*Bytes) RequiresExplicitValue() bool { 32 | return false 33 | } 34 | 35 | func (me Bytes) Int64() int64 { 36 | return int64(me) 37 | } 38 | 39 | func (me Bytes) String() string { 40 | return humanize.Bytes(uint64(me)) 41 | } 42 | -------------------------------------------------------------------------------- /cases_test.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type parseCase struct { 11 | args []string 12 | err func(*testing.T, error) 13 | expected interface{} 14 | } 15 | 16 | func noErrorCase(expected interface{}, args ...string) parseCase { 17 | return parseCase{args: args, expected: expected} 18 | } 19 | 20 | func errorCase(err error, args ...string) parseCase { 21 | return parseCase{ 22 | args: args, 23 | err: func(t *testing.T, actualErr error) { 24 | assert.EqualValues(t, err, actualErr) 25 | }, 26 | } 27 | } 28 | 29 | func anyErrorCase(args ...string) parseCase { 30 | return parseCase{ 31 | args: args, 32 | err: func(t *testing.T, err error) { 33 | assert.Error(t, err) 34 | }, 35 | } 36 | } 37 | 38 | func (me parseCase) Run(t *testing.T, newCmd func() interface{}) { 39 | cmd := newCmd() 40 | err := ParseErr(cmd, me.args) 41 | if me.err == nil { 42 | assert.NoError(t, err) 43 | assert.EqualValues(t, me.expected, reflect.ValueOf(cmd).Elem().Interface(), "%v", me) 44 | } else { 45 | me.err(t, err) 46 | } 47 | } 48 | 49 | func RunCases(t *testing.T, cases []parseCase, newCmd func() interface{}) { 50 | for _, _case := range cases { 51 | _case.Run(t, newCmd) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package tagflag uses reflection to derive flags and positional arguments to a 2 | // program, and parses and sets them from a slice of arguments. 3 | // 4 | // For example: 5 | // var opts struct { 6 | // Mmap bool `help:"memory-map torrent data"` 7 | // TestPeer []*net.TCPAddr `help:"addresses of some starting peers"` 8 | // tagflag.StartPos // Marks beginning of positional arguments. 9 | // Torrent []string `arity:"+" help:"torrent file path or magnet uri"` 10 | // } 11 | // tagflag.Parse(&opts) 12 | // 13 | // Supported tags include: 14 | // help: a line of text to show after the option 15 | // arity: defaults to 1. the number of arguments a field requires, or ? for one 16 | // optional argument, + for one or more, or * for zero or more. 17 | // 18 | // MarshalArgs is called on fields that implement ArgsMarshaler. A number of 19 | // arguments matching the arity of the field are passed if possible. 20 | // 21 | // Slices will collect successive values, within the provided arity constraints. 22 | // 23 | // A few helpful types have builtin marshallers, for example Bytes, 24 | // *net.TCPAddr, *url.URL, time.Duration, and net.IP. 25 | // 26 | // Flags are strictly passed with the form -K or -K=V. No space between -K and 27 | // the value is allowed. This allows positional arguments to be mixed in with 28 | // flags, and prevents any confusion due to some flags occasionally not taking 29 | // values. A `--` will terminate flag parsing, and treat all further arguments 30 | // as positional. 31 | // 32 | // A builtin help and usage printer are provided, and activated when passing 33 | // -h or -help. 34 | // 35 | // Flag and positional argument names are automatically munged to fit the 36 | // standard scheme within tagflag. 37 | package tagflag 38 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | type userError struct { 4 | msg string 5 | } 6 | 7 | func (ue userError) Error() string { 8 | return ue.msg 9 | } 10 | -------------------------------------------------------------------------------- /excess.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import "fmt" 4 | 5 | // The error returned if there are fields in a struct after ExcessArgs. 6 | var ErrFieldsAfterExcessArgs = fmt.Errorf("field(s) after %T", ExcessArgs{}) 7 | 8 | // This should be added to the end of a struct to soak up any arguments that didn't fit sooner. 9 | type ExcessArgs []string 10 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/anacrolix/tagflag 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/anacrolix/missinggo v1.3.0 // indirect 7 | github.com/anacrolix/missinggo/v2 v2.6.0 8 | github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 9 | github.com/dustin/go-humanize v1.0.0 10 | github.com/huandu/xstrings v1.3.2 11 | github.com/kr/pretty v0.3.0 // indirect 12 | github.com/pkg/errors v0.9.1 13 | github.com/rogpeppe/go-internal v1.8.0 // indirect 14 | github.com/stretchr/testify v1.7.0 15 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | crawshaw.io/iox v0.0.0-20181124134642-c51c3df30797/go.mod h1:sXBiorCo8c46JlQV3oXPKINnZ8mcqnye1EkVkqsectk= 4 | crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/RoaringBitmap/roaring v0.4.7/go.mod h1:8khRDP4HmeXns4xIj9oGrKSz7XTQiJx2zgh7AcNke4w= 7 | github.com/RoaringBitmap/roaring v0.4.17/go.mod h1:D3qVegWTmfCaX4Bl5CrBE9hfrSrrXIr8KVNvRsDi1NI= 8 | github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo= 9 | github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= 10 | github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= 11 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 12 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 13 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 14 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 15 | github.com/anacrolix/envpprof v0.0.0-20180404065416-323002cec2fa/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= 16 | github.com/anacrolix/envpprof v1.0.0/go.mod h1:KgHhUaQMc8cC0+cEflSgCFNFbKwi5h54gqtVn8yhP7c= 17 | github.com/anacrolix/envpprof v1.1.0/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4= 18 | github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= 19 | github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU= 20 | github.com/anacrolix/missinggo v1.1.0/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= 21 | github.com/anacrolix/missinggo v1.1.2-0.20190815015349-b888af804467/go.mod h1:MBJu3Sk/k3ZfGYcS7z18gwfu72Ey/xopPFJJbTi5yIo= 22 | github.com/anacrolix/missinggo v1.2.1/go.mod h1:J5cMhif8jPmFoC3+Uvob3OXXNIhOUikzMt+uUjeM21Y= 23 | github.com/anacrolix/missinggo v1.3.0 h1:06HlMsudotL7BAELRZs0yDZ4yVXsHXGi323QBjAVASw= 24 | github.com/anacrolix/missinggo v1.3.0/go.mod h1:bqHm8cE8xr+15uVfMG3BFui/TxyB6//H5fwlq/TeqMc= 25 | github.com/anacrolix/missinggo/perf v1.0.0/go.mod h1:ljAFWkBuzkO12MQclXzZrosP5urunoLS0Cbvb4V0uMQ= 26 | github.com/anacrolix/missinggo/v2 v2.2.0/go.mod h1:o0jgJoYOyaoYQ4E2ZMISVa9c88BbUBVQQW4QeRkNCGY= 27 | github.com/anacrolix/missinggo/v2 v2.5.1/go.mod h1:WEjqh2rmKECd0t1VhQkLGTdIWXO6f6NLjp5GlMZ+6FA= 28 | github.com/anacrolix/missinggo/v2 v2.6.0 h1:kHkn6nLy1isWYV4mthZX8itV1bRd2mwFVuXrxzJ4VX0= 29 | github.com/anacrolix/missinggo/v2 v2.6.0/go.mod h1:2IZIvmRTizALNYFYXsPR7ofXPzJgyBpKZ4kMqMEICkI= 30 | github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg= 31 | github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= 32 | github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw= 33 | github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8= 34 | github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= 35 | github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI= 36 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 37 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 38 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 39 | github.com/bradfitz/iter v0.0.0-20140124041915-454541ec3da2/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= 40 | github.com/bradfitz/iter v0.0.0-20190303215204-33e6a9893b0c/go.mod h1:PyRFw1Lt2wKX4ZVSQ2mk+PeDa1rxyObEDlApuIsUKuo= 41 | github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 h1:GKTyiRCL6zVf5wWaqKnf+7Qs6GbEPfd4iMOitWzXJx8= 42 | github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8/go.mod h1:spo1JLcs67NmW1aVLEgtA8Yy1elc+X8y5SRW1sFW4Og= 43 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 44 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 45 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 46 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 47 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 48 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 49 | github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= 50 | github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 51 | github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= 52 | github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= 53 | github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= 54 | github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= 55 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 56 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 57 | github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= 58 | github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= 59 | github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= 60 | github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= 61 | github.com/glycerine/goconvey v0.0.0-20190315024820-982ee783a72e/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= 62 | github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= 63 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 64 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 65 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 66 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 67 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 68 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 69 | github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 70 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 71 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 72 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 73 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 74 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 75 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 76 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 77 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 78 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 79 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 80 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 81 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 82 | github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 83 | github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 84 | github.com/google/btree v0.0.0-20180124185431-e89373fe6b4a/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 85 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 86 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 87 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 88 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 89 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 90 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 91 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 92 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 93 | github.com/gopherjs/gopherjs v0.0.0-20190309154008-847fc94819f9/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 94 | github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 95 | github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= 96 | github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 97 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 98 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 99 | github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo= 100 | github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4= 101 | github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 102 | github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= 103 | github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= 104 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 105 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 106 | github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 107 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 108 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 109 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 110 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 111 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 112 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 113 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 114 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 115 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 116 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 117 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 118 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 119 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 120 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 121 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 122 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 123 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 124 | github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg= 125 | github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw= 126 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 127 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 128 | github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 129 | github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= 130 | github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= 131 | github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= 132 | github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= 133 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 134 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 135 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 136 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 137 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 138 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 139 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 140 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 141 | github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= 142 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 143 | github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= 144 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 145 | github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 146 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 147 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 148 | github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 149 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 150 | github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= 151 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 152 | github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 153 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 154 | github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= 155 | github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 156 | github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 157 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= 158 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 159 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 160 | github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= 161 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 162 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 163 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 164 | github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 165 | github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= 166 | github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= 167 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 168 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 169 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 170 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 171 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 172 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 173 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 174 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 175 | github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 176 | github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 177 | github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= 178 | github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= 179 | github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= 180 | go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 181 | go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= 182 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 183 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 184 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 185 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 186 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 187 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 188 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 189 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 190 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 191 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 192 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 193 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 194 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 195 | golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 196 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 197 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 198 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 199 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 200 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 201 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 202 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 203 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 204 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 205 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 206 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 207 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 208 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 209 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 210 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 211 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 212 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 213 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 214 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 215 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 216 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 217 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 218 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 219 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 220 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 221 | golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 222 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 223 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 224 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 225 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 226 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 227 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 228 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 229 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 230 | google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= 231 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 232 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 233 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 234 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 235 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 236 | google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= 237 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 238 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 239 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 240 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 241 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 242 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 243 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 244 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 245 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 246 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 247 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 248 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 249 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 250 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 251 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 252 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 253 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 254 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 255 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 256 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 257 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 258 | honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 259 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 260 | -------------------------------------------------------------------------------- /marshalers.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strconv" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // TODO: Perhaps this should embed encoding.TextUnmarshaler instead. 12 | type Marshaler interface { 13 | Marshal(in string) error 14 | // Must have and ignore a pointer receiver. 15 | RequiresExplicitValue() bool 16 | } 17 | 18 | type marshaler interface { 19 | Marshal(reflect.Value, string) error 20 | RequiresExplicitValue() bool 21 | } 22 | 23 | type dynamicMarshaler struct { 24 | explicitValueRequired bool 25 | marshal func(reflect.Value, string) error 26 | } 27 | 28 | func (me dynamicMarshaler) Marshal(v reflect.Value, s string) error { 29 | return me.marshal(v, s) 30 | } 31 | 32 | func (me dynamicMarshaler) RequiresExplicitValue() bool { 33 | return me.explicitValueRequired 34 | } 35 | 36 | // The fallback marshaler, that attempts to use fmt.Sscan, and recursion to 37 | // sort marshal types. 38 | type defaultMarshaler struct{} 39 | 40 | func (defaultMarshaler) Marshal(v reflect.Value, s string) error { 41 | switch v.Kind() { 42 | case reflect.Slice: 43 | n := reflect.New(v.Type().Elem()) 44 | m := valueMarshaler(n.Elem().Type()) 45 | if m == nil { 46 | return fmt.Errorf("can't marshal type %s", n.Elem().Type()) 47 | } 48 | err := m.Marshal(n.Elem(), s) 49 | if err != nil { 50 | return err 51 | } 52 | v.Set(reflect.Append(v, n.Elem())) 53 | return nil 54 | case reflect.Int: 55 | x, err := strconv.ParseInt(s, 0, 0) 56 | v.SetInt(x) 57 | return err 58 | case reflect.Uint: 59 | x, err := strconv.ParseUint(s, 0, 0) 60 | v.SetUint(x) 61 | return err 62 | case reflect.Int64: 63 | x, err := strconv.ParseInt(s, 0, 64) 64 | v.SetInt(x) 65 | return err 66 | case reflect.String: 67 | v.SetString(s) 68 | return nil 69 | case reflect.Array: 70 | if v.Type().Elem().Kind() == reflect.Uint8 { 71 | if len(s) == 2*v.Len() { 72 | var sl []byte = v.Slice(0, v.Len()).Interface().([]byte) 73 | // log.Println(cap(sl), len(sl)) 74 | // hex.DecodeString(s) 75 | _, err := fmt.Sscanf(s, "%x", &sl) 76 | // log.Println(cap(sl), sl, err) 77 | if err != nil { 78 | return errors.Wrapf(err, "scanning hex") 79 | } 80 | // Seems the slice moves as part of the decoding :| 81 | reflect.Copy(v, reflect.ValueOf(sl)) 82 | } else { 83 | return errors.Errorf("argument has unhandled length %d", len(s)) 84 | } 85 | return nil 86 | } else { 87 | return errors.Errorf("unhandled array elem type: %s", v.Type().String()) 88 | } 89 | default: 90 | return fmt.Errorf("unhandled builtin type: %s", v.Type().String()) 91 | } 92 | } 93 | 94 | func (defaultMarshaler) RequiresExplicitValue() bool { 95 | return true 96 | } 97 | 98 | type ptrMarshaler struct { 99 | inner marshaler 100 | } 101 | 102 | func (me ptrMarshaler) Marshal(v reflect.Value, s string) error { 103 | elemValue := reflect.New(v.Type().Elem()) 104 | v.Set(elemValue) 105 | return me.inner.Marshal(elemValue.Elem(), s) 106 | } 107 | 108 | func (me ptrMarshaler) RequiresExplicitValue() bool { 109 | return false 110 | } 111 | -------------------------------------------------------------------------------- /misc.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "unicode" 7 | 8 | "github.com/bradfitz/iter" 9 | ) 10 | 11 | const flagPrefix = "-" 12 | 13 | // Walks the fields of the given struct, calling the function with the value 14 | // and StructField for each field. Returning true from the function will halt 15 | // traversal. 16 | func foreachStructField(_struct reflect.Value, f func(fv reflect.Value, sf reflect.StructField) (stop bool)) { 17 | t := _struct.Type() 18 | for i := range iter.N(t.NumField()) { 19 | sf := t.Field(i) 20 | fv := _struct.Field(i) 21 | if f(fv, sf) { 22 | break 23 | } 24 | } 25 | } 26 | 27 | func canMarshal(f reflect.Value) bool { 28 | return valueMarshaler(f.Type()) != nil 29 | } 30 | 31 | // Returns a marshaler for the given value, or nil if there isn't one. 32 | func valueMarshaler(t reflect.Type) marshaler { 33 | if zm, ok := reflect.Zero(reflect.PtrTo(t)).Interface().(Marshaler); ok { 34 | return dynamicMarshaler{ 35 | marshal: func(v reflect.Value, s string) error { 36 | return v.Addr().Interface().(Marshaler).Marshal(s) 37 | }, 38 | explicitValueRequired: zm.RequiresExplicitValue(), 39 | } 40 | } 41 | if bm, ok := builtinMarshalers[t]; ok { 42 | return bm 43 | } 44 | switch t.Kind() { 45 | case reflect.Ptr: 46 | m := valueMarshaler(t.Elem()) 47 | if m == nil { 48 | return nil 49 | } 50 | return ptrMarshaler{m} 51 | case reflect.Struct: 52 | return nil 53 | case reflect.Bool: 54 | return dynamicMarshaler{ 55 | marshal: func(v reflect.Value, s string) error { 56 | if s == "" { 57 | v.SetBool(true) 58 | return nil 59 | } 60 | b, err := strconv.ParseBool(s) 61 | v.SetBool(b) 62 | return err 63 | }, 64 | explicitValueRequired: false, 65 | } 66 | } 67 | return defaultMarshaler{} 68 | } 69 | 70 | // Turn a struct field name into a flag name. In particular this lower cases 71 | // leading acronyms, and the first capital letter. 72 | func fieldFlagName(fieldName string) flagNameComponent { 73 | return flagNameComponent(func() (ret []rune) { 74 | fieldNameRunes := []rune(fieldName) 75 | for i, r := range fieldNameRunes { 76 | prevUpper := func() bool { return unicode.IsUpper(fieldNameRunes[i-1]) } 77 | nextUpper := func() bool { return unicode.IsUpper(fieldNameRunes[i+1]) } 78 | if i == 0 || (prevUpper() && (i == len(fieldNameRunes)-1 || nextUpper())) { 79 | r = unicode.ToLower(r) 80 | } 81 | ret = append(ret, r) 82 | } 83 | return 84 | }()) 85 | } 86 | -------------------------------------------------------------------------------- /parseopt.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | type parseOpt func(p *Parser) 4 | 5 | // Don't perform default behaviour if -h or -help are passed. 6 | func NoDefaultHelp() parseOpt { 7 | return func(p *Parser) { 8 | p.noDefaultHelp = true 9 | } 10 | } 11 | 12 | // Provides a description for the program to be shown in the usage message. 13 | func Description(desc string) parseOpt { 14 | return func(p *Parser) { 15 | p.description = desc 16 | } 17 | } 18 | 19 | func Program(name string) parseOpt { 20 | return func(p *Parser) { 21 | p.program = name 22 | } 23 | } 24 | 25 | func ParseIntermixed(enabled bool) parseOpt { 26 | return func(p *Parser) { 27 | p.parseIntermixed = enabled 28 | } 29 | } 30 | 31 | func Parent(parent *Parser) parseOpt { 32 | return func(p *Parser) { 33 | p.parent = parent 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /parser.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | 8 | "github.com/anacrolix/missinggo/v2/slices" 9 | "github.com/huandu/xstrings" 10 | "golang.org/x/xerrors" 11 | ) 12 | 13 | type Parser struct { 14 | // The value from which the Parser is built, and values are assigned. 15 | cmd interface{} 16 | // Disables the default handling of -h and -help. 17 | noDefaultHelp bool 18 | program string 19 | description string 20 | // Whether the first non-option argument requires that all further arguments are to be treated 21 | // as positional. 22 | parseIntermixed bool 23 | // The Parser that preceded this one, such as in sub-command relationship. 24 | parent *Parser 25 | 26 | posArgs []arg 27 | // Maps -K=V to map[K]arg(V) 28 | flags map[string]arg 29 | excess *ExcessArgs 30 | 31 | // Count of positional arguments parsed so far. Used to locate the next 32 | // positional argument where it's non-trivial (non-unity arity). 33 | numPos int 34 | } 35 | 36 | func (p *Parser) hasOptions() bool { 37 | return len(p.flags) != 0 38 | } 39 | 40 | func (p *Parser) parse(args []string) (err error) { 41 | posOnly := false 42 | for len(args) != 0 { 43 | if p.excess != nil && p.nextPosArg() == nil { 44 | *p.excess = args 45 | return 46 | } 47 | a := args[0] 48 | args = args[1:] 49 | if !posOnly && a == "--" { 50 | posOnly = true 51 | continue 52 | } 53 | if !posOnly && isFlag(a) { 54 | err = p.parseFlag(a[1:]) 55 | if err != nil { 56 | err = xerrors.Errorf("parsing flag %q: %w", a[1:], err) 57 | } 58 | } else { 59 | err = p.parsePos(a) 60 | if !p.parseIntermixed { 61 | posOnly = true 62 | } 63 | } 64 | if err != nil { 65 | return 66 | } 67 | } 68 | if p.numPos < p.minPos() { 69 | return userError{fmt.Sprintf("missing argument: %q", p.indexPosArg(p.numPos).name)} 70 | } 71 | return 72 | } 73 | 74 | func (p *Parser) minPos() (min int) { 75 | for _, arg := range p.posArgs { 76 | min += arg.arity.min 77 | } 78 | return 79 | } 80 | 81 | func newParser(cmd interface{}, opts ...parseOpt) (p *Parser, err error) { 82 | p = &Parser{ 83 | cmd: cmd, 84 | parseIntermixed: true, 85 | } 86 | for _, opt := range opts { 87 | opt(p) 88 | } 89 | err = p.parseCmd() 90 | return 91 | } 92 | 93 | func (p *Parser) parseCmd() error { 94 | if p.cmd == nil { 95 | return nil 96 | } 97 | s := reflect.ValueOf(p.cmd).Elem() 98 | for s.Kind() == reflect.Interface { 99 | s = s.Elem() 100 | } 101 | if s.Kind() != reflect.Struct { 102 | return fmt.Errorf("expected struct got %s", s.Type()) 103 | } 104 | return p.parseStruct(s, nil) 105 | } 106 | 107 | // Positional arguments are marked per struct. 108 | func (p *Parser) parseStruct(st reflect.Value, path []flagNameComponent) (err error) { 109 | posStarted := false 110 | foreachStructField(st, func(f reflect.Value, sf reflect.StructField) (stop bool) { 111 | if !posStarted && f.Type() == reflect.TypeOf(StartPos{}) { 112 | posStarted = true 113 | return false 114 | } 115 | if f.Type() == reflect.TypeOf(ExcessArgs{}) { 116 | p.excess = f.Addr().Interface().(*ExcessArgs) 117 | return false 118 | } 119 | if sf.PkgPath != "" { 120 | return false 121 | } 122 | if p.excess != nil { 123 | err = ErrFieldsAfterExcessArgs 124 | return true 125 | } 126 | if canMarshal(f) { 127 | if posStarted { 128 | err = p.addPos(f, sf, path) 129 | } else { 130 | err = p.addFlag(f, sf, path) 131 | if err != nil { 132 | err = fmt.Errorf("error adding flag in %s: %s", st.Type(), err) 133 | } 134 | } 135 | return err != nil 136 | } 137 | var parsed bool 138 | parsed, err = p.parseEmbeddedStruct(f, sf, path) 139 | if err != nil { 140 | err = fmt.Errorf("parsing embedded struct: %w", err) 141 | stop = true 142 | return 143 | } 144 | if parsed { 145 | return false 146 | } 147 | err = fmt.Errorf("field has bad type: %v", f.Type()) 148 | return true 149 | }) 150 | return 151 | } 152 | 153 | func (p *Parser) parseEmbeddedStruct(f reflect.Value, sf reflect.StructField, path []flagNameComponent) (parsed bool, err error) { 154 | if f.Kind() == reflect.Ptr { 155 | f = f.Elem() 156 | } 157 | if f.Kind() != reflect.Struct { 158 | return 159 | } 160 | if canMarshal(f.Addr()) { 161 | err = fmt.Errorf("field %q has type %s, but %s is marshalable", sf.Name, f.Type(), f.Addr().Type()) 162 | return 163 | } 164 | parsed = true 165 | if !sf.Anonymous { 166 | path = append(path, structFieldFlagNameComponent(sf)) 167 | } 168 | err = p.parseStruct(f, path) 169 | return 170 | } 171 | 172 | func newArg(v reflect.Value, sf reflect.StructField, name string) arg { 173 | return arg{ 174 | arity: fieldArity(v, sf), 175 | value: v, 176 | name: name, 177 | help: sf.Tag.Get("help"), 178 | } 179 | } 180 | 181 | func (p *Parser) addPos(f reflect.Value, sf reflect.StructField, path []flagNameComponent) error { 182 | p.posArgs = append(p.posArgs, newArg(f, sf, strings.ToUpper(xstrings.ToSnakeCase(sf.Name)))) 183 | return nil 184 | } 185 | 186 | func flagName(comps []flagNameComponent) string { 187 | var ss []string 188 | slices.MakeInto(&ss, comps) 189 | return strings.Join(ss, ".") 190 | } 191 | 192 | func (p *Parser) addFlag(f reflect.Value, sf reflect.StructField, path []flagNameComponent) error { 193 | name := flagName(append(path, structFieldFlagNameComponent(sf))) 194 | if _, ok := p.flags[name]; ok { 195 | return fmt.Errorf("flag %q defined more than once", name) 196 | } 197 | if p.flags == nil { 198 | p.flags = make(map[string]arg) 199 | } 200 | p.flags[name] = newArg(f, sf, name) 201 | return nil 202 | } 203 | 204 | func isFlag(arg string) bool { 205 | return len(arg) > 1 && arg[0] == '-' 206 | } 207 | 208 | func (p *Parser) parseFlag(s string) error { 209 | i := strings.IndexByte(s, '=') 210 | k := s 211 | v := "" 212 | if i != -1 { 213 | k = s[:i] 214 | v = s[i+1:] 215 | } 216 | flag, ok := p.flags[k] 217 | if !ok { 218 | if (k == "help" || k == "h") && !p.noDefaultHelp { 219 | return ErrDefaultHelp 220 | } 221 | return userError{fmt.Sprintf("unknown flag: %q", k)} 222 | } 223 | err := flag.marshal(v, i != -1) 224 | if err != nil { 225 | return xerrors.Errorf("parsing value %q for flag %q: %w", v, k, err) 226 | } 227 | return nil 228 | } 229 | 230 | func (p *Parser) indexPosArg(i int) *arg { 231 | for _, arg := range p.posArgs { 232 | if i < arg.arity.max { 233 | return &arg 234 | } 235 | i -= arg.arity.max 236 | } 237 | return nil 238 | } 239 | 240 | func (p *Parser) nextPosArg() *arg { 241 | return p.indexPosArg(p.numPos) 242 | } 243 | 244 | func (p *Parser) parsePos(s string) (err error) { 245 | arg := p.nextPosArg() 246 | if arg == nil { 247 | return userError{fmt.Sprintf("excess argument: %q", s)} 248 | } 249 | err = arg.marshal(s, true) 250 | if err != nil { 251 | return 252 | } 253 | p.numPos++ 254 | return 255 | } 256 | 257 | type flagNameComponent string 258 | 259 | func structFieldFlagNameComponent(sf reflect.StructField) flagNameComponent { 260 | name := sf.Tag.Get("name") 261 | if name != "" { 262 | return flagNameComponent(name) 263 | } 264 | return fieldFlagName(sf.Name) 265 | } 266 | 267 | func (p *Parser) posWithHelp() (ret []arg) { 268 | for _, a := range p.posArgs { 269 | if a.help != "" { 270 | ret = append(ret, a) 271 | } 272 | } 273 | return 274 | } 275 | -------------------------------------------------------------------------------- /tagflag.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "reflect" 9 | 10 | "golang.org/x/xerrors" 11 | ) 12 | 13 | // Struct fields after this one are considered positional arguments. 14 | type StartPos struct{} 15 | 16 | // Default help flag was provided, and should be handled. 17 | var ErrDefaultHelp = errors.New("help flag") 18 | 19 | // Parses given arguments, returning any error. 20 | func ParseErr(cmd interface{}, args []string, opts ...parseOpt) (err error) { 21 | p, err := newParser(cmd, opts...) 22 | if err != nil { 23 | return 24 | } 25 | return p.parse(args) 26 | } 27 | 28 | // Parses the command-line arguments, exiting the process appropriately on 29 | // errors or if usage is printed. 30 | func Parse(cmd interface{}, opts ...parseOpt) *Parser { 31 | opts = append([]parseOpt{Program(filepath.Base(os.Args[0]))}, opts...) 32 | return ParseArgs(cmd, os.Args[1:], opts...) 33 | } 34 | 35 | // Like Parse, but operates on the given args instead. 36 | func ParseArgs(cmd interface{}, args []string, opts ...parseOpt) *Parser { 37 | p, err := newParser(cmd, opts...) 38 | if err == nil { 39 | err = p.parse(args) 40 | } 41 | if xerrors.Is(err, ErrDefaultHelp) { 42 | p.printUsage(os.Stdout) 43 | os.Exit(0) 44 | } 45 | if err != nil { 46 | fmt.Fprintf(os.Stderr, "tagflag: error parsing args: %v\n", err) 47 | if _, ok := err.(userError); ok { 48 | os.Exit(2) 49 | } 50 | os.Exit(1) 51 | } 52 | return p 53 | } 54 | 55 | func Unmarshal(arg string, v interface{}) error { 56 | _v := reflect.ValueOf(v).Elem() 57 | m := valueMarshaler(_v.Type()) 58 | if m == nil { 59 | return fmt.Errorf("can't unmarshal to type %s", _v.Type()) 60 | } 61 | return m.Marshal(_v, arg) 62 | } 63 | -------------------------------------------------------------------------------- /tagflag_test.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "os" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | "golang.org/x/xerrors" 13 | ) 14 | 15 | func TestBasic(t *testing.T) { 16 | type simpleCmd struct { 17 | Verbose bool `name:"v"` 18 | StartPos 19 | Arg string 20 | } 21 | for _, _case := range []struct { 22 | expected simpleCmd 23 | err error 24 | args []string 25 | }{ 26 | { 27 | simpleCmd{Verbose: true, Arg: "test"}, 28 | nil, 29 | []string{"-v", "test"}, 30 | }, 31 | { 32 | simpleCmd{Verbose: false, Arg: "hello"}, 33 | nil, 34 | []string{"hello"}, 35 | }, 36 | { 37 | simpleCmd{}, 38 | userError{`excess argument: "world"`}, 39 | []string{"hello", "world"}, 40 | }, 41 | { 42 | simpleCmd{Arg: "hello, world"}, 43 | nil, 44 | []string{"hello, world"}, 45 | }, 46 | { 47 | simpleCmd{}, 48 | userError{`excess argument: "answer = 42"`}, 49 | []string{"hello, world", "answer = 42"}, 50 | }, 51 | { 52 | simpleCmd{}, 53 | userError{`missing argument: "ARG"`}, 54 | []string{"-v"}, 55 | }, 56 | { 57 | simpleCmd{}, 58 | userError{`unknown flag: "no"`}, 59 | []string{"-no"}, 60 | }, 61 | } { 62 | var actual simpleCmd 63 | err := ParseErr(&actual, _case.args) 64 | assert.True(t, xerrors.Is(err, _case.err)) 65 | if _case.err != nil || _case.err != err { 66 | // The value we got doesn't matter. 67 | continue 68 | } 69 | assert.EqualValues(t, _case.expected, actual) 70 | } 71 | } 72 | 73 | func TestNotBasic(t *testing.T) { 74 | t.Skip("outdated use of parseCase") 75 | type cmd struct { 76 | Seed bool 77 | NoUpload bool 78 | ListenAddr string 79 | DataDir string `name:"d"` 80 | StartPos 81 | Torrent []string `arity:"+"` 82 | } 83 | for _, _case := range []parseCase{ 84 | errorCase(userError{`missing argument: "TORRENT"`}, "-seed"), 85 | { 86 | []string{"-seed", "a.torrent", "b.torrent"}, 87 | nil, 88 | cmd{ 89 | Torrent: []string{"a.torrent", "b.torrent"}, 90 | Seed: true, 91 | }, 92 | }, 93 | { 94 | []string{"-listenAddr=1.2.3.4:80", "a.torrent", "b.torrent"}, 95 | nil, 96 | cmd{ 97 | ListenAddr: "1.2.3.4:80", 98 | Torrent: []string{"a.torrent", "b.torrent"}, 99 | }, 100 | }, 101 | { 102 | []string{"-d=/tmp", "a.torrent", "b.torrent", "-listenAddr=1.2.3.4:80"}, 103 | nil, 104 | cmd{ 105 | DataDir: "/tmp", 106 | ListenAddr: "1.2.3.4:80", 107 | Torrent: []string{"a.torrent", "b.torrent"}, 108 | }, 109 | }, 110 | { 111 | []string{"-noUpload=true", "-noUpload=false", "a.torrent"}, 112 | nil, 113 | cmd{ 114 | NoUpload: false, 115 | Torrent: []string{"a.torrent"}, 116 | }, 117 | }, 118 | } { 119 | var actual cmd 120 | err := ParseErr(&actual, _case.args) 121 | assert.EqualValues(t, _case.err, err) 122 | if _case.err != nil { 123 | continue 124 | } 125 | assert.EqualValues(t, _case.expected, actual) 126 | } 127 | } 128 | 129 | func TestBadCommand(t *testing.T) { 130 | // assert.Error(t, ParseErr(struct{}{}, nil)) 131 | assert.NoError(t, ParseErr(new(struct{}), nil)) 132 | assert.NoError(t, ParseErr(nil, nil)) 133 | } 134 | 135 | func TestVarious(t *testing.T) { 136 | a := &struct { 137 | StartPos 138 | A string `arity:"?"` 139 | }{} 140 | assert.NoError(t, ParseErr(a, nil)) 141 | assert.NoError(t, ParseErr(a, []string{"a"})) 142 | assert.EqualValues(t, "a", a.A) 143 | assert.EqualError(t, ParseErr(a, []string{"a", "b"}), `excess argument: "b"`) 144 | } 145 | 146 | func TestUint(t *testing.T) { 147 | var a struct { 148 | A uint 149 | } 150 | assert.Error(t, ParseErr(&a, []string{"-a"})) 151 | assert.Error(t, ParseErr(&a, []string{"-a", "-1"})) 152 | assert.NoError(t, ParseErr(&a, []string{"-a=42"})) 153 | } 154 | 155 | func TestBasicPositionalArities(t *testing.T) { 156 | t.Skip("outdated use of parseCase") 157 | type cmd struct { 158 | C bool 159 | StartPos 160 | A string 161 | B int64 `arity:"?"` 162 | D []string `arity:"*"` 163 | } 164 | for _, _case := range []parseCase{ 165 | // {nil, userError{`missing argument: "A"`}, cmd{}}, 166 | {[]string{"abc"}, nil, cmd{A: "abc"}}, 167 | {[]string{"abc", "123"}, nil, cmd{A: "abc", B: 123}}, 168 | {[]string{"abc", "123", "first"}, nil, cmd{A: "abc", B: 123, D: []string{"first"}}}, 169 | {[]string{"abc", "123", "first", "second"}, nil, cmd{A: "abc", B: 123, D: []string{"first", "second"}}}, 170 | {[]string{"abc", "123", "-c", "first", "second"}, nil, cmd{A: "abc", B: 123, C: true, D: []string{"first", "second"}}}, 171 | } { 172 | var actual cmd 173 | err := ParseErr(&actual, _case.args) 174 | assert.EqualValues(t, _case.err, err) 175 | if _case.err != nil { 176 | continue 177 | } 178 | assert.EqualValues(t, _case.expected, actual) 179 | } 180 | } 181 | 182 | func TestBytes(t *testing.T) { 183 | var cmd struct { 184 | B Bytes 185 | } 186 | err := ParseErr(&cmd, []string{"-b=100g"}) 187 | assert.NoError(t, err) 188 | assert.EqualValues(t, 100e9, cmd.B) 189 | } 190 | 191 | func TestPtrToCustom(t *testing.T) { 192 | var cmd struct { 193 | Addr *net.TCPAddr 194 | } 195 | err := ParseErr(&cmd, []string{"-addr=:443"}) 196 | assert.NoError(t, err) 197 | assert.EqualValues(t, ":443", cmd.Addr.String()) 198 | err = ParseErr(&cmd, []string{"-addr="}) 199 | assert.NoError(t, err) 200 | assert.Nil(t, cmd.Addr) 201 | } 202 | 203 | func TestResolveTCPAddr(t *testing.T) { 204 | addr, err := net.ResolveTCPAddr("tcp", "") 205 | t.Log(addr, err) 206 | } 207 | 208 | func TestMain(m *testing.M) { 209 | log.SetFlags(log.Lshortfile) 210 | os.Exit(m.Run()) 211 | } 212 | 213 | func TestDefaultLongFlagName(t *testing.T) { 214 | f := func(expected, start string) { 215 | assert.EqualValues(t, expected, fieldFlagName(start), start) 216 | } 217 | f("noUpload", "NoUpload") 218 | f("dht", "DHT") 219 | f("noIPv6", "NoIPv6") 220 | f("noIpv6", "NoIpv6") 221 | f("tcpAddr", "TCPAddr") 222 | f("addr", "Addr") 223 | f("v", "V") 224 | f("a", "A") 225 | f("redisUrl", "RedisURL") 226 | f("redisUrl", "redisURL") 227 | } 228 | 229 | func TestPrintUsage(t *testing.T) { 230 | err := ParseErr(nil, []string{"-h"}) 231 | assert.True(t, xerrors.Is(err, ErrDefaultHelp), "%#v", err) 232 | err = ParseErr(nil, []string{"-help"}) 233 | assert.True(t, xerrors.Is(err, ErrDefaultHelp)) 234 | } 235 | 236 | func TestParseUnnamedTypes(t *testing.T) { 237 | var cmd1 struct { 238 | A []byte 239 | B bool 240 | } 241 | assert.NoError(t, ParseErr(&cmd1, nil)) 242 | type B []byte 243 | var cmd2 struct { 244 | A B 245 | } 246 | ParseErr(&cmd2, nil) 247 | type C bool 248 | var cmd3 struct { 249 | A C 250 | } 251 | ParseErr(&cmd3, nil) 252 | } 253 | 254 | func TestPosArgSlice(t *testing.T) { 255 | var cmd1 struct { 256 | StartPos 257 | Args []string 258 | } 259 | require.NoError(t, ParseErr(&cmd1, []string{"a", "b", "c"})) 260 | assert.EqualValues(t, []string{"a", "b", "c"}, cmd1.Args) 261 | } 262 | 263 | func TestTCPAddrNoExplicitValue(t *testing.T) { 264 | var cmd struct { 265 | Addr *net.TCPAddr 266 | } 267 | assert.Error(t, ParseErr(&cmd, []string{"-addr"})) 268 | assert.NoError(t, ParseErr(&cmd, []string{"-addr="})) 269 | } 270 | 271 | func TestUnexportedStructField(t *testing.T) { 272 | var cmd struct { 273 | badField bool 274 | } 275 | assert.NoError(t, ParseErr(&cmd, nil)) 276 | var ue userError 277 | require.True(t, xerrors.As(ParseErr(&cmd, []string{"-badField"}), &ue)) 278 | assert.EqualValues(t, userError{`unknown flag: "badField"`}, ue) 279 | } 280 | 281 | func TestExcessArgsEmpty(t *testing.T) { 282 | var cmd struct { 283 | ExcessArgs 284 | } 285 | require.NoError(t, ParseErr(&cmd, nil)) 286 | assert.Len(t, cmd.ExcessArgs, 0) 287 | } 288 | 289 | func TestExcessArgs(t *testing.T) { 290 | var cmd struct { 291 | ExcessArgs 292 | } 293 | excess := []string{"yo", "-addr=hi"} 294 | require.NoError(t, ParseErr(&cmd, excess)) 295 | assert.EqualValues(t, excess, cmd.ExcessArgs) 296 | } 297 | 298 | func TestExcessArgsComplex(t *testing.T) { 299 | var cmd struct { 300 | Verbose bool `name:"v"` 301 | StartPos 302 | Command string 303 | ExcessArgs 304 | } 305 | excess := []string{"-addr=hi"} 306 | require.NoError(t, ParseErr(&cmd, append([]string{"-v", "serve"}, excess...))) 307 | assert.EqualValues(t, excess, cmd.ExcessArgs) 308 | } 309 | 310 | func TestFieldAfterExcessArgs(t *testing.T) { 311 | var cmd struct { 312 | ExcessArgs 313 | Badness string 314 | } 315 | require.EqualValues(t, ErrFieldsAfterExcessArgs, ParseErr(&cmd, nil)) 316 | } 317 | 318 | func TestSliceOfUnmarshallableStruct(t *testing.T) { 319 | var cmd struct { 320 | StartPos 321 | Complex []struct{} 322 | } 323 | require.EqualError(t, ParseErr(&cmd, []string{"herp"}), "can't marshal type struct {}") 324 | } 325 | 326 | func newStruct(ref interface{}) func() interface{} { 327 | return func() interface{} { 328 | return reflect.New(reflect.TypeOf(ref)).Interface() 329 | } 330 | } 331 | 332 | func TestBasicPointer(t *testing.T) { 333 | type cmd struct { 334 | Maybe *bool 335 | } 336 | _true := true 337 | RunCases(t, []parseCase{ 338 | noErrorCase(cmd{}), 339 | errorCase(userError{`excess argument: "nope"`}, "nope"), 340 | noErrorCase(cmd{Maybe: &_true}, "-maybe=true"), 341 | }, newStruct(cmd{})) 342 | } 343 | 344 | func TestMarshalByteArray(t *testing.T) { 345 | type cmd struct { 346 | StartPos 347 | Bs [2]byte 348 | } 349 | RunCases(t, []parseCase{ 350 | noErrorCase(cmd{Bs: func() (ret [2]byte) { copy(ret[:], "AB"); return }()}, "4142"), 351 | anyErrorCase("41424"), 352 | // anyErrorCase("4142"), 353 | }, newStruct(cmd{})) 354 | } 355 | 356 | func TestMarshalStruct(t *testing.T) { 357 | type InternalStruct struct { 358 | A bool 359 | } 360 | var cmd struct { 361 | Struct InternalStruct 362 | StartPos 363 | StructPos InternalStruct 364 | } 365 | ParseErr(&cmd, []string{"-struct", "structpos"}) 366 | } 367 | -------------------------------------------------------------------------------- /usage.go: -------------------------------------------------------------------------------- 1 | package tagflag 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "text/tabwriter" 7 | 8 | "github.com/anacrolix/missinggo/v2" 9 | "github.com/anacrolix/missinggo/v2/slices" 10 | ) 11 | 12 | func (p *Parser) printPosArgUsage(w io.Writer) { 13 | if p.parent != nil { 14 | p.parent.printPosArgUsage(w) 15 | } 16 | for _, arg := range p.posArgs { 17 | fs := func() string { 18 | switch arg.arity { 19 | case arity{0, 1}: 20 | return "[%s]" 21 | case arity{1, infArity}: 22 | return "%s..." 23 | case arity{0, infArity}: 24 | return "[%s...]" 25 | default: 26 | return "<%s>" 27 | } 28 | }() 29 | // if arg.arity != arity{1,1} { 30 | fmt.Fprintf(w, " "+fs, arg.name) 31 | // } 32 | // if arg.arity > 1 { 33 | // for range iter.N(int(arg.arity - 1)) { 34 | // fmt.Fprintf(w, " "+fs, arg.name) 35 | // } 36 | // } 37 | } 38 | } 39 | 40 | func (p *Parser) printUsage(w io.Writer) { 41 | fmt.Fprintf(w, "Usage:\n %s", p.program) 42 | if p.hasOptions() { 43 | fmt.Fprintf(w, " [OPTIONS...]") 44 | } 45 | p.printPosArgUsage(w) 46 | fmt.Fprintf(w, "\n") 47 | if p.description != "" { 48 | fmt.Fprintf(w, "\n%s\n", missinggo.Unchomp(p.description)) 49 | } 50 | if awd := p.posWithHelp(); len(awd) != 0 { 51 | fmt.Fprintf(w, "Arguments:\n") 52 | tw := newUsageTabwriter(w) 53 | for _, a := range awd { 54 | fmt.Fprintf(tw, " %s\t(%s)\t%s\n", a.name, a.value.Type(), a.help) 55 | } 56 | tw.Flush() 57 | } 58 | var opts []arg 59 | for _, v := range p.flags { 60 | opts = append(opts, v) 61 | } 62 | slices.Sort(opts, func(left, right arg) bool { 63 | return left.name < right.name 64 | }) 65 | writeOptionUsage(w, opts) 66 | } 67 | 68 | func newUsageTabwriter(w io.Writer) *tabwriter.Writer { 69 | return tabwriter.NewWriter(w, 8, 2, 3, ' ', 0) 70 | } 71 | 72 | func writeOptionUsage(w io.Writer, flags []arg) { 73 | if len(flags) == 0 { 74 | return 75 | } 76 | fmt.Fprintf(w, "Options:\n") 77 | tw := newUsageTabwriter(w) 78 | for _, f := range flags { 79 | fmt.Fprint(tw, " ") 80 | fmt.Fprintf(tw, "%s%s", flagPrefix, f.name) 81 | help := f.help 82 | if !f.hasZeroValue() { 83 | if help != "" { 84 | help += " " 85 | } 86 | help += fmt.Sprintf("(Default: %v)", f.value) 87 | } 88 | fmt.Fprintf(tw, "\t(%s)\t%s\n", f.value.Type(), help) 89 | } 90 | tw.Flush() 91 | } 92 | --------------------------------------------------------------------------------