Skip to content

Commit 9577909

Browse files
hujun-openitzg
andauthored
add support for net.IP, net.IPNet, net.HardwareAddr(MAC) and time.Time (#20)
Co-authored-by: Geoff Bourne <itzgeoff@gmail.com>
1 parent b3fbd38 commit 9577909

File tree

9 files changed

+440
-23
lines changed

9 files changed

+440
-23
lines changed

.github/dependabot.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ updates:
44
directory: "/" # Location of package manifests
55
schedule:
66
interval: "weekly"
7+
- package-ecosystem: "github-actions"
8+
directory: "/"
9+
schedule:
10+
interval: "weekly"

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ jobs:
1414
- uses: actions/checkout@v2
1515

1616
- name: Set up Go
17-
uses: actions/setup-go@v2
17+
uses: actions/setup-go@v4
1818
with:
19-
go-version: 1.17
19+
go-version: '1.20'
2020

2121
- name: Test
2222
run: go test -v ./...

addtional_test.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package flagsfiller_test
2+
3+
import (
4+
"flag"
5+
"net"
6+
"testing"
7+
"time"
8+
9+
"github.com/itzg/go-flagsfiller"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
)
13+
14+
func TestTime(t *testing.T) {
15+
type Config struct {
16+
T time.Time `layout:"2006-Jan-02==15:04:05"`
17+
}
18+
19+
var config Config
20+
21+
filler := flagsfiller.New()
22+
23+
var flagset flag.FlagSet
24+
err := filler.Fill(&flagset, &config)
25+
require.NoError(t, err)
26+
27+
err = flagset.Parse([]string{"-t", "2016-Dec-13==16:03:02"})
28+
require.NoError(t, err)
29+
expeted, _ := time.Parse(time.DateTime, "2016-12-13 16:03:02")
30+
assert.Equal(t, expeted, config.T)
31+
}
32+
33+
func TestNetIP(t *testing.T) {
34+
type Config struct {
35+
Addr net.IP
36+
}
37+
38+
var config Config
39+
40+
filler := flagsfiller.New()
41+
42+
var flagset flag.FlagSet
43+
err := filler.Fill(&flagset, &config)
44+
require.NoError(t, err)
45+
46+
err = flagset.Parse([]string{"-addr", "1.2.3.4"})
47+
require.NoError(t, err)
48+
49+
assert.Equal(t, net.ParseIP("1.2.3.4"), config.Addr)
50+
}
51+
52+
func TestMACAddr(t *testing.T) {
53+
type Config struct {
54+
Addr net.HardwareAddr
55+
}
56+
57+
var config Config
58+
59+
filler := flagsfiller.New()
60+
61+
var flagset flag.FlagSet
62+
err := filler.Fill(&flagset, &config)
63+
require.NoError(t, err)
64+
65+
err = flagset.Parse([]string{"-addr", "1c:2a:11:ce:23:45"})
66+
require.NoError(t, err)
67+
68+
assert.Equal(t, net.HardwareAddr{0x1c, 0x2a, 0x11, 0xce, 0x23, 0x45}, config.Addr)
69+
}
70+
71+
func TestIPNet(t *testing.T) {
72+
type Config struct {
73+
Prefix net.IPNet
74+
}
75+
76+
var config Config
77+
78+
filler := flagsfiller.New()
79+
80+
var flagset flag.FlagSet
81+
err := filler.Fill(&flagset, &config)
82+
require.NoError(t, err)
83+
84+
err = flagset.Parse([]string{"-prefix", "192.168.1.0/24"})
85+
require.NoError(t, err)
86+
_, expected, _ := net.ParseCIDR("192.168.1.0/24")
87+
assert.Equal(t, *expected, config.Prefix)
88+
}

docs.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Package flagsfiller makes Go's flag package pleasant to use by mapping the fields of a given struct
33
into flags in a FlagSet.
44
5-
Quick Start
5+
# Quick Start
66
77
A FlagSetFiller is created with the New constructor, passing it any desired FillerOptions.
88
With that, call Fill, passing it a flag.FlatSet, such as flag.CommandLine, and your struct to
@@ -37,7 +37,7 @@ as the snippet above in one call:
3737
3838
flagsfiller.Parse(&config)
3939
40-
Flag Naming
40+
# Flag Naming
4141
4242
By default, the flags are named by taking the field name and performing a word-wise conversion
4343
to kebab-case. For example the field named "MyMultiWordField" becomes the flag named
@@ -52,7 +52,8 @@ Additional aliases, such as short names, can be declared with the `aliases` tag
5252
Timeout time.Duration `aliases:"t"`
5353
Limit int `aliases:"l,lim"`
5454
}
55-
Nested Structs
55+
56+
# Nested Structs
5657
5758
FlagSetFiller supports nested structs and computes the flag names by prefixing the field
5859
name of the struct to the names of the fields it contains. For example, the following maps to
@@ -68,7 +69,7 @@ the flags named remote-host, remote-auth-username, and remote-auth-password:
6869
}
6970
}
7071
71-
Flag Usage
72+
# Flag Usage
7273
7374
To declare a flag's usage add a `usage:""` tag to the field, such as:
7475
@@ -87,7 +88,7 @@ results in the rendered output:
8788
-some-url URL
8889
a URL to configure
8990
90-
Defaults
91+
# Defaults
9192
9293
To declare the default value of a flag, you can either set a field's value before passing the
9394
struct to process, such as:
@@ -105,7 +106,7 @@ converted into the field's type. For example,
105106
Timeout time.Duration `default:"1m"`
106107
}
107108
108-
String Slices
109+
# String Slices
109110
110111
FlagSetFiller also includes support for []string fields.
111112
Repetition of the argument appends to the slice and/or an argument value can contain a
@@ -121,7 +122,7 @@ The default tag's value is provided as a comma-separated list, such as
121122
122123
MultiValues []string `default:"one,two,three"`
123124
124-
Maps of String to String
125+
# Maps of String to String
125126
126127
FlagSetFiller also includes support for map[string]string fields.
127128
Each argument entry is a key=value and/or repetition of the arguments adds to the map or
@@ -137,7 +138,16 @@ The default tag's value is provided a comma-separate list of key=value entries,
137138
138139
Mappings map[string]string `default:"k1=v1,k2=v2,k3=v3"`
139140
140-
Environment variable mapping
141+
# Other supported types
142+
143+
FlagSetFiller also supports following field types:
144+
145+
- net.IP: format used by net.ParseIP()
146+
- net.IPNet: format used by net.ParseCIDR()
147+
- net.HardwareAddr (MAC addr): format used by net.ParseMAC()
148+
- time.Time: format is the layout string used by time.Parse(), default layout is time.DateTime, could be overriden by field tag "layout"
149+
150+
# Environment variable mapping
141151
142152
To activate the setting of flag values from environment variables, pass the WithEnv option to
143153
flagsfiller.New or flagsfiller.Parse. That option takes a prefix that will be prepended to the
@@ -151,10 +161,10 @@ with the following field declaration
151161
152162
would render the following usage:
153163
154-
-host string
155-
the host to use (env APP_HOST) (default "localhost")
164+
-host string
165+
the host to use (env APP_HOST) (default "localhost")
156166
157-
Per-field overrides
167+
# Per-field overrides
158168
159169
To override the naming of a flag, the field can be declared with the tag `flag:"name"` where
160170
the given name will be used exactly as the flag name. An empty string for the name indicates
@@ -171,6 +181,5 @@ per field with `env:"name"` even when the flagsfiller-wide option was not includ
171181
172182
Host string `env:"SERVER_ADDRESS"`
173183
NotEnvMapped string `env:""`
174-
175184
*/
176185
package flagsfiller

flagset.go

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,41 @@ func (f *FlagSetFiller) Fill(flagSet *flag.FlagSet, from interface{}) error {
5555
}
5656
}
5757

58+
// this is a list of supported struct, like time.Time, that walkFields() won't walk into,
59+
// the key is the is string returned by the getTypeName(<struct_type>),
60+
// each supported struct need to be added in this map in init()
61+
var supportedStructList = make(map[string]struct{})
62+
63+
func isSupportedStruct(name string) bool {
64+
_, ok := supportedStructList[name]
65+
return ok
66+
}
67+
68+
func getTypeName(t reflect.Type) string {
69+
return t.PkgPath() + "." + t.Name()
70+
}
71+
5872
func (f *FlagSetFiller) walkFields(flagSet *flag.FlagSet, prefix string,
5973
structVal reflect.Value, structType reflect.Type) error {
6074

6175
if prefix != "" {
6276
prefix += "-"
6377
}
78+
handleDefault := func(field reflect.StructField, fieldValue reflect.Value) error {
79+
addr := fieldValue.Addr()
80+
// make sure it is exported/public
81+
ftype := field.Type
82+
if field.Type.Kind() == reflect.Ptr {
83+
ftype = field.Type.Elem()
84+
}
85+
if addr.CanInterface() {
86+
err := f.processField(flagSet, addr.Interface(), prefix+field.Name, ftype, field.Tag)
87+
if err != nil {
88+
return fmt.Errorf("failed to process %s of %s: %w", field.Name, structType.String(), err)
89+
}
90+
}
91+
return nil
92+
}
6493
for i := 0; i < structVal.NumField(); i++ {
6594
field := structType.Field(i)
6695
fieldValue := structVal.Field(i)
@@ -73,17 +102,33 @@ func (f *FlagSetFiller) walkFields(flagSet *flag.FlagSet, prefix string,
73102

74103
switch field.Type.Kind() {
75104
case reflect.Struct:
105+
fieldTypeName := getTypeName(field.Type)
106+
if isSupportedStruct(fieldTypeName) {
107+
err := handleDefault(field, fieldValue)
108+
if err != nil {
109+
return err
110+
}
111+
continue
112+
}
76113
err := f.walkFields(flagSet, prefix+field.Name, fieldValue, field.Type)
77114
if err != nil {
78115
return fmt.Errorf("failed to process %s of %s: %w", field.Name, structType.String(), err)
79116
}
80117

81118
case reflect.Ptr:
82119
if fieldValue.CanSet() && field.Type.Elem().Kind() == reflect.Struct {
120+
fieldTypeName := getTypeName(field.Type.Elem())
83121
// fill the pointer with a new struct of their type if it is nil
84122
if fieldValue.IsNil() {
85123
fieldValue.Set(reflect.New(field.Type.Elem()))
86124
}
125+
if isSupportedStruct(fieldTypeName) {
126+
err := handleDefault(field, fieldValue.Elem())
127+
if err != nil {
128+
return err
129+
}
130+
continue
131+
}
87132

88133
err := f.walkFields(flagSet, field.Name, fieldValue.Elem(), field.Type.Elem())
89134
if err != nil {
@@ -92,13 +137,9 @@ func (f *FlagSetFiller) walkFields(flagSet *flag.FlagSet, prefix string,
92137
}
93138

94139
default:
95-
addr := fieldValue.Addr()
96-
// make sure it is exported/public
97-
if addr.CanInterface() {
98-
err := f.processField(flagSet, addr.Interface(), prefix+field.Name, field.Type, field.Tag)
99-
if err != nil {
100-
return fmt.Errorf("failed to process %s of %s: %w", field.Name, structType.String(), err)
101-
}
140+
err := handleDefault(field, fieldValue)
141+
if err != nil {
142+
return err
102143
}
103144
}
104145
}
@@ -139,8 +180,21 @@ func (f *FlagSetFiller) processField(flagSet *flag.FlagSet, fieldRef interface{}
139180
} else {
140181
renamed = f.options.renameLongName(name)
141182
}
142-
183+
typeName := getTypeName(t)
143184
switch {
185+
//check the typeName
186+
case typeName == "net.IP":
187+
f.processIP(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases)
188+
case typeName == "net.IPNet":
189+
f.processIPNet(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases)
190+
case typeName == "net.HardwareAddr":
191+
f.processMAC(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases)
192+
193+
case typeName == "time.Time":
194+
layoutStr, _ := tag.Lookup("layout")
195+
f.processTime(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases, layoutStr)
196+
//end of check typeName
197+
144198
case t.Kind() == reflect.String:
145199
f.processString(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases)
146200

go.mod

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
module github.com/itzg/go-flagsfiller
22

3-
go 1.16
3+
go 1.20
44

55
require (
66
github.com/iancoleman/strcase v0.2.0
77
github.com/stretchr/testify v1.8.2
88
)
9+
10+
require (
11+
github.com/davecgh/go-spew v1.1.1 // indirect
12+
github.com/pmezard/go-difflib v1.0.0 // indirect
13+
gopkg.in/yaml.v3 v3.0.1 // indirect
14+
)

0 commit comments

Comments
 (0)