Bring your own struct and make Go's flag package pleasant to use.
go get github.com/itzg/go-flagsfiller
import "github.com/itzg/go-flagsfiller"- Populates Go's flag.FlagSet from a struct of your choosing
- By default, field names are converted to flag names using kebab-case, but can be configured.
- Use nested structs where flag name is prefixed by the nesting struct field names
- Allows defaults to be given via struct tag
default - Falls back to using instance field values as declared default
- Declare flag usage via struct tag
usage - Mark flags as required via struct tag
requiredand validate withVerify()method (cannot be combined withdefaulttag) - Can be combined with other modules, such as google/subcommands for sub-command processing. Can also be integrated with spf13/cobra by using pflag's AddGoFlagSet
- Beyond the standard types supported by flag.FlagSet also includes support for:
[]stringwhere 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,threemap[string]stringwhere each entry is akey=valueand/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=v3time.Timeparse via time.Parse(), with taglayoutspecify the layout string, default is "2006-01-02 15:04:05"net.IPparse via net.ParseIP()net.IPNetparse via net.ParseCIDR()net.HardwareAddrparse via net.ParseMAC()- and all types that implement encoding.TextUnmarshaler interface
- Optionally set flag values from environment variables. Similar to flag names, environment variable names are derived automatically from the field names
- New types could be supported via user code, via
RegisterSimpleType(ConvertFunc), check time.go and net.go to see how it works- note: in case of a registered type also implements encoding.TextUnmarshaler, then registered type's ConvertFunc is preferred
package main
import (
"flag"
"fmt"
"github.com/itzg/go-flagsfiller"
"log"
"time"
)
type Config struct {
Host string `default:"localhost" usage:"The remote host"`
DebugEnabled bool `default:"true" usage:"Show debugs"`
MaxTimeout time.Duration `default:"5s" usage:"How long to wait"`
Feature struct {
Faster bool `usage:"Go faster"`
LudicrousSpeed bool `usage:"Go even faster"`
}
}
func main() {
var config Config
// create a FlagSetFiller
filler := flagsfiller.New()
// fill and map struct fields to flags
err := filler.Fill(flag.CommandLine, &config)
if err != nil {
log.Fatal(err)
}
// parse command-line like usual
flag.Parse()
fmt.Printf("Loaded: %+v\n", config)
}The following shows an example of the usage provided when passing --help:
-debug-enabled
Show debugs (default true)
-feature-faster
Go faster
-feature-ludicrous-speed
Go even faster
-host string
The remote host (default "localhost")
-max-timeout duration
How long to wait (default 5s)
By default, Fill() will return an error if an environment variable is provided but cannot be parsed into the field's type. This can be problematic if you want to show usage/help even when an environment variable is invalid.
You can use the IgnoreEnvErrors() option to suppress these errors:
filler := flagsfiller.New(flagsfiller.IgnoreEnvErrors())
err := filler.Fill(flag.CommandLine, &config)Flags can be marked as required using the required:"true" struct tag. After parsing command-line arguments, call the Verify() method to ensure all required flags have been provided:
type Config struct {
Host string `required:"true" usage:"The remote host"`
Port int `default:"8080" usage:"The port"`
Username string `required:"true" usage:"Username for authentication"`
}
var config Config
filler := flagsfiller.New()
err := filler.Fill(flag.CommandLine, &config)
if err != nil {
log.Fatal(err)
}
flag.Parse()
// Verify all required fields are set
err = filler.Verify()
if err != nil {
log.Fatal(err) // Will fail if Host or Username not provided
}Note: A field cannot be both required and have a default value. Attempting to use both tags will result in an error during Fill().
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, and parses the command line:
func main() {
var serverConfig server.Config
filler := flagsfiller.New()
err := filler.Fill(flag.CommandLine, &serverConfig)
if err != nil {
log.Fatal(err)
}
envy.Parse("SAML_PROXY")
flag.Parse()where server.Config is declared as
type Config struct {
Version bool `usage:"show version and exit"`
Bind string `default:":8080" usage:"host:port to bind for serving HTTP"`
BaseUrl string `usage:"External URL of this proxy"`
BackendUrl string `usage:"URL of the backend being proxied"`
IdpMetadataUrl string `usage:"URL of the IdP's metadata XML"`
IdpCaPath string `usage:"Optional path to a CA certificate PEM file for the IdP"`
// ...see https://github.com/itzg/saml-auth-proxy/blob/master/server/server.go for full set
}Flagsfiller can be used in combination with google/subcommands to fill both global command-line flags and subcommand flags.
For the global flags, it is best to declare a struct type, such as
type GlobalConfig struct {
Debug bool `usage:"enable debug logging"`
}Prior to calling Execute on the subcommands' Commander, fill and parse the global flags like normal:
func main() {
//... register subcommands here
var globalConfig GlobalConfig
err := flagsfiller.Parse(&globalConfig)
if err != nil {
log.Fatal(err)
}
//... execute subcommands but pass global config
os.Exit(int(subcommands.Execute(context.Background(), &globalConfig)))
}Each of your subcommand struct types should contain the flag fields to fill and parse, such as:
type connectCmd struct {
Host string `usage:"the hostname of the server" env:"GITHUB_TOKEN"`
Port int `usage:"the port of the server" default:"8080"`
}Your implementation of SetFlags will use flagsfiller to fill the definition of the subcommand's flagset, such as:
func (c *connectCmd) SetFlags(f *flag.FlagSet) {
filler := flagsfiller.New()
err := filler.Fill(f, c)
if err != nil {
log.Fatal(err)
}
}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:
func (c *loadFromGitCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
globalConfig := args[0].(*GlobalConfig)
if globalConfig.Debug {
//... enable debug logs
}
// ...operate on subcommand flags, such as
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.Host, c.Port))
}Refer to the GoDocs for more information about this module.