Skip to content

Commit

Permalink
Initial support for CLI commands
Browse files Browse the repository at this point in the history
  • Loading branch information
pteich committed Nov 28, 2020
1 parent 2110572 commit bd3e71f
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 3 deletions.
49 changes: 46 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# configstruct
Simple Go module to parse a configuration from environment and cli flags using struct tags.
Simple Go module to parse a configuration from environment values and CLI flags using struct tags.
Starting with v1.3.0 there there is also support for CLI commands and subcommands

Usage
## Usage without commands
```Go
// define a struct with tags for env name, cli flag and usage
type Config struct {
Expand Down Expand Up @@ -33,7 +34,49 @@ if err != nil {...}
port := conf.Port
host := conf.Hostname
if conf.Debug {...}
```

## Usage with commands
The program with global flags and a command `count` should be called like this:
````bash
mycmd -hostname localhost count -number 2

````

```
This is the code to model this behaviour:

```Go
// define a struct with tags for env name, cli flag and usage
type RootConfig struct {
Hostname string `env:"CONFIGSTRUCT_HOSTNAME" cli:"hostname" usage:"hostname value"`
Debug bool `env:"CONFIGSTRUCT_DEBUG" cli:"debug" usage:"debug mode"`
}

type CountConfig struct {
Number int `cli:"number" usage:"number to count"`
}

// create a variable of the struct type and define defaults if needed
rootCfg := RootConfig{
Hostname: "localhost",
Debug: true,
}

countCfg := CountConfig {
Number: 1
}

countCmd := NewCommand("count", &subConfig, func(cfg interface{}) error {
cfgValues := cfg.(*CountConfig)
...
return nil
})

cmd := NewCommand("", &rootCfg, func(cfg interface{}) error {
cfgValues := cfg.(*RootConfig)
...
return nil
}, subCmd)

err := cmd.ParseAndRun(os.Args)
```
56 changes: 56 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package configstruct

import (
"flag"
"strings"
)

// Command defines a command that consists of a name (empty for root command), a struct that models all
// flags, a function that is executed if the command matches and that gets the config struct as argument
// several sub-commands can be added
type Command struct {
fs *flag.FlagSet
config interface{}
f func(cfg interface{}) error
subCommands []*Command
}

// NewCommand creates a command that is triggered by the given name in the command line
// all flags are defined by a struct that is parsed and filled with real values
// this struct is then set as argument for the function that is executed if the name matches
func NewCommand(name string, config interface{}, f func(cfg interface{}) error, subCommands ...*Command) *Command {
fs := flag.NewFlagSet(name, flag.ExitOnError)

return &Command{
fs: fs,
config: config,
f: f,
subCommands: subCommands,
}
}

// ParseAndRun parses the given arguments and executes command functions
func (c *Command) ParseAndRun(args []string, opts ...Option) error {
err := ParseWithFlagSet(c.fs, args, c.config, opts...)
if err != nil {
return err
}

if c.fs.Name() == "" || strings.EqualFold(c.fs.Name(), args[0]) {
err := c.f(c.config)
if err != nil {
return err
}
}

args = c.fs.Args()
if len(args) > 0 {
for i := range c.subCommands {
if strings.EqualFold(c.subCommands[i].fs.Name(), args[0]) {
c.subCommands[i].ParseAndRun(args)
}
}
}

return nil
}
40 changes: 40 additions & 0 deletions commands_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package configstruct

import (
"testing"
)

type rootCmdConfig struct {
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"`
FloatValue float64 `env:"CONFIGSTRUCT_FLOAT" cli:"floatValue" usage:"float value"`
}

type subCmdConfig struct {
Number int `cli:"number" usage:"number to count"`
}

func TestCommand_ParseAndRun(t *testing.T) {
args := []string{"cliName", "-hostname", "localhost", "count", "-number", "2"}

var rootConfig rootCmdConfig
var subConfig subCmdConfig

subCmd := NewCommand("count", &subConfig, func(cfg interface{}) error {
cfgValues := cfg.(*subCmdConfig)
t.Log("sub command", cfgValues.Number)
return nil
})

cmd := NewCommand("", &rootConfig, func(cfg interface{}) error {
cfgValues := cfg.(*rootCmdConfig)
t.Log("root command", cfgValues.Hostname)
return nil
}, subCmd)

err := cmd.ParseAndRun(args)
if err != nil {
t.Errorf("error should be nil but is %v", err)
}
}

0 comments on commit bd3e71f

Please sign in to comment.