├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── .gitignore ├── .grenrc.yml ├── LICENSE.txt ├── README.md ├── additional.go ├── addtional_test.go ├── docs.go ├── example_test.go ├── examples └── flagsfiller-example │ └── main.go ├── flagset.go ├── flagset_test.go ├── general.go ├── go.mod ├── go.sum ├── net.go ├── options.go ├── options_test.go ├── simple.go ├── time.go └── txtunmarshaler.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" # Location of package manifests 5 | schedule: 6 | interval: "weekly" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Set up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version-file: go.mod 21 | 22 | - name: Test 23 | run: go test -v ./... 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /*.iml 3 | -------------------------------------------------------------------------------- /.grenrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dataSource: "commits" 3 | prefix: "" 4 | includeMessages: "all" 5 | ignoreCommitsWith: 6 | - "docs:" 7 | - "ci:" 8 | - "misc:" 9 | changelogFilename: "CHANGELOG.md" 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2020 Geoff Bourne 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-flagsfiller 2 | 3 | [![](https://godoc.org/github.com/itzg/go-flagsfiller?status.svg)](https://godoc.org/github.com/itzg/go-flagsfiller) 4 | [![](https://img.shields.io/badge/go.dev-module-007D9C)](https://pkg.go.dev/github.com/itzg/go-flagsfiller) 5 | 6 | Bring your own struct and make Go's flag package pleasant to use. 7 | 8 | ## Install 9 | 10 | ``` 11 | go get github.com/itzg/go-flagsfiller 12 | ``` 13 | 14 | ## Import 15 | 16 | ```go 17 | import "github.com/itzg/go-flagsfiller" 18 | ``` 19 | 20 | ## Features 21 | 22 | - Populates Go's [flag.FlagSet](https://golang.org/pkg/flag/#FlagSet) from a struct of your choosing 23 | - By default, field names are converted to flag names using [kebab-case](https://en.wiktionary.org/wiki/kebab_case), but can be configured. 24 | - Use nested structs where flag name is prefixed by the nesting struct field names 25 | - Allows defaults to be given via struct tag `default` 26 | - Falls back to using instance field values as declared default 27 | - Declare flag usage via struct tag `usage` 28 | - Can be combined with other modules, such as [google/subcommands](https://github.com/google/subcommands) for sub-command processing. Can also be integrated with [spf13/cobra](https://github.com/spf13/cobra) by using pflag's [AddGoFlagSet](https://godoc.org/github.com/spf13/pflag#FlagSet.AddGoFlagSet) 29 | - Beyond the standard types supported by flag.FlagSet also includes support for: 30 | - `[]string` where repetition of the argument appends to the slice and/or an argument value can contain a comma or newline-separated list of values. For example: `--arg one --arg two,three` 31 | - `map[string]string` where each entry is a `key=value` and/or repetition of the arguments adds to the map or multiple entries can be comma or newline-separated in a single argument value. For example: `--arg k1=v1 --arg k2=v2,k3=v3` 32 | - `time.Time` parse via time.Parse(), with tag `layout` specify the layout string, default is "2006-01-02 15:04:05" 33 | - `net.IP` parse via net.ParseIP() 34 | - `net.IPNet` parse via net.ParseCIDR() 35 | - `net.HardwareAddr` parse via net.ParseMAC() 36 | - and all types that implement encoding.TextUnmarshaler interface 37 | - Optionally set flag values from environment variables. Similar to flag names, environment variable names are derived automatically from the field names 38 | - New types could be supported via user code, via `RegisterSimpleType(ConvertFunc)`, check [time.go](time.go) and [net.go](net.go) to see how it works 39 | - note: in case of a registered type also implements encoding.TextUnmarshaler, then registered type's ConvertFunc is preferred 40 | 41 | ## Quick example 42 | 43 | ```go 44 | package main 45 | 46 | import ( 47 | "flag" 48 | "fmt" 49 | "github.com/itzg/go-flagsfiller" 50 | "log" 51 | "time" 52 | ) 53 | 54 | type Config struct { 55 | Host string `default:"localhost" usage:"The remote host"` 56 | DebugEnabled bool `default:"true" usage:"Show debugs"` 57 | MaxTimeout time.Duration `default:"5s" usage:"How long to wait"` 58 | Feature struct { 59 | Faster bool `usage:"Go faster"` 60 | LudicrousSpeed bool `usage:"Go even faster"` 61 | } 62 | } 63 | 64 | func main() { 65 | var config Config 66 | 67 | // create a FlagSetFiller 68 | filler := flagsfiller.New() 69 | // fill and map struct fields to flags 70 | err := filler.Fill(flag.CommandLine, &config) 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | 75 | // parse command-line like usual 76 | flag.Parse() 77 | 78 | fmt.Printf("Loaded: %+v\n", config) 79 | } 80 | ``` 81 | 82 | The following shows an example of the usage provided when passing `--help`: 83 | ``` 84 | -debug-enabled 85 | Show debugs (default true) 86 | -feature-faster 87 | Go faster 88 | -feature-ludicrous-speed 89 | Go even faster 90 | -host string 91 | The remote host (default "localhost") 92 | -max-timeout duration 93 | How long to wait (default 5s) 94 | ``` 95 | 96 | ## Real world example 97 | 98 | [saml-auth-proxy](https://github.com/itzg/saml-auth-proxy) shows an end-to-end usage of flagsfiller where the main function fills the flags, maps those to environment variables with [envy](https://github.com/jamiealquiza/envy), and parses the command line: 99 | 100 | ```go 101 | func main() { 102 | var serverConfig server.Config 103 | 104 | filler := flagsfiller.New() 105 | err := filler.Fill(flag.CommandLine, &serverConfig) 106 | if err != nil { 107 | log.Fatal(err) 108 | } 109 | 110 | envy.Parse("SAML_PROXY") 111 | flag.Parse() 112 | ``` 113 | 114 | where `server.Config` is declared as 115 | 116 | ```go 117 | type Config struct { 118 | Version bool `usage:"show version and exit"` 119 | Bind string `default:":8080" usage:"host:port to bind for serving HTTP"` 120 | BaseUrl string `usage:"External URL of this proxy"` 121 | BackendUrl string `usage:"URL of the backend being proxied"` 122 | IdpMetadataUrl string `usage:"URL of the IdP's metadata XML"` 123 | IdpCaPath string `usage:"Optional path to a CA certificate PEM file for the IdP"` 124 | // ...see https://github.com/itzg/saml-auth-proxy/blob/master/server/server.go for full set 125 | } 126 | ``` 127 | 128 | ## Using with google/subcommands 129 | 130 | Flagsfiller can be used in combination with [google/subcommands](https://github.com/google/subcommands) to fill both global command-line flags and subcommand flags. 131 | 132 | For the global flags, it is best to declare a struct type, such as 133 | 134 | ```go 135 | type GlobalConfig struct { 136 | Debug bool `usage:"enable debug logging"` 137 | } 138 | ``` 139 | 140 | Prior to calling `Execute` on the subcommands' `Commander`, fill and parse the global flags like normal: 141 | 142 | ```go 143 | func main() { 144 | //... register subcommands here 145 | 146 | var globalConfig GlobalConfig 147 | 148 | err := flagsfiller.Parse(&globalConfig) 149 | if err != nil { 150 | log.Fatal(err) 151 | } 152 | 153 | //... execute subcommands but pass global config 154 | os.Exit(int(subcommands.Execute(context.Background(), &globalConfig))) 155 | } 156 | ``` 157 | 158 | Each of your subcommand struct types should contain the flag fields to fill and parse, such as: 159 | 160 | ```go 161 | type connectCmd struct { 162 | Host string `usage:"the hostname of the server" env:"GITHUB_TOKEN"` 163 | Port int `usage:"the port of the server" default:"8080"` 164 | } 165 | ``` 166 | 167 | Your implementation of `SetFlags` will use flagsfiller to fill the definition of the subcommand's flagset, such as: 168 | 169 | ```go 170 | func (c *connectCmd) SetFlags(f *flag.FlagSet) { 171 | filler := flagsfiller.New() 172 | err := filler.Fill(f, c) 173 | if err != nil { 174 | log.Fatal(err) 175 | } 176 | } 177 | ``` 178 | 179 | Finally, your subcommand's `Execute` function can accept the global config passed from the main `Execute` call and access its own fields populated from the subcommand flags: 180 | 181 | ```go 182 | func (c *loadFromGitCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus { 183 | globalConfig := args[0].(*GlobalConfig) 184 | if globalConfig.Debug { 185 | //... enable debug logs 186 | } 187 | 188 | // ...operate on subcommand flags, such as 189 | conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.Host, c.Port)) 190 | } 191 | ``` 192 | ## More information 193 | 194 | [Refer to the GoDocs](https://godoc.org/github.com/itzg/go-flagsfiller) for more information about this module. 195 | -------------------------------------------------------------------------------- /additional.go: -------------------------------------------------------------------------------- 1 | package flagsfiller 2 | 3 | import ( 4 | "log/slog" 5 | "reflect" 6 | ) 7 | 8 | func init() { 9 | RegisterSimpleType(slogLevelConverter) 10 | } 11 | 12 | func slogLevelConverter(s string, _ reflect.StructTag) (slog.Level, error) { 13 | var level slog.Level 14 | err := level.UnmarshalText([]byte(s)) 15 | if err != nil { 16 | return slog.LevelInfo, err 17 | } 18 | return level, nil 19 | } 20 | -------------------------------------------------------------------------------- /addtional_test.go: -------------------------------------------------------------------------------- 1 | package flagsfiller_test 2 | 3 | import ( 4 | "flag" 5 | "log/slog" 6 | "net" 7 | "net/netip" 8 | "testing" 9 | "time" 10 | 11 | "github.com/itzg/go-flagsfiller" 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestTime(t *testing.T) { 17 | type Config struct { 18 | T time.Time `default:"2010-Oct-01==10:02:03" layout:"2006-Jan-02==15:04:05"` 19 | } 20 | 21 | var config Config 22 | 23 | filler := flagsfiller.New() 24 | 25 | var flagset flag.FlagSet 26 | err := filler.Fill(&flagset, &config) 27 | require.NoError(t, err) 28 | 29 | //test default tag 30 | err = flagset.Parse([]string{}) 31 | require.NoError(t, err) 32 | expeted, _ := time.Parse("2006-Jan-02==15:04:05", "2010-Oct-01==10:02:03") 33 | assert.Equal(t, expeted, config.T) 34 | 35 | err = flagset.Parse([]string{"-t", "2016-Dec-13==16:03:02"}) 36 | require.NoError(t, err) 37 | expeted, _ = time.Parse("2006-01-02 15:04:05", "2016-12-13 16:03:02") 38 | assert.Equal(t, expeted, config.T) 39 | } 40 | 41 | func TestNetIP(t *testing.T) { 42 | type Config struct { 43 | Addr net.IP 44 | } 45 | 46 | var config Config 47 | 48 | filler := flagsfiller.New() 49 | 50 | var flagset flag.FlagSet 51 | err := filler.Fill(&flagset, &config) 52 | require.NoError(t, err) 53 | 54 | err = flagset.Parse([]string{"-addr", "1.2.3.4"}) 55 | require.NoError(t, err) 56 | 57 | assert.Equal(t, net.ParseIP("1.2.3.4"), config.Addr) 58 | } 59 | 60 | func TestMACAddr(t *testing.T) { 61 | type Config struct { 62 | Addr net.HardwareAddr 63 | } 64 | 65 | var config Config 66 | 67 | filler := flagsfiller.New() 68 | 69 | var flagset flag.FlagSet 70 | err := filler.Fill(&flagset, &config) 71 | require.NoError(t, err) 72 | 73 | err = flagset.Parse([]string{"-addr", "1c:2a:11:ce:23:45"}) 74 | require.NoError(t, err) 75 | 76 | assert.Equal(t, net.HardwareAddr{0x1c, 0x2a, 0x11, 0xce, 0x23, 0x45}, config.Addr) 77 | } 78 | 79 | func TestIPNet(t *testing.T) { 80 | type Config struct { 81 | Prefix net.IPNet 82 | } 83 | 84 | var config Config 85 | 86 | filler := flagsfiller.New() 87 | 88 | var flagset flag.FlagSet 89 | err := filler.Fill(&flagset, &config) 90 | require.NoError(t, err) 91 | 92 | err = flagset.Parse([]string{"-prefix", "192.168.1.0/24"}) 93 | require.NoError(t, err) 94 | _, expected, _ := net.ParseCIDR("192.168.1.0/24") 95 | assert.Equal(t, *expected, config.Prefix) 96 | } 97 | 98 | func TestTextUnmarshalerType(t *testing.T) { 99 | type Config struct { 100 | Addr netip.Addr `default:"9.9.9.9"` 101 | } 102 | 103 | var config Config 104 | 105 | filler := flagsfiller.New() 106 | 107 | var flagset flag.FlagSet 108 | err := filler.Fill(&flagset, &config) 109 | require.NoError(t, err) 110 | 111 | //test default tag 112 | err = flagset.Parse([]string{}) 113 | require.NoError(t, err) 114 | assert.Equal(t, netip.AddrFrom4([4]byte{9, 9, 9, 9}), config.Addr) 115 | 116 | err = flagset.Parse([]string{"-addr", "1.2.3.4"}) 117 | require.NoError(t, err) 118 | 119 | assert.Equal(t, netip.AddrFrom4([4]byte{1, 2, 3, 4}), config.Addr) 120 | } 121 | 122 | func TestSlogLevels(t *testing.T) { 123 | tests := []struct { 124 | name string 125 | value string 126 | expected slog.Level 127 | }{ 128 | { 129 | name: "info", 130 | value: "info", 131 | expected: slog.LevelInfo, 132 | }, 133 | { 134 | name: "error", 135 | value: "error", 136 | expected: slog.LevelError, 137 | }, 138 | { 139 | name: "numeric offset", 140 | // Borrowed from https://pkg.go.dev/log/slog#Level.UnmarshalText 141 | value: "Error-8", 142 | expected: slog.LevelInfo, 143 | }, 144 | } 145 | for _, test := range tests { 146 | t.Run(test.name, func(t *testing.T) { 147 | var args struct { 148 | Level slog.Level 149 | } 150 | 151 | var flagset flag.FlagSet 152 | err := flagsfiller.New().Fill(&flagset, &args) 153 | require.NoError(t, err) 154 | 155 | err = flagset.Parse([]string{"--level", test.value}) 156 | require.NoError(t, err) 157 | 158 | assert.Equal(t, test.expected, args.Level) 159 | }) 160 | } 161 | } 162 | 163 | func TestSlogLevelWithDefault(t *testing.T) { 164 | var args struct { 165 | Level slog.Level `default:"info"` 166 | } 167 | 168 | var flagset flag.FlagSet 169 | err := flagsfiller.New().Fill(&flagset, &args) 170 | require.NoError(t, err) 171 | 172 | err = flagset.Parse([]string{}) 173 | require.NoError(t, err) 174 | 175 | assert.Equal(t, slog.LevelInfo, args.Level) 176 | } 177 | -------------------------------------------------------------------------------- /docs.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package flagsfiller makes Go's flag package pleasant to use by mapping the fields of a given struct 3 | into flags in a FlagSet. 4 | 5 | # Quick Start 6 | 7 | A FlagSetFiller is created with the New constructor, passing it any desired FillerOptions. 8 | With that, call Fill, passing it a flag.FlatSet, such as flag.CommandLine, and your struct to 9 | be mapped. 10 | 11 | Even a simple struct with no special changes can be used, such as: 12 | 13 | type Config struct { 14 | Host string 15 | Enabled bool 16 | } 17 | var config Config 18 | 19 | // create a FlagSetFiller 20 | filler := flagsfiller.New() 21 | // fill and map struct fields to flags 22 | filler.Fill(flag.CommandLine, &config) 23 | // parse command-line like usual 24 | flag.Parse() 25 | 26 | After calling Parse on the flag.FlagSet, the corresponding fields of the mapped struct will 27 | be populated with values passed from the command-line. 28 | 29 | For an even quicker start, flagsfiller provides a convenience Parse function that does the same 30 | as the snippet above in one call: 31 | 32 | type Config struct { 33 | Host string 34 | Enabled bool 35 | } 36 | var config Config 37 | 38 | flagsfiller.Parse(&config) 39 | 40 | # Flag Naming 41 | 42 | By default, the flags are named by taking the field name and performing a word-wise conversion 43 | to kebab-case. For example the field named "MyMultiWordField" becomes the flag named 44 | "my-multi-word-field". 45 | 46 | The naming strategy can be changed by passing a custom Renamer using the WithFieldRenamer 47 | option in the constructor. 48 | 49 | Additional aliases, such as short names, can be declared with the `aliases` tag as a comma-separated list: 50 | 51 | type Config struct { 52 | Timeout time.Duration `aliases:"t"` 53 | Limit int `aliases:"l,lim"` 54 | } 55 | 56 | # Nested Structs 57 | 58 | FlagSetFiller supports nested structs and computes the flag names by prefixing the field 59 | name of the struct to the names of the fields it contains. For example, the following maps to 60 | the flags named remote-host, remote-auth-username, and remote-auth-password: 61 | 62 | type Config struct { 63 | Remote struct { 64 | Host string 65 | Auth struct { 66 | Username string 67 | Password string 68 | } 69 | } 70 | } 71 | 72 | # Flag Usage 73 | 74 | To declare a flag's usage add a `usage:""` tag to the field, such as: 75 | 76 | type Config struct { 77 | Host string `usage:"the name of the host to access"` 78 | } 79 | 80 | Since flag.UnquoteUsage normally uses back quotes to locate the argument placeholder name but 81 | struct tags also use back quotes, flagsfiller will instead use [square brackets] to define the 82 | placeholder name, such as: 83 | 84 | SomeUrl string `usage:"a [URL] to configure"` 85 | 86 | results in the rendered output: 87 | 88 | -some-url URL 89 | a URL to configure 90 | 91 | # Defaults 92 | 93 | To declare the default value of a flag, you can either set a field's value before passing the 94 | struct to process, such as: 95 | 96 | type Config struct { 97 | Host string 98 | } 99 | var config = Config{Host:"localhost"} 100 | 101 | or add a `default:""` tag to the field. Be sure to provide a valid string that can be 102 | converted into the field's type. For example, 103 | 104 | type Config struct { 105 | Host string `default:"localhost"` 106 | Timeout time.Duration `default:"1m"` 107 | } 108 | 109 | # String Slices 110 | 111 | FlagSetFiller also includes support for []string fields. 112 | Repetition of the argument appends to the slice and/or an argument value can contain a 113 | comma or newline separated list of values. 114 | 115 | For example: 116 | 117 | --arg one --arg two,three 118 | 119 | results in a three element slice. 120 | 121 | The default tag's value is provided as a comma-separated list, such as 122 | 123 | MultiValues []string `default:"one,two,three"` 124 | 125 | # Maps of String to String 126 | 127 | FlagSetFiller also includes support for map[string]string fields. 128 | Each argument entry is a key=value and/or repetition of the arguments adds to the map or 129 | multiple entries can be comma or newline separated in a single argument value. 130 | 131 | For example: 132 | 133 | --arg k1=v1 --arg k2=v2,k3=v3 134 | 135 | results in a map with three entries. 136 | 137 | The default tag's value is provided a comma-separate list of key=value entries, such as 138 | 139 | Mappings map[string]string `default:"k1=v1,k2=v2,k3=v3"` 140 | 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 | - slog.Level: parsed as specified by https://pkg.go.dev/log/slog#Level.UnmarshalText, such as "info" 150 | 151 | # Environment variable mapping 152 | 153 | To activate the setting of flag values from environment variables, pass the WithEnv option to 154 | flagsfiller.New or flagsfiller.Parse. That option takes a prefix that will be prepended to the 155 | resolved field name and then the whole thing is converted to SCREAMING_SNAKE_CASE. 156 | 157 | The environment variable name will be automatically included in the flag usage along with the 158 | standard inclusion of the default value. For example, using the option WithEnv("App") along 159 | with the following field declaration 160 | 161 | Host string `default:"localhost" usage:"the host to use"` 162 | 163 | would render the following usage: 164 | 165 | -host string 166 | the host to use (env APP_HOST) (default "localhost") 167 | 168 | # Per-field overrides 169 | 170 | To override the naming of a flag, the field can be declared with the tag `flag:"name"` where 171 | the given name will be used exactly as the flag name. An empty string for the name indicates 172 | the field should be ignored and no flag is declared. For example, 173 | 174 | Host string `flag:"server_address" 175 | GetsIgnored string `flag:""` 176 | 177 | Environment variable naming and processing can be overridden with the `env:"name"` tag, where 178 | the given name will be used exactly as the mapped environment variable name. If the WithEnv 179 | or WithEnvRenamer options were enabled, a field can be excluded from environment variable 180 | mapping by giving an empty string. Conversely, environment variable mapping can be enabled 181 | per field with `env:"name"` even when the flagsfiller-wide option was not included. For example, 182 | 183 | Host string `env:"SERVER_ADDRESS"` 184 | NotEnvMapped string `env:""` 185 | */ 186 | package flagsfiller 187 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package flagsfiller_test 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/itzg/go-flagsfiller" 7 | "log" 8 | "time" 9 | ) 10 | 11 | func Example() { 12 | type Config struct { 13 | Host string `default:"localhost" usage:"The remote host"` 14 | Enabled bool `default:"true" usage:"Turn it on"` 15 | Automatic bool `default:"false" usage:"Make it automatic" aliases:"a"` 16 | Retries int `default:"1" usage:"Retry" aliases:"r,t"` 17 | Timeout time.Duration `default:"5s" usage:"How long to wait"` 18 | } 19 | 20 | var config Config 21 | 22 | flagset := flag.NewFlagSet("ExampleBasic", flag.ExitOnError) 23 | 24 | filler := flagsfiller.New() 25 | err := filler.Fill(flagset, &config) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | 30 | err = flagset.Parse([]string{"--host", "external.svc", "--timeout", "10m", "-a", "-t", "2"}) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | 35 | fmt.Printf("%+v\n", config) 36 | // Output: 37 | // {Host:external.svc Enabled:true Automatic:true Retries:2 Timeout:10m0s} 38 | } 39 | -------------------------------------------------------------------------------- /examples/flagsfiller-example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "github.com/itzg/go-flagsfiller" 7 | "log" 8 | "time" 9 | ) 10 | 11 | type Config struct { 12 | Host string `default:"localhost" usage:"The remote host"` 13 | DebugEnabled bool `default:"true" usage:"Show debugs"` 14 | MaxTimeout time.Duration `default:"5s" usage:"How long to wait"` 15 | IgnoreCertificate bool `default:"false" usage:"Make it automatic" aliase:"k"` 16 | Feature struct { 17 | Faster bool `usage:"Go faster"` 18 | LudicrousSpeed bool `usage:"Go even faster"` 19 | } 20 | } 21 | 22 | func main() { 23 | var config Config 24 | 25 | filler := flagsfiller.New() 26 | err := filler.Fill(flag.CommandLine, &config) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | flag.Parse() 32 | 33 | fmt.Printf("Loaded: %+v\n", config) 34 | } 35 | -------------------------------------------------------------------------------- /flagset.go: -------------------------------------------------------------------------------- 1 | package flagsfiller 2 | 3 | import ( 4 | "encoding" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "reflect" 9 | "regexp" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | var ( 16 | durationType = reflect.TypeOf(time.Duration(0)) 17 | stringSliceType = reflect.TypeOf([]string{}) 18 | stringToStringMapType = reflect.TypeOf(map[string]string{}) 19 | ) 20 | 21 | // FlagSetFiller is used to map the fields of a struct into flags of a flag.FlagSet 22 | type FlagSetFiller struct { 23 | options *fillerOptions 24 | } 25 | 26 | // Parse is a convenience function that creates a FlagSetFiller with the given options, 27 | // fills and maps the flags from the given struct reference into flag.CommandLine, and uses 28 | // flag.Parse to parse the os.Args. 29 | // Returns an error if the given struct could not be used for filling flags. 30 | func Parse(from interface{}, options ...FillerOption) error { 31 | filler := New(options...) 32 | err := filler.Fill(flag.CommandLine, from) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | flag.Parse() 38 | return nil 39 | } 40 | 41 | // New creates a new FlagSetFiller with zero or more of the given FillerOption's 42 | func New(options ...FillerOption) *FlagSetFiller { 43 | return &FlagSetFiller{options: newFillerOptions(options...)} 44 | } 45 | 46 | // Fill populates the flagSet with a flag for each field in given struct passed in the 'from' 47 | // argument which must be a struct reference. 48 | // Fill returns an error when a non-struct reference is passed as 'from' or a field has a 49 | // default tag which could not converted to the field's type. 50 | func (f *FlagSetFiller) Fill(flagSet *flag.FlagSet, from interface{}) error { 51 | v := reflect.ValueOf(from) 52 | t := v.Type() 53 | if t.Kind() == reflect.Ptr && t.Elem().Kind() == reflect.Struct { 54 | return f.walkFields(flagSet, "", v.Elem(), t.Elem()) 55 | } else { 56 | return fmt.Errorf("can only fill from struct pointer, but it was %s", t.Kind()) 57 | } 58 | } 59 | 60 | func isSupportedStruct(in any) bool { 61 | t := reflect.TypeOf(in) 62 | _, ok := extendedTypes[getTypeName(t)] 63 | if ok { 64 | return true 65 | } 66 | if t.Kind() != reflect.Pointer { 67 | val := reflect.ValueOf(in) 68 | t = val.Addr().Type() 69 | } 70 | if t.Implements(reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem()) { 71 | RegisterTextUnmarshaler(in) 72 | return true 73 | } 74 | return false 75 | } 76 | 77 | func getTypeName(t reflect.Type) string { 78 | if t.Kind() == reflect.Pointer { 79 | t = t.Elem() 80 | } 81 | return fmt.Sprint(t) 82 | } 83 | 84 | func (f *FlagSetFiller) walkFields(flagSet *flag.FlagSet, prefix string, 85 | structVal reflect.Value, structType reflect.Type) error { 86 | 87 | if prefix != "" { 88 | prefix += "-" 89 | } 90 | handleDefault := func(field reflect.StructField, fieldValue reflect.Value) error { 91 | addr := fieldValue.Addr() 92 | // make sure it is exported/public 93 | ftype := field.Type 94 | if field.Type.Kind() == reflect.Ptr { 95 | ftype = field.Type.Elem() 96 | } 97 | if addr.CanInterface() { 98 | err := f.processField(flagSet, addr.Interface(), prefix+field.Name, ftype, field.Tag) 99 | if err != nil { 100 | return fmt.Errorf("failed to process %s of %s: %w", field.Name, structType.String(), err) 101 | } 102 | } 103 | return nil 104 | } 105 | for i := 0; i < structVal.NumField(); i++ { 106 | field := structType.Field(i) 107 | fieldValue := structVal.Field(i) 108 | 109 | if flagTag, ok := field.Tag.Lookup("flag"); ok { 110 | if flagTag == "" { 111 | continue 112 | } 113 | } 114 | 115 | switch field.Type.Kind() { 116 | case reflect.Struct: 117 | // fieldTypeName := getTypeName(field.Type) 118 | if field.IsExported() { 119 | if isSupportedStruct(fieldValue.Addr().Interface()) { 120 | err := handleDefault(field, fieldValue) 121 | if err != nil { 122 | return err 123 | } 124 | continue 125 | } 126 | } 127 | err := f.walkFields(flagSet, prefix+field.Name, fieldValue, field.Type) 128 | if err != nil { 129 | return fmt.Errorf("failed to process %s of %s: %w", field.Name, structType.String(), err) 130 | } 131 | 132 | case reflect.Ptr: 133 | if fieldValue.CanSet() && field.Type.Elem().Kind() == reflect.Struct { 134 | // fieldTypeName := getTypeName(field.Type.Elem()) 135 | // fill the pointer with a new struct of their type if it is nil 136 | if fieldValue.IsNil() { 137 | fieldValue.Set(reflect.New(field.Type.Elem())) 138 | } 139 | if field.IsExported() { 140 | if isSupportedStruct(fieldValue.Interface()) { 141 | err := handleDefault(field, fieldValue.Elem()) 142 | if err != nil { 143 | return err 144 | } 145 | continue 146 | } 147 | } 148 | 149 | err := f.walkFields(flagSet, field.Name, fieldValue.Elem(), field.Type.Elem()) 150 | if err != nil { 151 | return fmt.Errorf("failed to process %s of %s: %w", field.Name, structType.String(), err) 152 | } 153 | } 154 | 155 | default: 156 | err := handleDefault(field, fieldValue) 157 | if err != nil { 158 | return err 159 | } 160 | } 161 | } 162 | 163 | return nil 164 | } 165 | 166 | func (f *FlagSetFiller) processField(flagSet *flag.FlagSet, fieldRef interface{}, 167 | name string, t reflect.Type, tag reflect.StructTag) (err error) { 168 | 169 | var envName string 170 | if override, exists := tag.Lookup("env"); exists { 171 | envName = override 172 | } else if len(f.options.envRenamer) > 0 { 173 | envName = name 174 | for _, renamer := range f.options.envRenamer { 175 | envName = renamer(envName) 176 | } 177 | } 178 | 179 | aliases := tag.Get("aliases") 180 | usage := requoteUsage(tag.Get("usage")) 181 | if envName != "" { 182 | usage = fmt.Sprintf("%s (env %s)", usage, envName) 183 | } 184 | 185 | tagDefault, hasDefaultTag := tag.Lookup("default") 186 | 187 | fieldType, _ := tag.Lookup("type") 188 | 189 | var renamed string 190 | if override, exists := tag.Lookup("flag"); exists { 191 | if override == "" { 192 | // empty flag override signal to skip this field 193 | return nil 194 | } 195 | renamed = override 196 | } else { 197 | renamed = f.options.renameLongName(name) 198 | } 199 | // go through all supported structs 200 | if isSupportedStruct(fieldRef) { 201 | handler := extendedTypes[getTypeName(t)] 202 | err = handler(tag, fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 203 | return err 204 | } 205 | 206 | switch { 207 | case t.Kind() == reflect.String: 208 | f.processString(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 209 | 210 | case t.Kind() == reflect.Bool: 211 | err = f.processBool(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 212 | 213 | case t.Kind() == reflect.Float64: 214 | err = f.processFloat64(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 215 | 216 | // NOTE check time.Duration before int64 since it is aliasesed from int64 217 | case t == durationType, fieldType == "duration": 218 | err = f.processDuration(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 219 | 220 | case t.Kind() == reflect.Int64: 221 | err = f.processInt64(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 222 | 223 | case t.Kind() == reflect.Int: 224 | err = f.processInt(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 225 | 226 | case t.Kind() == reflect.Uint64: 227 | err = f.processUint64(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 228 | 229 | case t.Kind() == reflect.Uint: 230 | err = f.processUint(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 231 | 232 | case t == stringSliceType, fieldType == "stringSlice": 233 | var override bool 234 | if overrideValue, exists := tag.Lookup("override-value"); exists { 235 | if value, err := strconv.ParseBool(overrideValue); err == nil { 236 | override = value 237 | } 238 | } 239 | f.processStringSlice(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, override, aliases) 240 | 241 | case t == stringToStringMapType, fieldType == "stringMap": 242 | f.processStringToStringMap(fieldRef, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 243 | 244 | // ignore any other types 245 | } 246 | 247 | if err != nil { 248 | return err 249 | } 250 | 251 | if !f.options.noSetFromEnv && envName != "" { 252 | if val, exists := os.LookupEnv(envName); exists { 253 | err := flagSet.Lookup(renamed).Value.Set(val) 254 | if err != nil { 255 | return fmt.Errorf("failed to set from environment variable %s: %w", 256 | envName, err) 257 | } 258 | } 259 | } 260 | 261 | return nil 262 | } 263 | 264 | func (f *FlagSetFiller) processStringToStringMap(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) { 265 | casted, ok := fieldRef.(*map[string]string) 266 | if !ok { 267 | _ = f.processCustom( 268 | fieldRef, 269 | func(s string) (interface{}, error) { 270 | return parseStringToStringMap(s), nil 271 | }, 272 | hasDefaultTag, 273 | tagDefault, 274 | flagSet, 275 | renamed, 276 | usage, 277 | aliases, 278 | ) 279 | return 280 | } 281 | var val map[string]string 282 | if hasDefaultTag { 283 | val = parseStringToStringMap(tagDefault) 284 | *casted = val 285 | } else if *casted == nil { 286 | val = make(map[string]string) 287 | *casted = val 288 | } else { 289 | val = *casted 290 | } 291 | flagSet.Var(&strToStrMapVar{val: val}, renamed, usage) 292 | if aliases != "" { 293 | for _, alias := range strings.Split(aliases, ",") { 294 | flagSet.Var(&strToStrMapVar{val: val}, alias, usage) 295 | } 296 | } 297 | } 298 | 299 | func (f *FlagSetFiller) processStringSlice(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, override bool, aliases string) { 300 | casted, ok := fieldRef.(*[]string) 301 | if !ok { 302 | _ = f.processCustom( 303 | fieldRef, 304 | func(s string) (interface{}, error) { 305 | return parseStringSlice(s, f.options.valueSplitPattern), nil 306 | }, 307 | hasDefaultTag, 308 | tagDefault, 309 | flagSet, 310 | renamed, 311 | usage, 312 | aliases, 313 | ) 314 | return 315 | } 316 | if hasDefaultTag { 317 | *casted = parseStringSlice(tagDefault, f.options.valueSplitPattern) 318 | } 319 | flagSet.Var(&strSliceVar{ 320 | ref: casted, 321 | override: override, 322 | valueSplitPattern: f.options.valueSplitPattern, 323 | }, renamed, usage) 324 | if aliases != "" { 325 | for _, alias := range strings.Split(aliases, ",") { 326 | flagSet.Var(&strSliceVar{ 327 | ref: casted, 328 | override: override, 329 | valueSplitPattern: f.options.valueSplitPattern, 330 | }, alias, usage) 331 | } 332 | } 333 | } 334 | 335 | func (f *FlagSetFiller) processUint(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) (err error) { 336 | casted, ok := fieldRef.(*uint) 337 | if !ok { 338 | return f.processCustom( 339 | fieldRef, 340 | func(s string) (interface{}, error) { 341 | value, err := strconv.Atoi(s) 342 | return value, err 343 | }, 344 | hasDefaultTag, 345 | tagDefault, 346 | flagSet, 347 | renamed, 348 | usage, 349 | aliases, 350 | ) 351 | } 352 | var defaultVal uint 353 | if hasDefaultTag { 354 | var asInt int 355 | asInt, err = strconv.Atoi(tagDefault) 356 | defaultVal = uint(asInt) 357 | if err != nil { 358 | return fmt.Errorf("failed to parse default into uint: %w", err) 359 | } 360 | } else { 361 | defaultVal = *casted 362 | } 363 | flagSet.UintVar(casted, renamed, defaultVal, usage) 364 | if aliases != "" { 365 | for _, alias := range strings.Split(aliases, ",") { 366 | flagSet.UintVar(casted, alias, defaultVal, usage) 367 | } 368 | } 369 | return err 370 | } 371 | 372 | func (f *FlagSetFiller) processUint64(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) (err error) { 373 | casted, ok := fieldRef.(*uint64) 374 | if !ok { 375 | return f.processCustom( 376 | fieldRef, 377 | func(s string) (interface{}, error) { 378 | value, err := strconv.ParseUint(s, 10, 64) 379 | return value, err 380 | }, 381 | hasDefaultTag, 382 | tagDefault, 383 | flagSet, 384 | renamed, 385 | usage, 386 | aliases, 387 | ) 388 | } 389 | var defaultVal uint64 390 | if hasDefaultTag { 391 | defaultVal, err = strconv.ParseUint(tagDefault, 10, 64) 392 | if err != nil { 393 | return fmt.Errorf("failed to parse default into uint64: %w", err) 394 | } 395 | } else { 396 | defaultVal = *casted 397 | } 398 | flagSet.Uint64Var(casted, renamed, defaultVal, usage) 399 | if aliases != "" { 400 | for _, alias := range strings.Split(aliases, ",") { 401 | flagSet.Uint64Var(casted, alias, defaultVal, usage) 402 | } 403 | } 404 | return err 405 | } 406 | 407 | func (f *FlagSetFiller) processInt(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) (err error) { 408 | casted, ok := fieldRef.(*int) 409 | if !ok { 410 | return f.processCustom( 411 | fieldRef, 412 | func(s string) (interface{}, error) { 413 | value, err := strconv.Atoi(s) 414 | return value, err 415 | }, 416 | hasDefaultTag, 417 | tagDefault, 418 | flagSet, 419 | renamed, 420 | usage, 421 | aliases, 422 | ) 423 | } 424 | var defaultVal int 425 | if hasDefaultTag { 426 | defaultVal, err = strconv.Atoi(tagDefault) 427 | if err != nil { 428 | return fmt.Errorf("failed to parse default into int: %w", err) 429 | } 430 | } else { 431 | defaultVal = *casted 432 | } 433 | flagSet.IntVar(casted, renamed, defaultVal, usage) 434 | if aliases != "" { 435 | for _, alias := range strings.Split(aliases, ",") { 436 | flagSet.IntVar(casted, alias, defaultVal, usage) 437 | } 438 | } 439 | return err 440 | } 441 | 442 | func (f *FlagSetFiller) processInt64(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) (err error) { 443 | casted, ok := fieldRef.(*int64) 444 | if !ok { 445 | return f.processCustom( 446 | fieldRef, 447 | func(s string) (interface{}, error) { 448 | value, err := strconv.ParseInt(s, 10, 64) 449 | return value, err 450 | }, 451 | hasDefaultTag, 452 | tagDefault, 453 | flagSet, 454 | renamed, 455 | usage, 456 | aliases, 457 | ) 458 | } 459 | var defaultVal int64 460 | if hasDefaultTag { 461 | defaultVal, err = strconv.ParseInt(tagDefault, 10, 64) 462 | if err != nil { 463 | return fmt.Errorf("failed to parse default into int64: %w", err) 464 | } 465 | } else { 466 | defaultVal = *casted 467 | } 468 | flagSet.Int64Var(casted, renamed, defaultVal, usage) 469 | if aliases != "" { 470 | for _, alias := range strings.Split(aliases, ",") { 471 | flagSet.Int64Var(casted, alias, defaultVal, usage) 472 | } 473 | } 474 | return nil 475 | } 476 | 477 | func (f *FlagSetFiller) processDuration(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) (err error) { 478 | casted, ok := fieldRef.(*time.Duration) 479 | if !ok { 480 | return f.processCustom( 481 | fieldRef, 482 | func(s string) (interface{}, error) { 483 | value, err := time.ParseDuration(s) 484 | return value, err 485 | }, 486 | hasDefaultTag, 487 | tagDefault, 488 | flagSet, 489 | renamed, 490 | usage, 491 | aliases, 492 | ) 493 | } 494 | var defaultVal time.Duration 495 | if hasDefaultTag { 496 | defaultVal, err = time.ParseDuration(tagDefault) 497 | if err != nil { 498 | return fmt.Errorf("failed to parse default into time.Duration: %w", err) 499 | } 500 | } else { 501 | defaultVal = *casted 502 | } 503 | flagSet.DurationVar(casted, renamed, defaultVal, usage) 504 | if aliases != "" { 505 | for _, alias := range strings.Split(aliases, ",") { 506 | flagSet.DurationVar(casted, alias, defaultVal, usage) 507 | } 508 | } 509 | return nil 510 | } 511 | 512 | func (f *FlagSetFiller) processFloat64(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) (err error) { 513 | casted, ok := fieldRef.(*float64) 514 | if !ok { 515 | return f.processCustom( 516 | fieldRef, 517 | func(s string) (interface{}, error) { 518 | value, err := strconv.ParseFloat(s, 64) 519 | return value, err 520 | }, 521 | hasDefaultTag, 522 | tagDefault, 523 | flagSet, 524 | renamed, 525 | usage, 526 | aliases, 527 | ) 528 | } 529 | var defaultVal float64 530 | if hasDefaultTag { 531 | defaultVal, err = strconv.ParseFloat(tagDefault, 64) 532 | if err != nil { 533 | return fmt.Errorf("failed to parse default into float64: %w", err) 534 | } 535 | } else { 536 | defaultVal = *casted 537 | } 538 | flagSet.Float64Var(casted, renamed, defaultVal, usage) 539 | if aliases != "" { 540 | for _, alias := range strings.Split(aliases, ",") { 541 | flagSet.Float64Var(casted, alias, defaultVal, usage) 542 | } 543 | } 544 | return nil 545 | } 546 | 547 | func (f *FlagSetFiller) processBool(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) (err error) { 548 | casted, ok := fieldRef.(*bool) 549 | if !ok { 550 | return f.processCustom( 551 | fieldRef, 552 | func(s string) (interface{}, error) { 553 | value, err := strconv.ParseBool(s) 554 | return value, err 555 | }, 556 | hasDefaultTag, 557 | tagDefault, 558 | flagSet, 559 | renamed, 560 | usage, 561 | aliases, 562 | ) 563 | } 564 | var defaultVal bool 565 | if hasDefaultTag { 566 | defaultVal, err = strconv.ParseBool(tagDefault) 567 | if err != nil { 568 | return fmt.Errorf("failed to parse default into bool: %w", err) 569 | } 570 | } else { 571 | defaultVal = *casted 572 | } 573 | flagSet.BoolVar(casted, renamed, defaultVal, usage) 574 | if aliases != "" { 575 | for _, alias := range strings.Split(aliases, ",") { 576 | flagSet.BoolVar(casted, alias, defaultVal, usage) 577 | } 578 | } 579 | return nil 580 | } 581 | 582 | func (f *FlagSetFiller) processString(fieldRef interface{}, hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) { 583 | casted, ok := fieldRef.(*string) 584 | if !ok { 585 | _ = f.processCustom( 586 | fieldRef, 587 | func(s string) (interface{}, error) { 588 | return s, nil 589 | }, 590 | hasDefaultTag, 591 | tagDefault, 592 | flagSet, 593 | renamed, 594 | usage, 595 | aliases, 596 | ) 597 | return 598 | } 599 | var defaultVal string 600 | if hasDefaultTag { 601 | defaultVal = tagDefault 602 | } else { 603 | defaultVal = *casted 604 | } 605 | flagSet.StringVar(casted, renamed, defaultVal, usage) 606 | if aliases != "" { 607 | for _, alias := range strings.Split(aliases, ",") { 608 | flagSet.StringVar(casted, alias, defaultVal, usage) 609 | } 610 | } 611 | } 612 | 613 | func (f *FlagSetFiller) processCustom(fieldRef interface{}, converter func(string) (interface{}, error), hasDefaultTag bool, tagDefault string, flagSet *flag.FlagSet, renamed string, usage string, aliases string) error { 614 | if hasDefaultTag { 615 | value, err := converter(tagDefault) 616 | if err != nil { 617 | return fmt.Errorf("failed to parse default into custom type: %w", err) 618 | } 619 | reflect.ValueOf(fieldRef).Elem().Set(reflect.ValueOf(value).Convert(reflect.TypeOf(fieldRef).Elem())) 620 | } 621 | flagSet.Func(renamed, usage, func(s string) error { 622 | value, err := converter(s) 623 | if err != nil { 624 | return err 625 | } 626 | reflect.ValueOf(fieldRef).Elem().Set(reflect.ValueOf(value).Convert(reflect.TypeOf(fieldRef).Elem())) 627 | return nil 628 | }) 629 | if aliases != "" { 630 | for _, alias := range strings.Split(aliases, ",") { 631 | flagSet.Func(alias, usage, func(s string) error { 632 | value, err := converter(s) 633 | if err != nil { 634 | return err 635 | } 636 | reflect.ValueOf(fieldRef).Elem().Set(reflect.ValueOf(value).Convert(reflect.TypeOf(fieldRef).Elem())) 637 | return nil 638 | }) 639 | } 640 | } 641 | return nil 642 | } 643 | 644 | type strSliceVar struct { 645 | ref *[]string 646 | override bool 647 | valueSplitPattern string 648 | } 649 | 650 | func (s *strSliceVar) String() string { 651 | if s.ref == nil { 652 | return "" 653 | } 654 | return strings.Join(*s.ref, ",") 655 | } 656 | 657 | func (s *strSliceVar) Set(val string) error { 658 | parts := parseStringSlice(val, s.valueSplitPattern) 659 | 660 | if s.override { 661 | *s.ref = parts 662 | return nil 663 | } 664 | 665 | *s.ref = append(*s.ref, parts...) 666 | 667 | return nil 668 | } 669 | 670 | func parseStringSlice(val string, valueSplitPattern string) []string { 671 | if valueSplitPattern == "" { 672 | return []string{val} 673 | } 674 | 675 | splitter := regexp.MustCompile(valueSplitPattern) 676 | parts := splitter.Split(val, -1) 677 | 678 | // trim out blank parts 679 | result := make([]string, 0, len(parts)) 680 | for _, s := range parts { 681 | s = strings.TrimSpace(s) 682 | if s != "" { 683 | result = append(result, s) 684 | } 685 | } 686 | return result 687 | } 688 | 689 | type strToStrMapVar struct { 690 | val map[string]string 691 | } 692 | 693 | func (s strToStrMapVar) String() string { 694 | if s.val == nil { 695 | return "" 696 | } 697 | 698 | var sb strings.Builder 699 | first := true 700 | for k, v := range s.val { 701 | if !first { 702 | sb.WriteString(",") 703 | } else { 704 | first = false 705 | } 706 | sb.WriteString(k) 707 | sb.WriteString("=") 708 | sb.WriteString(v) 709 | } 710 | return sb.String() 711 | } 712 | 713 | func (s strToStrMapVar) Set(val string) error { 714 | content := parseStringToStringMap(val) 715 | for k, v := range content { 716 | s.val[k] = v 717 | } 718 | return nil 719 | } 720 | 721 | func parseStringToStringMap(val string) map[string]string { 722 | result := make(map[string]string) 723 | 724 | splitter := regexp.MustCompile("[\n,]") 725 | 726 | pairs := splitter.Split(val, -1) 727 | for _, pair := range pairs { 728 | pair = strings.TrimSpace(pair) 729 | 730 | if pair != "" { 731 | kv := strings.SplitN(pair, "=", 2) 732 | if len(kv) == 2 { 733 | result[kv[0]] = kv[1] 734 | } else { 735 | result[kv[0]] = "" 736 | } 737 | } 738 | } 739 | 740 | return result 741 | } 742 | 743 | // requoteUsage converts a [name] quoted usage string into the back quote form processed by flag.UnquoteUsage 744 | func requoteUsage(usage string) string { 745 | return strings.Map(func(r rune) rune { 746 | switch r { 747 | case '[': 748 | return '`' 749 | case ']': 750 | return '`' 751 | default: 752 | return r 753 | } 754 | }, usage) 755 | } 756 | -------------------------------------------------------------------------------- /flagset_test.go: -------------------------------------------------------------------------------- 1 | package flagsfiller_test 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "os" 8 | "testing" 9 | "time" 10 | 11 | "github.com/iancoleman/strcase" 12 | "github.com/itzg/go-flagsfiller" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestStringFields(t *testing.T) { 18 | type Config struct { 19 | Host string 20 | MultiWordName string 21 | } 22 | 23 | var config Config 24 | 25 | filler := flagsfiller.New() 26 | 27 | var flagset flag.FlagSet 28 | err := filler.Fill(&flagset, &config) 29 | require.NoError(t, err) 30 | 31 | err = flagset.Parse([]string{"--host", "h1", "--multi-word-name", "val1"}) 32 | require.NoError(t, err) 33 | 34 | assert.Equal(t, "h1", config.Host) 35 | assert.Equal(t, "val1", config.MultiWordName) 36 | } 37 | 38 | func TestCustomFields(t *testing.T) { 39 | type CustomStringType string 40 | type CustomBoolType bool 41 | type CustomFloat64 float64 42 | type CustomDuration time.Duration 43 | type CustomInt64 int64 44 | type CustomInt int 45 | type CustomUint64 uint64 46 | type CustomUint uint 47 | type CustomStringSlice []string 48 | type CustomStringMap map[string]string 49 | 50 | t.Run("Default values", func(t *testing.T) { 51 | type Config struct { 52 | String CustomStringType `default:"stringValue"` 53 | Bool CustomBoolType `default:"true"` 54 | Float64 CustomFloat64 `default:"1.234"` 55 | Duration CustomDuration `type:"duration" default:"2s"` 56 | Int64 CustomInt64 `default:"-1"` 57 | Int CustomInt `default:"-2"` 58 | Uint64 CustomUint64 `default:"1"` 59 | Uint CustomUint `default:"2"` 60 | StringSlice CustomStringSlice `type:"stringSlice" default:"one,two"` 61 | StringMap CustomStringMap `type:"stringMap" default:"one=value1,two=value2"` 62 | } 63 | 64 | var config Config 65 | 66 | filler := flagsfiller.New() 67 | 68 | var flagset flag.FlagSet 69 | err := filler.Fill(&flagset, &config) 70 | require.NoError(t, err) 71 | 72 | err = flagset.Parse([]string{}) 73 | require.NoError(t, err) 74 | 75 | assert.Equal(t, CustomStringType("stringValue"), config.String) 76 | assert.Equal(t, CustomBoolType(true), config.Bool) 77 | assert.Equal(t, CustomFloat64(1.234), config.Float64) 78 | assert.Equal(t, CustomDuration(2*time.Second), config.Duration) 79 | assert.Equal(t, CustomInt64(-1), config.Int64) 80 | assert.Equal(t, CustomInt(-2), config.Int) 81 | assert.Equal(t, CustomUint64(1), config.Uint64) 82 | assert.Equal(t, CustomUint(2), config.Uint) 83 | assert.Equal(t, CustomStringSlice{"one", "two"}, config.StringSlice) 84 | assert.Equal(t, CustomStringMap{"one": "value1", "two": "value2"}, config.StringMap) 85 | }) 86 | 87 | t.Run("Values set from arguments", func(t *testing.T) { 88 | type Config struct { 89 | String CustomStringType 90 | Bool CustomBoolType 91 | Float64 CustomFloat64 92 | Duration CustomDuration `type:"duration"` 93 | Int64 CustomInt64 94 | Int CustomInt 95 | Uint64 CustomUint64 96 | Uint CustomUint 97 | StringSlice CustomStringSlice `type:"stringSlice"` 98 | StringMap CustomStringMap `type:"stringMap"` 99 | } 100 | 101 | var config Config 102 | 103 | filler := flagsfiller.New() 104 | 105 | var flagset flag.FlagSet 106 | err := filler.Fill(&flagset, &config) 107 | require.NoError(t, err) 108 | 109 | err = flagset.Parse([]string{ 110 | "--string", "stringValue", 111 | "--bool", "true", 112 | "--float-64", "1.234", 113 | "--duration", "2s", 114 | "--int-64", "-1", 115 | "--int", "-2", 116 | "--uint-64", "1", 117 | "--uint", "2", 118 | "--string-slice", "one,two", 119 | "--string-map", "one=value1,two=value2", 120 | }) 121 | require.NoError(t, err) 122 | 123 | assert.Equal(t, CustomStringType("stringValue"), config.String) 124 | assert.Equal(t, CustomBoolType(true), config.Bool) 125 | assert.Equal(t, CustomFloat64(1.234), config.Float64) 126 | assert.Equal(t, CustomDuration(2*time.Second), config.Duration) 127 | assert.Equal(t, CustomInt64(-1), config.Int64) 128 | assert.Equal(t, CustomInt(-2), config.Int) 129 | assert.Equal(t, CustomUint64(1), config.Uint64) 130 | assert.Equal(t, CustomUint(2), config.Uint) 131 | assert.Equal(t, CustomStringSlice{"one", "two"}, config.StringSlice) 132 | assert.Equal(t, CustomStringMap{"one": "value1", "two": "value2"}, config.StringMap) 133 | }) 134 | } 135 | 136 | func TestUsage(t *testing.T) { 137 | type Config struct { 138 | MultiWordName string `usage:"usage goes here"` 139 | } 140 | 141 | var config Config 142 | 143 | filler := flagsfiller.New() 144 | 145 | var flagset flag.FlagSet 146 | err := filler.Fill(&flagset, &config) 147 | require.NoError(t, err) 148 | 149 | buf := grabUsage(flagset) 150 | 151 | assert.Equal(t, ` 152 | -multi-word-name string 153 | usage goes here 154 | `, buf.String()) 155 | } 156 | 157 | func TestRenamerOption(t *testing.T) { 158 | type Config struct { 159 | Host string 160 | MultiWordName string 161 | } 162 | 163 | var config Config 164 | 165 | filler := flagsfiller.New(flagsfiller.WithFieldRenamer(strcase.ToSnake)) 166 | 167 | var flagset flag.FlagSet 168 | err := filler.Fill(&flagset, &config) 169 | require.NoError(t, err) 170 | 171 | err = flagset.Parse([]string{"--host", "h1", "--multi_word_name", "val1"}) 172 | require.NoError(t, err) 173 | 174 | assert.Equal(t, "h1", config.Host) 175 | assert.Equal(t, "val1", config.MultiWordName) 176 | } 177 | 178 | func TestNestedFields(t *testing.T) { 179 | type Config struct { 180 | Host string 181 | SomeGrouping struct { 182 | SomeField string 183 | } 184 | ALLCAPS struct { 185 | ALLCAPS string 186 | } 187 | } 188 | 189 | var config Config 190 | 191 | filler := flagsfiller.New() 192 | 193 | var flagset flag.FlagSet 194 | err := filler.Fill(&flagset, &config) 195 | require.NoError(t, err) 196 | 197 | err = flagset.Parse([]string{"--host", "h1", "--some-grouping-some-field", "val1", "--allcaps-allcaps", "val2"}) 198 | require.NoError(t, err) 199 | 200 | assert.Equal(t, "h1", config.Host) 201 | assert.Equal(t, "val1", config.SomeGrouping.SomeField) 202 | assert.Equal(t, "val2", config.ALLCAPS.ALLCAPS) 203 | } 204 | 205 | func TestNestedAdjacentFields(t *testing.T) { 206 | type SomeGrouping struct { 207 | SomeField string 208 | EvenDeeper struct { 209 | Deepest string 210 | } 211 | } 212 | type Config struct { 213 | Host string 214 | SomeGrouping SomeGrouping 215 | } 216 | 217 | var config Config 218 | 219 | filler := flagsfiller.New() 220 | 221 | var flagset flag.FlagSet 222 | err := filler.Fill(&flagset, &config) 223 | require.NoError(t, err) 224 | 225 | err = flagset.Parse([]string{"--host", "h1", "--some-grouping-some-field", "val1"}) 226 | require.NoError(t, err) 227 | 228 | assert.Equal(t, "h1", config.Host) 229 | assert.Equal(t, "val1", config.SomeGrouping.SomeField) 230 | 231 | var buf bytes.Buffer 232 | flagset.SetOutput(&buf) 233 | flagset.PrintDefaults() 234 | 235 | assert.Equal(t, ` -host string 236 | 237 | -some-grouping-even-deeper-deepest string 238 | 239 | -some-grouping-some-field string 240 | 241 | `, buf.String()) 242 | } 243 | 244 | func TestNestedUnexportedFields(t *testing.T) { 245 | type Config struct { 246 | Host string 247 | hiddenField struct { 248 | SomeField string 249 | anotherField string 250 | } 251 | } 252 | 253 | var config Config 254 | 255 | filler := flagsfiller.New() 256 | 257 | var flagset flag.FlagSet 258 | err := filler.Fill(&flagset, &config) 259 | require.NoError(t, err) 260 | 261 | var buf bytes.Buffer 262 | flagset.SetOutput(&buf) 263 | flagset.PrintDefaults() 264 | 265 | assert.Equal(t, ` -host string 266 | 267 | `, buf.String()) 268 | } 269 | 270 | func TestNestedStructPtr(t *testing.T) { 271 | type Nested struct { 272 | SomeField string 273 | } 274 | type Config struct { 275 | Host string 276 | SomeGrouping *Nested 277 | } 278 | 279 | var config Config 280 | 281 | filler := flagsfiller.New() 282 | 283 | var flagset flag.FlagSet 284 | err := filler.Fill(&flagset, &config) 285 | require.NoError(t, err) 286 | 287 | err = flagset.Parse([]string{"--host", "h1", "--some-grouping-some-field", "val1"}) 288 | require.NoError(t, err) 289 | 290 | assert.Equal(t, "h1", config.Host) 291 | assert.Equal(t, "val1", config.SomeGrouping.SomeField) 292 | } 293 | 294 | func TestNestedUnexportedStructPtr(t *testing.T) { 295 | type Nested struct { 296 | SomeField string 297 | } 298 | type Config struct { 299 | Host string 300 | hiddenField *Nested 301 | } 302 | 303 | var config Config 304 | 305 | filler := flagsfiller.New() 306 | 307 | var flagset flag.FlagSet 308 | err := filler.Fill(&flagset, &config) 309 | require.NoError(t, err) 310 | 311 | var buf bytes.Buffer 312 | flagset.SetOutput(&buf) 313 | flagset.PrintDefaults() 314 | 315 | assert.Equal(t, ` -host string 316 | 317 | `, buf.String()) 318 | } 319 | 320 | func TestPtrField(t *testing.T) { 321 | type Config struct { 322 | // this should get ignored only inner struct pointers are supported 323 | Host *string 324 | } 325 | 326 | var config Config 327 | 328 | filler := flagsfiller.New() 329 | 330 | var flagset flag.FlagSet 331 | err := filler.Fill(&flagset, &config) 332 | require.NoError(t, err) 333 | 334 | var buf bytes.Buffer 335 | flagset.SetOutput(&buf) 336 | flagset.PrintDefaults() 337 | 338 | // not in usage 339 | assert.Equal(t, "", buf.String()) 340 | } 341 | 342 | func TestDuration(t *testing.T) { 343 | type Config struct { 344 | Timeout time.Duration 345 | } 346 | 347 | var config Config 348 | 349 | filler := flagsfiller.New() 350 | 351 | var flagset flag.FlagSet 352 | err := filler.Fill(&flagset, &config) 353 | require.NoError(t, err) 354 | 355 | err = flagset.Parse([]string{"--timeout", "10s"}) 356 | require.NoError(t, err) 357 | 358 | assert.Equal(t, 10*time.Second, config.Timeout) 359 | } 360 | 361 | func TestNumbers(t *testing.T) { 362 | type Config struct { 363 | ValFloat64 float64 `default:"3.14"` 364 | ValInt64 int64 `default:"43"` 365 | ValInt int `default:"44"` 366 | ValUint64 uint64 `default:"45"` 367 | ValUint uint `default:"46"` 368 | } 369 | 370 | var config Config 371 | 372 | filler := flagsfiller.New() 373 | 374 | var flagset flag.FlagSet 375 | err := filler.Fill(&flagset, &config) 376 | require.NoError(t, err) 377 | 378 | buf := grabUsage(flagset) 379 | 380 | assert.Equal(t, ` 381 | -val-float-64 float 382 | (default 3.14) 383 | -val-int int 384 | (default 44) 385 | -val-int-64 int 386 | (default 43) 387 | -val-uint uint 388 | (default 46) 389 | -val-uint-64 uint 390 | (default 45) 391 | `, buf.String()) 392 | } 393 | 394 | func TestDefaultsViaLiteral(t *testing.T) { 395 | type Nested struct { 396 | Exported string 397 | unExported string 398 | } 399 | type Config struct { 400 | Host string 401 | Enabled bool 402 | Timeout time.Duration 403 | Nested *Nested 404 | } 405 | 406 | var config = Config{ 407 | Host: "h1", 408 | Enabled: true, 409 | Timeout: 5 * time.Second, 410 | Nested: &Nested{ 411 | Exported: "exported", 412 | unExported: "un-exported", 413 | }, 414 | } 415 | 416 | filler := flagsfiller.New() 417 | 418 | var flagset flag.FlagSet 419 | err := filler.Fill(&flagset, &config) 420 | require.NoError(t, err) 421 | 422 | buf := grabUsage(flagset) 423 | 424 | assert.Equal(t, "un-exported", config.Nested.unExported) 425 | 426 | assert.Equal(t, ` 427 | -enabled 428 | (default true) 429 | -host string 430 | (default "h1") 431 | -nested-exported string 432 | (default "exported") 433 | -timeout duration 434 | (default 5s) 435 | `, buf.String()) 436 | } 437 | 438 | func TestDefaultsViaTag(t *testing.T) { 439 | type Config struct { 440 | Host string `default:"h1"` 441 | Enabled bool `default:"true"` 442 | Timeout time.Duration `default:"5s"` 443 | } 444 | 445 | var config Config 446 | 447 | filler := flagsfiller.New() 448 | 449 | var flagset flag.FlagSet 450 | err := filler.Fill(&flagset, &config) 451 | require.NoError(t, err) 452 | 453 | buf := grabUsage(flagset) 454 | 455 | assert.Equal(t, ` 456 | -enabled 457 | (default true) 458 | -host string 459 | (default "h1") 460 | -timeout duration 461 | (default 5s) 462 | `, buf.String()) 463 | } 464 | 465 | func TestBadDefaultsViaTag(t *testing.T) { 466 | type BadBoolConfig struct { 467 | Enabled bool `default:"wrong"` 468 | } 469 | type BadDurationConfig struct { 470 | Timeout time.Duration `default:"wrong"` 471 | } 472 | 473 | tests := []struct { 474 | Name string 475 | Config interface{} 476 | }{ 477 | {Name: "bool", Config: &BadBoolConfig{}}, 478 | {Name: "duration", Config: &BadDurationConfig{}}, 479 | } 480 | 481 | for _, tt := range tests { 482 | t.Run(tt.Name, func(t *testing.T) { 483 | filler := flagsfiller.New() 484 | 485 | var flagset flag.FlagSet 486 | err := filler.Fill(&flagset, tt.Config) 487 | require.Error(t, err) 488 | }) 489 | } 490 | } 491 | 492 | func TestBadFieldErrorMessage(t *testing.T) { 493 | type BadBoolConfig struct { 494 | Enabled bool `default:"wrong"` 495 | } 496 | 497 | var config BadBoolConfig 498 | 499 | filler := flagsfiller.New() 500 | 501 | var flagset flag.FlagSet 502 | err := filler.Fill(&flagset, &config) 503 | require.Error(t, err) 504 | assert.Equal(t, "failed to process Enabled of flagsfiller_test.BadBoolConfig: failed to parse default into bool: strconv.ParseBool: parsing \"wrong\": invalid syntax", err.Error()) 505 | 506 | } 507 | 508 | func TestHiddenFields(t *testing.T) { 509 | type Config struct { 510 | hidden string 511 | } 512 | 513 | var config Config 514 | 515 | filler := flagsfiller.New() 516 | 517 | var flagset flag.FlagSet 518 | err := filler.Fill(&flagset, &config) 519 | require.NoError(t, err) 520 | 521 | var buf bytes.Buffer 522 | flagset.SetOutput(&buf) 523 | flagset.PrintDefaults() 524 | 525 | // not in usage 526 | assert.Equal(t, "", buf.String()) 527 | } 528 | 529 | func TestStringSlice(t *testing.T) { 530 | type Config struct { 531 | NoDefault []string 532 | InstanceDefault []string 533 | TagDefault []string `default:"one,two"` 534 | TagOverride []string `default:"one,two" override-value:"true"` 535 | } 536 | 537 | var config Config 538 | config.InstanceDefault = []string{"apple", "orange"} 539 | 540 | filler := flagsfiller.New() 541 | 542 | var flagset flag.FlagSet 543 | err := filler.Fill(&flagset, &config) 544 | require.NoError(t, err) 545 | 546 | buf := grabUsage(flagset) 547 | 548 | assert.Equal(t, ` 549 | -instance-default value 550 | (default apple,orange) 551 | -no-default value 552 | 553 | -tag-default value 554 | (default one,two) 555 | -tag-override value 556 | (default one,two) 557 | `, buf.String()) 558 | 559 | err = flagset.Parse([]string{ 560 | "--no-default", "nd1", 561 | "--no-default", "nd2", 562 | "--no-default", "nd3,nd4", 563 | "--no-default", "nd5\nnd6", 564 | "--tag-default", "three", 565 | "--tag-override", "three", 566 | }) 567 | require.NoError(t, err) 568 | 569 | assert.Equal(t, []string{"nd1", "nd2", "nd3", "nd4", "nd5", "nd6"}, config.NoDefault) 570 | assert.Equal(t, []string{"apple", "orange"}, config.InstanceDefault) 571 | assert.Equal(t, []string{"one", "two", "three"}, config.TagDefault) 572 | assert.Equal(t, []string{"three"}, config.TagOverride) 573 | } 574 | 575 | func TestStringSliceWithEmptyValuePattern(t *testing.T) { 576 | type Config struct { 577 | NoDefault []string 578 | TagDefault []string `default:"one,two"` 579 | } 580 | 581 | var config Config 582 | filler := flagsfiller.New(flagsfiller.WithValueSplitPattern("")) 583 | 584 | var flagset flag.FlagSet 585 | err := filler.Fill(&flagset, &config) 586 | require.NoError(t, err) 587 | 588 | err = flagset.Parse([]string{ 589 | "--no-default", "nd1,nd2", 590 | "--no-default", "nd3", 591 | }) 592 | require.NoError(t, err) 593 | 594 | assert.Equal(t, []string{"nd1,nd2", "nd3"}, config.NoDefault) 595 | assert.Equal(t, []string{"one,two"}, config.TagDefault) 596 | } 597 | 598 | func TestStringToStringMap(t *testing.T) { 599 | type Config struct { 600 | NoDefault map[string]string 601 | InstanceDefault map[string]string 602 | TagDefault map[string]string `default:"fruit=apple,veggie=carrot"` 603 | } 604 | 605 | var config Config 606 | config.InstanceDefault = map[string]string{"fruit": "orange"} 607 | 608 | filler := flagsfiller.New() 609 | 610 | var flagset flag.FlagSet 611 | err := filler.Fill(&flagset, &config) 612 | require.NoError(t, err) 613 | 614 | buf := grabUsage(flagset) 615 | 616 | // using regexp assertion since -tag-default's map entries can be either order 617 | assert.Regexp(t, ` 618 | -instance-default value 619 | \(default fruit=orange\) 620 | -no-default value 621 | 622 | -tag-default value 623 | \(default (veggie=carrot,fruit=apple|fruit=apple,veggie=carrot)\) 624 | `, buf.String()) 625 | 626 | err = flagset.Parse([]string{"--no-default", 627 | "k1=v1", 628 | "--no-default", 629 | "k2=v2,k3=v3\nk4=v4\n", 630 | }) 631 | require.NoError(t, err) 632 | 633 | assert.Equal(t, map[string]string{"k1": "v1", "k2": "v2", "k3": "v3", "k4": "v4"}, config.NoDefault) 634 | assert.Equal(t, map[string]string{"fruit": "apple", "veggie": "carrot"}, config.TagDefault) 635 | } 636 | 637 | func TestUsagePlaceholders(t *testing.T) { 638 | type Config struct { 639 | SomeUrl string `usage:"a [URL] to configure"` 640 | } 641 | 642 | var config Config 643 | 644 | filler := flagsfiller.New() 645 | 646 | var flagset flag.FlagSet 647 | err := filler.Fill(&flagset, &config) 648 | require.NoError(t, err) 649 | 650 | buf := grabUsage(flagset) 651 | 652 | assert.Equal(t, ` 653 | -some-url URL 654 | a URL to configure 655 | `, buf.String()) 656 | } 657 | 658 | func TestParse(t *testing.T) { 659 | type Config struct { 660 | Host string 661 | } 662 | 663 | var config Config 664 | os.Args = []string{"app", "--host", "host-a"} 665 | 666 | err := flagsfiller.Parse(&config) 667 | assert.NoError(t, err) 668 | 669 | require.Equal(t, "host-a", config.Host) 670 | } 671 | 672 | func TestParseError(t *testing.T) { 673 | type Config struct { 674 | BadDefault int `default:"not an int"` 675 | } 676 | 677 | var config Config 678 | os.Args = []string{"app", "--bad-default", "5"} 679 | 680 | err := flagsfiller.Parse(&config) 681 | assert.Error(t, err) 682 | } 683 | 684 | func TestIgnoreNonExportedFields(t *testing.T) { 685 | type Config struct { 686 | Host string 687 | hiddenField string 688 | } 689 | 690 | var config Config 691 | filler := flagsfiller.New() 692 | 693 | var flagset flag.FlagSet 694 | err := filler.Fill(&flagset, &config) 695 | require.NoError(t, err) 696 | 697 | buf := grabUsage(flagset) 698 | 699 | assert.Equal(t, ` 700 | -host string 701 | 702 | `, buf.String()) 703 | } 704 | 705 | func TestIgnoreNonExportedStructFields(t *testing.T) { 706 | type Config struct { 707 | Host string 708 | nested struct { 709 | NotVisible string 710 | } 711 | } 712 | 713 | var config Config 714 | filler := flagsfiller.New() 715 | 716 | var flagset flag.FlagSet 717 | err := filler.Fill(&flagset, &config) 718 | require.NoError(t, err) 719 | 720 | buf := grabUsage(flagset) 721 | 722 | assert.Equal(t, ` 723 | -host string 724 | 725 | `, buf.String()) 726 | } 727 | 728 | func TestWithEnv(t *testing.T) { 729 | type Config struct { 730 | Host string `default:"localhost" usage:"the host to use"` 731 | MultiWordName string 732 | } 733 | 734 | var config Config 735 | 736 | assert.NoError(t, os.Setenv("APP_HOST", "host from env")) 737 | assert.NoError(t, os.Setenv("APP_MULTI_WORD_NAME", "value from env")) 738 | 739 | filler := flagsfiller.New(flagsfiller.WithEnv("App")) 740 | 741 | var flagset flag.FlagSet 742 | err := filler.Fill(&flagset, &config) 743 | require.NoError(t, err) 744 | 745 | buf := grabUsage(flagset) 746 | 747 | assert.Equal(t, ` 748 | -host string 749 | the host to use (env APP_HOST) (default "localhost") 750 | -multi-word-name string 751 | (env APP_MULTI_WORD_NAME) 752 | `, buf.String()) 753 | 754 | err = flagset.Parse([]string{"--host", "host from args"}) 755 | require.NoError(t, err) 756 | 757 | assert.Equal(t, "host from args", config.Host) 758 | assert.Equal(t, "value from env", config.MultiWordName) 759 | } 760 | 761 | func TestWithEnvOverride(t *testing.T) { 762 | type Config struct { 763 | Host string `env:"SERVER_ADDRESS"` 764 | } 765 | 766 | var config Config 767 | 768 | filler := flagsfiller.New(flagsfiller.WithEnv("App")) 769 | 770 | var flagset flag.FlagSet 771 | err := filler.Fill(&flagset, &config) 772 | require.NoError(t, err) 773 | 774 | buf := grabUsage(flagset) 775 | 776 | assert.Equal(t, ` 777 | -host string 778 | (env SERVER_ADDRESS) 779 | `, buf.String()) 780 | } 781 | 782 | func TestWithEnvOverrideDisable(t *testing.T) { 783 | type Config struct { 784 | Host string `env:"" usage:"arg only"` 785 | } 786 | 787 | var config Config 788 | 789 | filler := flagsfiller.New(flagsfiller.WithEnv("App")) 790 | 791 | var flagset flag.FlagSet 792 | err := filler.Fill(&flagset, &config) 793 | require.NoError(t, err) 794 | 795 | buf := grabUsage(flagset) 796 | 797 | assert.Equal(t, ` 798 | -host string 799 | arg only 800 | `, buf.String()) 801 | } 802 | 803 | func TestNoSetFromEnv(t *testing.T) { 804 | type Config struct { 805 | Host string `usage:"arg only"` 806 | } 807 | 808 | var config Config 809 | 810 | assert.NoError(t, os.Setenv("APP_HOST", "host from env")) 811 | 812 | filler := flagsfiller.New( 813 | flagsfiller.WithEnv("App"), 814 | flagsfiller.NoSetFromEnv(), 815 | ) 816 | 817 | var flagset flag.FlagSet 818 | err := filler.Fill(&flagset, &config) 819 | require.NoError(t, err) 820 | 821 | buf := grabUsage(flagset) 822 | 823 | assert.Empty(t, config.Host) 824 | 825 | assert.Equal(t, ` 826 | -host string 827 | arg only (env APP_HOST) 828 | `, buf.String()) 829 | } 830 | 831 | func TestFlagNameOverride(t *testing.T) { 832 | type Config struct { 833 | Host string `flag:"server_address" usage:"address of server"` 834 | GetsIgnored string `flag:""` 835 | } 836 | 837 | var config Config 838 | 839 | filler := flagsfiller.New() 840 | 841 | var flagset flag.FlagSet 842 | err := filler.Fill(&flagset, &config) 843 | require.NoError(t, err) 844 | buf := grabUsage(flagset) 845 | 846 | assert.Equal(t, ` 847 | -server_address string 848 | address of server 849 | `, buf.String()) 850 | 851 | } 852 | 853 | func grabUsage(flagset flag.FlagSet) *bytes.Buffer { 854 | var buf bytes.Buffer 855 | buf.Write([]byte{'\n'}) 856 | // start with newline to make expected string nicer below 857 | flagset.SetOutput(&buf) 858 | flagset.PrintDefaults() 859 | return &buf 860 | } 861 | 862 | func ExampleWithEnv() { 863 | type Config struct { 864 | MultiWordName string 865 | } 866 | 867 | // simulate env variables from program invocation 868 | _ = os.Setenv("MY_APP_MULTI_WORD_NAME", "from env") 869 | 870 | var config Config 871 | 872 | // enable environment variable processing with given prefix 873 | filler := flagsfiller.New(flagsfiller.WithEnv("MyApp")) 874 | var flagset flag.FlagSet 875 | _ = filler.Fill(&flagset, &config) 876 | 877 | // simulate no args passed in 878 | _ = flagset.Parse([]string{}) 879 | 880 | fmt.Println(config.MultiWordName) 881 | // Output: 882 | // from env 883 | } 884 | -------------------------------------------------------------------------------- /general.go: -------------------------------------------------------------------------------- 1 | package flagsfiller 2 | 3 | /* 4 | The code in this file could be opened up in future if more complex implementation is needed 5 | */ 6 | 7 | import ( 8 | "flag" 9 | "fmt" 10 | "reflect" 11 | "strings" 12 | ) 13 | 14 | // this is a list of additional supported types(include struct), like time.Time, that walkFields() won't walk into, 15 | // the key is the is string returned by the getTypeName(), 16 | // each supported type need to be added in this map in init() 17 | var extendedTypes = make(map[string]handlerFunc) 18 | 19 | type handlerFunc func(tag reflect.StructTag, fieldRef interface{}, 20 | hasDefaultTag bool, tagDefault string, 21 | flagSet *flag.FlagSet, renamed string, 22 | usage string, aliases string) error 23 | 24 | type flagVal[T any] interface { 25 | flag.Value 26 | StrConverter(string) (T, error) 27 | SetRef(*T) 28 | } 29 | 30 | func processGeneral[T any](fieldRef interface{}, val flagVal[T], 31 | hasDefaultTag bool, tagDefault string, 32 | flagSet *flag.FlagSet, renamed string, 33 | usage string, aliases string) (err error) { 34 | casted := fieldRef.(*T) 35 | if hasDefaultTag { 36 | *casted, err = val.StrConverter(tagDefault) 37 | if err != nil { 38 | return fmt.Errorf("failed to parse default into %T: %w", *new(T), err) 39 | } 40 | } 41 | val.SetRef(casted) 42 | flagSet.Var(val, renamed, usage) 43 | if aliases != "" { 44 | for _, alias := range strings.Split(aliases, ",") { 45 | flagSet.Var(val, alias, usage) 46 | } 47 | } 48 | return nil 49 | 50 | } 51 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/itzg/go-flagsfiller 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/iancoleman/strcase v0.3.0 7 | github.com/stretchr/testify v1.10.0 8 | ) 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 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= 4 | github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 8 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 11 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 12 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 13 | -------------------------------------------------------------------------------- /net.go: -------------------------------------------------------------------------------- 1 | package flagsfiller 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "reflect" 7 | ) 8 | 9 | func init() { 10 | RegisterSimpleType(ipConverter) 11 | RegisterSimpleType(ipnetConverter) 12 | RegisterSimpleType(macConverter) 13 | } 14 | 15 | func ipConverter(s string, tag reflect.StructTag) (net.IP, error) { 16 | addr := net.ParseIP(s) 17 | if addr == nil { 18 | return nil, fmt.Errorf("%s is not a valid IP address", s) 19 | } 20 | return addr, nil 21 | } 22 | 23 | func ipnetConverter(s string, tag reflect.StructTag) (net.IPNet, error) { 24 | _, prefix, err := net.ParseCIDR(s) 25 | if err != nil { 26 | return net.IPNet{}, err 27 | } 28 | return *prefix, nil 29 | } 30 | 31 | func macConverter(s string, tag reflect.StructTag) (net.HardwareAddr, error) { 32 | return net.ParseMAC(s) 33 | } 34 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package flagsfiller 2 | 3 | import "github.com/iancoleman/strcase" 4 | 5 | // Renamer takes a field's name and returns the flag name to be used 6 | type Renamer func(name string) string 7 | 8 | // DefaultFieldRenamer is used when no WithFieldRenamer option is passed to the FlagSetFiller 9 | // constructor. 10 | var DefaultFieldRenamer = KebabRenamer() 11 | 12 | // FillerOption instances are passed to the FlagSetFiller constructor. 13 | type FillerOption func(opt *fillerOptions) 14 | 15 | type fillerOptions struct { 16 | fieldRenamer []Renamer 17 | envRenamer []Renamer 18 | noSetFromEnv bool 19 | valueSplitPattern string 20 | } 21 | 22 | // WithFieldRenamer declares an option to customize the Renamer used to convert field names 23 | // to flag names. 24 | func WithFieldRenamer(renamer Renamer) FillerOption { 25 | return func(opt *fillerOptions) { 26 | opt.fieldRenamer = append(opt.fieldRenamer, renamer) 27 | } 28 | } 29 | 30 | // WithEnv activates pre-setting the flag values from environment variables. 31 | // Fields are mapped to environment variables names by prepending the given prefix and 32 | // converting word-wise to SCREAMING_SNAKE_CASE. The given prefix can be empty. 33 | func WithEnv(prefix string) FillerOption { 34 | return WithEnvRenamer( 35 | CompositeRenamer(PrefixRenamer(prefix), ScreamingSnakeRenamer())) 36 | } 37 | 38 | // WithEnvRenamer activates pre-setting the flag values from environment variables where fields 39 | // are mapped to environment variable names by applying the given Renamer 40 | func WithEnvRenamer(renamer Renamer) FillerOption { 41 | return func(opt *fillerOptions) { 42 | opt.envRenamer = append(opt.envRenamer, renamer) 43 | } 44 | } 45 | 46 | // NoSetFromEnv ignores setting values from the environment. 47 | // All environment variable renamers are run but values are not set from the environment. 48 | // This is good to use if you need to build a flag set with default values that don't consider the current environment. 49 | func NoSetFromEnv() FillerOption { 50 | return func(opt *fillerOptions) { 51 | opt.noSetFromEnv = true 52 | } 53 | } 54 | 55 | // WithValueSplitPattern allows for changing the default value splitting regex pattern from newlines and commas. 56 | // Any empty string can be provided for pattern to disable value splitting. 57 | func WithValueSplitPattern(pattern string) FillerOption { 58 | return func(opt *fillerOptions) { 59 | opt.valueSplitPattern = pattern 60 | } 61 | } 62 | 63 | func (o *fillerOptions) renameLongName(name string) string { 64 | if len(o.fieldRenamer) == 0 { 65 | return DefaultFieldRenamer(name) 66 | } else { 67 | for _, renamer := range o.fieldRenamer { 68 | name = renamer(name) 69 | } 70 | return name 71 | } 72 | } 73 | 74 | func newFillerOptions(options ...FillerOption) *fillerOptions { 75 | v := &fillerOptions{ 76 | valueSplitPattern: "[\n,]", 77 | } 78 | for _, opt := range options { 79 | opt(v) 80 | } 81 | return v 82 | } 83 | 84 | // PrefixRenamer prepends the given prefix to a name 85 | func PrefixRenamer(prefix string) Renamer { 86 | return func(name string) string { 87 | return prefix + name 88 | } 89 | } 90 | 91 | // KebabRenamer converts a given name into kebab-case 92 | func KebabRenamer() Renamer { 93 | return strcase.ToKebab 94 | } 95 | 96 | // ScreamingSnakeRenamer converts a given name into SCREAMING_SNAKE_CASE 97 | func ScreamingSnakeRenamer() Renamer { 98 | return strcase.ToScreamingSnake 99 | } 100 | 101 | // CompositeRenamer applies all of the given Renamers to a name 102 | func CompositeRenamer(renamers ...Renamer) Renamer { 103 | return func(name string) string { 104 | for _, r := range renamers { 105 | name = r(name) 106 | } 107 | return name 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package flagsfiller_test 2 | 3 | import ( 4 | "fmt" 5 | "github.com/itzg/go-flagsfiller" 6 | ) 7 | 8 | func ExampleCompositeRenamer() { 9 | renamer := flagsfiller.CompositeRenamer( 10 | flagsfiller.PrefixRenamer("App"), 11 | flagsfiller.ScreamingSnakeRenamer()) 12 | 13 | renamed := renamer("SomeFieldName") 14 | fmt.Println(renamed) 15 | // Output: 16 | // APP_SOME_FIELD_NAME 17 | } 18 | -------------------------------------------------------------------------------- /simple.go: -------------------------------------------------------------------------------- 1 | package flagsfiller 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "reflect" 7 | ) 8 | 9 | // RegisterSimpleType register a new type, 10 | // should be called in init(), 11 | // see time.go and net.go for implementation examples 12 | func RegisterSimpleType[T any](c ConvertFunc[T]) { 13 | base := simpleType[T]{converter: c} 14 | extendedTypes[getTypeName(reflect.TypeOf(*new(T)))] = base.Process 15 | } 16 | 17 | // ConvertFunc is a function convert string s into a specific type T, the tag is the struct field tag, as addtional input. 18 | // see time.go and net.go for implementation examples 19 | type ConvertFunc[T any] func(s string, tag reflect.StructTag) (T, error) 20 | 21 | type simpleType[T any] struct { 22 | val *T 23 | tags reflect.StructTag 24 | converter ConvertFunc[T] 25 | } 26 | 27 | func newSimpleType[T any](c ConvertFunc[T], tag reflect.StructTag) simpleType[T] { 28 | return simpleType[T]{val: new(T), converter: c, tags: tag} 29 | } 30 | 31 | func (v *simpleType[T]) String() string { 32 | if v.val == nil { 33 | return fmt.Sprint(nil) 34 | } 35 | return fmt.Sprint(v.val) 36 | } 37 | 38 | func (v *simpleType[T]) StrConverter(s string) (T, error) { 39 | return v.converter(s, v.tags) 40 | } 41 | 42 | func (v *simpleType[T]) Set(s string) error { 43 | var err error 44 | *v.val, err = v.converter(s, v.tags) 45 | if err != nil { 46 | return fmt.Errorf("failed to parse %s into %T, %w", s, *(new(T)), err) 47 | } 48 | return nil 49 | } 50 | 51 | func (v *simpleType[T]) SetRef(t *T) { 52 | v.val = t 53 | } 54 | 55 | func (v *simpleType[T]) Process(tag reflect.StructTag, fieldRef interface{}, 56 | hasDefaultTag bool, tagDefault string, 57 | flagSet *flag.FlagSet, renamed string, 58 | usage string, aliases string) error { 59 | val := newSimpleType(v.converter, tag) 60 | return processGeneral[T](fieldRef, &val, hasDefaultTag, tagDefault, flagSet, renamed, usage, aliases) 61 | } 62 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package flagsfiller 2 | 3 | import ( 4 | "reflect" 5 | "time" 6 | ) 7 | 8 | func init() { 9 | RegisterSimpleType(timeConverter) 10 | } 11 | 12 | // DefaultTimeLayout is the default layout string to parse time, following golang time.Parse() format, 13 | // can be overridden per field by field tag "layout". Default value is "2006-01-02 15:04:05", which is 14 | // the same as time.DateTime in Go 1.20 15 | var DefaultTimeLayout = "2006-01-02 15:04:05" 16 | 17 | func timeConverter(s string, tag reflect.StructTag) (time.Time, error) { 18 | layout, _ := tag.Lookup("layout") 19 | if layout == "" { 20 | layout = DefaultTimeLayout 21 | } 22 | return time.Parse(layout, s) 23 | } 24 | -------------------------------------------------------------------------------- /txtunmarshaler.go: -------------------------------------------------------------------------------- 1 | // This file implements support for all types that support interface encoding.TextUnmarshaler 2 | package flagsfiller 3 | 4 | import ( 5 | "encoding" 6 | "flag" 7 | "fmt" 8 | "reflect" 9 | "strings" 10 | ) 11 | 12 | // RegisterTextUnmarshaler use is optional, since flagsfiller will automatically register the types implement encoding.TextUnmarshaler it encounters 13 | func RegisterTextUnmarshaler(in any) { 14 | base := textUnmarshalerType{} 15 | extendedTypes[getTypeName(reflect.TypeOf(in).Elem())] = base.process 16 | } 17 | 18 | type textUnmarshalerType struct { 19 | val encoding.TextUnmarshaler 20 | } 21 | 22 | // String implements flag.Value interface 23 | func (tv *textUnmarshalerType) String() string { 24 | if tv.val == nil { 25 | return fmt.Sprint(nil) 26 | } 27 | return fmt.Sprint(tv.val) 28 | } 29 | 30 | // Set implements flag.Value interface 31 | func (tv *textUnmarshalerType) Set(s string) error { 32 | return tv.val.UnmarshalText([]byte(s)) 33 | } 34 | 35 | func (tv *textUnmarshalerType) process(tag reflect.StructTag, fieldRef interface{}, 36 | hasDefaultTag bool, tagDefault string, 37 | flagSet *flag.FlagSet, renamed string, 38 | usage string, aliases string) error { 39 | v, ok := fieldRef.(encoding.TextUnmarshaler) 40 | if !ok { 41 | return fmt.Errorf("can't cast %v into encoding.TextUnmarshaler", fieldRef) 42 | } 43 | newval := textUnmarshalerType{ 44 | val: v, 45 | } 46 | if hasDefaultTag { 47 | err := newval.Set(tagDefault) 48 | if err != nil { 49 | return fmt.Errorf("failed to parse default value into %v: %w", reflect.TypeOf(fieldRef), err) 50 | } 51 | } 52 | flagSet.Var(&newval, renamed, usage) 53 | if aliases != "" { 54 | for _, alias := range strings.Split(aliases, ",") { 55 | flagSet.Var(&newval, alias, usage) 56 | } 57 | } 58 | return nil 59 | 60 | } 61 | --------------------------------------------------------------------------------