Skip to content

Commit

Permalink
Support for parsing arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
pteich committed Dec 18, 2021
1 parent 981a3d7 commit fbc4d3b
Show file tree
Hide file tree
Showing 3 changed files with 119 additions and 6 deletions.
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -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"`
Expand All @@ -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 {...}

Expand All @@ -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

````

Expand Down Expand Up @@ -112,4 +121,4 @@ cmd := NewCommand("", &rootCfg, func(c *configstruct.Command, cfg interface{}) e
}, subCmd)


```
```
40 changes: 39 additions & 1 deletion configstruct.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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
}
68 changes: 67 additions & 1 deletion configstruct_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package configstruct
import (
"flag"
"fmt"
"github.com/stretchr/testify/assert"
"os"
"testing"

"github.com/stretchr/testify/assert"
)

type testConfig struct {
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit fbc4d3b

Please sign in to comment.