diff --git a/README.md b/README.md index 461d712..2a1642c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ # configstruct -Simple Go module to parse a configuration from environment values and CLI flags using struct tags. +Simple Go module to parse a configuration from environment values and CLI flags or arguments using struct tags. Starting with v1.3.0 there there is also support for CLI commands and subcommands ## Usage without commands ```Go // define a struct with tags for env name, cli flag and usage type Config struct { + Filename string `arg:"1" name:"filename" required:"true"` Hostname string `env:"CONFIGSTRUCT_HOSTNAME" cli:"hostname" usage:"hostname value"` Port int `env:"CONFIGSTRUCT_PORT" cli:"port" usage:"listen port"` Debug bool `env:"CONFIGSTRUCT_DEBUG" cli:"debug" usage:"debug mode"` @@ -18,7 +19,10 @@ conf := Config{ Debug: true, } -// now parse values from first env and then cli into this var +// imagine the programm is called like this: +// ./myprogram -hostname=myhost -port=9000 testfile +// the flag values (hostname, port) and argument (filename) are parsed into the struct +// all pre-set defaults are overwritten if a value is provided otherwise it is left as is err := configstruct.Parse(&conf) if err != nil {...} @@ -33,13 +37,18 @@ if err != nil {...} // after parsing you can pass through you config struct and access values port := conf.Port host := conf.Hostname +filename := conf.Filename if conf.Debug {...} + +// cli arguments are also possible + ``` ## Usage with commands +You can also define "commands" that can be used to execute callback functions. The program with global flags and a command `count` should be called like this: ````bash -mycmd -hostname localhost count -number 2 +mycmd -hostname=localhost count -number=2 ```` @@ -112,4 +121,4 @@ cmd := NewCommand("", &rootCfg, func(c *configstruct.Command, cfg interface{}) e }, subCmd) -``` \ No newline at end of file +``` diff --git a/configstruct.go b/configstruct.go index e76747a..3509d36 100644 --- a/configstruct.go +++ b/configstruct.go @@ -34,6 +34,36 @@ func ParseWithFlagSet(flagSet *flag.FlagSet, cliArgs []string, c interface{}, op valueRef := reflect.ValueOf(c) confType := valueRef.Elem().Type() + // parse arguments + parseArgs := func() error { + // iterate over struct fields for arg flags + for i := 0; i < confType.NumField(); i++ { + field := confType.Field(i) + value := valueRef.Elem().Field(i) + name := field.Tag.Get("name") + + required := field.Tag.Get("required") == "true" + arg, err := strconv.Atoi(field.Tag.Get("arg")) + if err != nil { + arg = -1 + } + + if arg > 0 { + argVal := flagSet.Arg(arg - 1) + if required && argVal == "" { + flagSet.Usage() + return fmt.Errorf("argument %s is required", name) + } + + if required { + value.Set(reflect.ValueOf(argVal)) + } + } + } + + return nil + } + // parse cli flags parseCli := func() error { // iterate over struct fields for cli flags @@ -55,7 +85,7 @@ func ParseWithFlagSet(flagSet *flag.FlagSet, cliArgs []string, c interface{}, op case reflect.Float64: flagSet.Float64Var(valueRef.Elem().FieldByName(field.Name).Addr().Interface().(*float64), name, value.Float(), usage) default: - return fmt.Errorf("config cli type %s not implemented", field.Type.Name()) + return fmt.Errorf("config cli type %s not implemented", field.Type.Kind()) } return nil @@ -123,6 +153,10 @@ func ParseWithFlagSet(flagSet *flag.FlagSet, cliArgs []string, c interface{}, op if err != nil { return err } + err = parseArgs() + if err != nil { + return err + } return nil } @@ -135,6 +169,10 @@ func ParseWithFlagSet(flagSet *flag.FlagSet, cliArgs []string, c interface{}, op if err != nil { return err } + err = parseArgs() + if err != nil { + return err + } return nil } diff --git a/configstruct_test.go b/configstruct_test.go index 41d442b..47a6796 100644 --- a/configstruct_test.go +++ b/configstruct_test.go @@ -3,9 +3,10 @@ package configstruct import ( "flag" "fmt" - "github.com/stretchr/testify/assert" "os" "testing" + + "github.com/stretchr/testify/assert" ) type testConfig struct { @@ -120,6 +121,71 @@ func TestParse(t *testing.T) { assert.Error(t, err) }) + t.Run("one required argument", func(t *testing.T) { + cliArgs := []string{"command", "-hostname=localhost", "-port=8080", "start"} + flagSet := flag.NewFlagSet(cliArgs[0], flag.ExitOnError) + + conf := struct { + Hostname string `env:"CONFIGSTRUCT_HOSTNAME" cli:"hostname" usage:"hostname value"` + Port int `env:"CONFIGSTRUCT_PORT" cli:"port" usage:"listen port"` + Command string `arg:"1" name:"command" required:"true"` + }{} + + err := ParseWithFlagSet(flagSet, cliArgs, &conf) + assert.NoError(t, err) + assert.Equal(t, "start", conf.Command) + }) + + t.Run("two arguments", func(t *testing.T) { + cliArgs := []string{"command", "-hostname=localhost", "-port=8080", "start", "myfile"} + flagSet := flag.NewFlagSet(cliArgs[0], flag.ExitOnError) + + conf := struct { + Hostname string `env:"CONFIGSTRUCT_HOSTNAME" cli:"hostname" usage:"hostname value"` + Port int `env:"CONFIGSTRUCT_PORT" cli:"port" usage:"listen port"` + Command string `arg:"1" name:"command" required:"true"` + Filename string `arg:"2" name:"filename" required:"true"` + }{} + + err := ParseWithFlagSet(flagSet, cliArgs, &conf) + assert.NoError(t, err) + assert.Equal(t, "start", conf.Command) + assert.Equal(t, "myfile", conf.Filename) + }) + + t.Run("arguments with defaults", func(t *testing.T) { + cliArgs := []string{"command", "-hostname=localhost", "-port=8080", "start"} + flagSet := flag.NewFlagSet(cliArgs[0], flag.ExitOnError) + + conf := struct { + Hostname string `env:"CONFIGSTRUCT_HOSTNAME" cli:"hostname" usage:"hostname value"` + Port int `env:"CONFIGSTRUCT_PORT" cli:"port" usage:"listen port"` + Command string `arg:"1" name:"command" required:"true"` + Filename string `arg:"2" name:"filename"` + }{ + Filename: "myfile", + } + + err := ParseWithFlagSet(flagSet, cliArgs, &conf) + assert.NoError(t, err) + assert.Equal(t, "start", conf.Command) + assert.Equal(t, "myfile", conf.Filename) + }) + + t.Run("required argument missing", func(t *testing.T) { + cliArgs := []string{"command", "-hostname=localhost", "-port=8080"} + flagSet := flag.NewFlagSet(cliArgs[0], flag.ExitOnError) + + conf := struct { + Hostname string `env:"CONFIGSTRUCT_HOSTNAME" cli:"hostname" usage:"hostname value"` + Port int `env:"CONFIGSTRUCT_PORT" cli:"port" usage:"listen port"` + Command string `arg:"1" name:"command" required:"true"` + }{} + + err := ParseWithFlagSet(flagSet, cliArgs, &conf) + assert.Error(t, err) + }) + } // Example for using `configstruct` with default values.