commit 0c78105a26af5663b6bb2c5be1fed4ed7d81d687 Author: Knut Ahlers Date: Sun Jul 12 11:51:19 2015 +0200 Initial version of rconfig diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4fde5d2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,13 @@ +Copyright 2015 Knut Ahlers + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cebe16a --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +[![Circle CI](https://circleci.com/gh/Luzifer/rconfig.svg?style=svg)](https://circleci.com/gh/Luzifer/rconfig) +[![License: Apache v2.0](https://badge.luzifer.io/v1/badge?color=5d79b5&title=license&text=Apache+v2.0)](http://www.apache.org/licenses/LICENSE-2.0) +[![Documentation](https://badge.luzifer.io/v1/badge?title=godoc&text=reference)](https://godoc.org/github.com/Luzifer/rconfig) + +## Description + +> Package rconfig implements a CLI configuration reader with struct-embedded defaults, environment variables and posix compatible flag parsing using the [pflag](https://github.com/spf13/pflag) library. + +## Installation + +Install by running: + +``` +go get -u github.com/Luzifer/rconfig +``` + +Run tests by running: + +``` +go test -v -race -cover github.com/Luzifer/rconfig +``` + +## Usage + +As a first step define a struct holding your configuration: + +```go +type config struct { + Username string `default:"unknown" flag:"user" description:"Your name"` + Details struct { + Age int `default:"25" flag:"age" env:"age" description:"Your age"` + } +} +``` + +Next create an instance of that struct and let `rconfig` fill that config: + +```go +var cfg config +func init() { + cfg = config{} + rconfig.Parse(&cfg) +} +``` + +You're ready to access your configuration: + +```go +func main() { + fmt.Printf("Hello %s, happy birthday for your %dth birthday.", + cfg.Username, + cfg.Details.Age) +} +``` + +## More info + +You can see the full reference documentation of the rconfig package [at godoc.org](https://godoc.org/github.com/Luzifer/rconfig), or through go's standard documentation system by running `godoc -http=:6060` and browsing to [http://localhost:6060/pkg/github.com/Luzifer/rconfig](http://localhost:6060/pkg/github.com/Luzifer/rconfig) after installation. diff --git a/config.go b/config.go new file mode 100644 index 0000000..ceaebed --- /dev/null +++ b/config.go @@ -0,0 +1,282 @@ +// Package rconfig implements a CLI configuration reader with struct-embedded +// defaults, environment variables and posix compatible flag parsing using +// the pflag library. +package rconfig //import "github.com/Luzifer/rconfig" + +import ( + "errors" + "fmt" + "os" + "reflect" + "strconv" + "strings" + + "github.com/spf13/pflag" +) + +var fs *pflag.FlagSet + +// Parse takes the pointer to a struct filled with variables which should be read +// from ENV, default or flag. The precedence in this is flag > ENV > default. So +// if a flag is specified on the CLI it will overwrite the ENV and otherwise ENV +// overwrites the default specified. +// +// For your configuration struct you can use the following struct-tags to control +// the behavior of rconfig: +// +// default: Set a default value +// env: Read the value from this environment variable +// flag: Flag to read in format "long,short" (for example "listen,l") +// description: A help text for Usage output to guide your users +// +// The format you need to specify those values you can see in the example to this +// function. +// +func Parse(config interface{}) error { + return parse(config, nil) +} + +// Usage prints a basic usage with the corresponding defaults for the flags to +// os.Stdout. The defaults are derived from the `default` struct-tag and the ENV. +func Usage() { + if fs != nil && fs.Parsed() { + fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) + fs.PrintDefaults() + } +} + +func parse(in interface{}, args []string) error { + if args == nil { + args = os.Args + } + + fs = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError) + if err := execTags(in, fs); err != nil { + return err + } + + return fs.Parse(args) +} + +func execTags(in interface{}, fs *pflag.FlagSet) error { + if reflect.TypeOf(in).Kind() != reflect.Ptr { + return errors.New("Calling parser with non-pointer") + } + + if reflect.ValueOf(in).Elem().Kind() != reflect.Struct { + return errors.New("Calling parser with pointer to non-struct") + } + + st := reflect.ValueOf(in).Elem() + for i := 0; i < st.NumField(); i++ { + valField := st.Field(i) + typeField := st.Type().Field(i) + + if typeField.Tag.Get("default") == "" && typeField.Tag.Get("env") == "" && typeField.Tag.Get("flag") == "" && typeField.Type.Kind() != reflect.Struct { + // None of our supported tags is present and it's not a sub-struct + continue + } + + value := envDefault(typeField.Tag.Get("env"), typeField.Tag.Get("default")) + parts := strings.Split(typeField.Tag.Get("flag"), ",") + + switch typeField.Type.Kind() { + case reflect.String: + if typeField.Tag.Get("flag") != "" { + if len(parts) == 1 { + fs.StringVar(valField.Addr().Interface().(*string), parts[0], value, typeField.Tag.Get("description")) + } else { + fs.StringVarP(valField.Addr().Interface().(*string), parts[0], parts[1], value, typeField.Tag.Get("description")) + } + } else { + valField.SetString(value) + } + + case reflect.Bool: + v := value == "true" + if typeField.Tag.Get("flag") != "" { + if len(parts) == 1 { + fs.BoolVar(valField.Addr().Interface().(*bool), parts[0], v, typeField.Tag.Get("description")) + } else { + fs.BoolVarP(valField.Addr().Interface().(*bool), parts[0], parts[1], v, typeField.Tag.Get("description")) + } + } else { + valField.SetBool(v) + } + + case reflect.Int, reflect.Int8, reflect.Int32, reflect.Int64: + vt, err := strconv.ParseInt(value, 10, 64) + if err != nil { + if value == "" { + vt = 0 + } else { + return err + } + } + if typeField.Tag.Get("flag") != "" { + registerFlagInt(typeField.Type.Kind(), fs, valField.Addr().Interface(), parts, vt, typeField.Tag.Get("description")) + } else { + valField.SetInt(vt) + } + + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + vt, err := strconv.ParseUint(value, 10, 64) + if err != nil { + if value == "" { + vt = 0 + } else { + return err + } + } + if typeField.Tag.Get("flag") != "" { + registerFlagUint(typeField.Type.Kind(), fs, valField.Addr().Interface(), parts, vt, typeField.Tag.Get("description")) + } else { + valField.SetUint(vt) + } + + case reflect.Float32, reflect.Float64: + vt, err := strconv.ParseFloat(value, 64) + if err != nil { + if value == "" { + vt = 0.0 + } else { + return err + } + } + if typeField.Tag.Get("flag") != "" { + registerFlagFloat(typeField.Type.Kind(), fs, valField.Addr().Interface(), parts, vt, typeField.Tag.Get("description")) + } else { + valField.SetFloat(vt) + } + + case reflect.Struct: + if err := execTags(valField.Addr().Interface(), fs); err != nil { + return err + } + + case reflect.Slice: + switch typeField.Type.Elem().Kind() { + case reflect.Int: + def := []int{} + for _, v := range strings.Split(value, ",") { + it, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64) + if err != nil { + return err + } + def = append(def, int(it)) + } + if len(parts) == 1 { + fs.IntSliceVar(valField.Addr().Interface().(*[]int), parts[0], def, typeField.Tag.Get("description")) + } else { + fs.IntSliceVarP(valField.Addr().Interface().(*[]int), parts[0], parts[1], def, typeField.Tag.Get("description")) + } + case reflect.String: + del := typeField.Tag.Get("delimiter") + if len(del) == 0 { + del = "," + } + def := strings.Split(value, del) + if len(parts) == 1 { + fs.StringSliceVar(valField.Addr().Interface().(*[]string), parts[0], def, typeField.Tag.Get("description")) + } else { + fs.StringSliceVarP(valField.Addr().Interface().(*[]string), parts[0], parts[1], def, typeField.Tag.Get("description")) + } + } + } + } + + return nil +} + +func registerFlagFloat(t reflect.Kind, fs *pflag.FlagSet, field interface{}, parts []string, vt float64, desc string) { + switch t { + case reflect.Float32: + if len(parts) == 1 { + fs.Float32Var(field.(*float32), parts[0], float32(vt), desc) + } else { + fs.Float32VarP(field.(*float32), parts[0], parts[1], float32(vt), desc) + } + case reflect.Float64: + if len(parts) == 1 { + fs.Float64Var(field.(*float64), parts[0], float64(vt), desc) + } else { + fs.Float64VarP(field.(*float64), parts[0], parts[1], float64(vt), desc) + } + } +} + +func registerFlagInt(t reflect.Kind, fs *pflag.FlagSet, field interface{}, parts []string, vt int64, desc string) { + switch t { + case reflect.Int: + if len(parts) == 1 { + fs.IntVar(field.(*int), parts[0], int(vt), desc) + } else { + fs.IntVarP(field.(*int), parts[0], parts[1], int(vt), desc) + } + case reflect.Int8: + if len(parts) == 1 { + fs.Int8Var(field.(*int8), parts[0], int8(vt), desc) + } else { + fs.Int8VarP(field.(*int8), parts[0], parts[1], int8(vt), desc) + } + case reflect.Int32: + if len(parts) == 1 { + fs.Int32Var(field.(*int32), parts[0], int32(vt), desc) + } else { + fs.Int32VarP(field.(*int32), parts[0], parts[1], int32(vt), desc) + } + case reflect.Int64: + if len(parts) == 1 { + fs.Int64Var(field.(*int64), parts[0], int64(vt), desc) + } else { + fs.Int64VarP(field.(*int64), parts[0], parts[1], int64(vt), desc) + } + } +} + +func registerFlagUint(t reflect.Kind, fs *pflag.FlagSet, field interface{}, parts []string, vt uint64, desc string) { + switch t { + case reflect.Uint: + if len(parts) == 1 { + fs.UintVar(field.(*uint), parts[0], uint(vt), desc) + } else { + fs.UintVarP(field.(*uint), parts[0], parts[1], uint(vt), desc) + } + case reflect.Uint8: + if len(parts) == 1 { + fs.Uint8Var(field.(*uint8), parts[0], uint8(vt), desc) + } else { + fs.Uint8VarP(field.(*uint8), parts[0], parts[1], uint8(vt), desc) + } + case reflect.Uint16: + if len(parts) == 1 { + fs.Uint16Var(field.(*uint16), parts[0], uint16(vt), desc) + } else { + fs.Uint16VarP(field.(*uint16), parts[0], parts[1], uint16(vt), desc) + } + case reflect.Uint32: + if len(parts) == 1 { + fs.Uint32Var(field.(*uint32), parts[0], uint32(vt), desc) + } else { + fs.Uint32VarP(field.(*uint32), parts[0], parts[1], uint32(vt), desc) + } + case reflect.Uint64: + if len(parts) == 1 { + fs.Uint64Var(field.(*uint64), parts[0], uint64(vt), desc) + } else { + fs.Uint64VarP(field.(*uint64), parts[0], parts[1], uint64(vt), desc) + } + } +} + +func envDefault(env, def string) string { + value := def + + if env != "" { + if e := os.Getenv(env); e != "" { + value = e + } + } + + return value +} diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..84830a8 --- /dev/null +++ b/config_test.go @@ -0,0 +1,274 @@ +package rconfig + +import ( + "os" + "testing" +) + +func TestGeneralMechanics(t *testing.T) { + cfg := struct { + Test string `default:"foo" env:"shell" flag:"shell" description:"Test"` + Test2 string `default:"blub" env:"testvar" flag:"testvar,t" description:"Test"` + DefaultFlag string `default:"goo"` + SadFlag string + }{} + + parse(&cfg, []string{ + "--shell=test23", + "-t", "bla", + }) + + if cfg.Test != "test23" { + t.Errorf("Test should be 'test23', is '%s'", cfg.Test) + } + + if cfg.Test2 != "bla" { + t.Errorf("Test2 should be 'bla', is '%s'", cfg.Test2) + } + + if cfg.SadFlag != "" { + t.Errorf("SadFlag should be '', is '%s'", cfg.SadFlag) + } + + if cfg.DefaultFlag != "goo" { + t.Errorf("DefaultFlag should be 'goo', is '%s'", cfg.DefaultFlag) + } + + parse(&cfg, []string{}) + + if cfg.Test != "foo" { + t.Errorf("Test should be 'foo', is '%s'", cfg.Test) + } + + os.Setenv("shell", "test546") + parse(&cfg, []string{}) + + if cfg.Test != "test546" { + t.Errorf("Test should be 'test546', is '%s'", cfg.Test) + } +} + +func TestBool(t *testing.T) { + cfg := struct { + Test1 bool `default:"true"` + Test2 bool `default:"false" flag:"test2"` + Test3 bool `default:"true" flag:"test3,t"` + Test4 bool `flag:"test4"` + }{} + + parse(&cfg, []string{ + "--test2", + "-t", + }) + + if !cfg.Test1 { + t.Errorf("Test1 should be 'true', is '%+v'", cfg.Test1) + } + if !cfg.Test2 { + t.Errorf("Test1 should be 'true', is '%+v'", cfg.Test2) + } + if !cfg.Test3 { + t.Errorf("Test1 should be 'true', is '%+v'", cfg.Test3) + } + if cfg.Test4 { + t.Errorf("Test1 should be 'false', is '%+v'", cfg.Test3) + } +} + +func TestInt(t *testing.T) { + cfg := struct { + Test int `flag:"int"` + TestP int `flag:"intp,i"` + Test8 int8 `flag:"int8"` + Test8P int8 `flag:"int8p,8"` + Test32 int32 `flag:"int32"` + Test32P int32 `flag:"int32p,3"` + Test64 int64 `flag:"int64"` + Test64P int64 `flag:"int64p,6"` + TestDef int8 `default:"66"` + }{} + + parse(&cfg, []string{ + "--int=1", "-i", "2", + "--int8=3", "-8", "4", + "--int32=5", "-3", "6", + "--int64=7", "-6", "8", + }) + + if cfg.Test != 1 || cfg.TestP != 2 || cfg.Test8 != 3 || cfg.Test8P != 4 || cfg.Test32 != 5 || cfg.Test32P != 6 || cfg.Test64 != 7 || cfg.Test64P != 8 { + t.Errorf("One of the int tests failed.") + } + + if cfg.TestDef != 66 { + t.Errorf("TestDef should be '66', is '%d'", cfg.TestDef) + } +} + +func TestUint(t *testing.T) { + cfg := struct { + Test uint `flag:"int"` + TestP uint `flag:"intp,i"` + Test8 uint8 `flag:"int8"` + Test8P uint8 `flag:"int8p,8"` + Test16 uint16 `flag:"int16"` + Test16P uint16 `flag:"int16p,1"` + Test32 uint32 `flag:"int32"` + Test32P uint32 `flag:"int32p,3"` + Test64 uint64 `flag:"int64"` + Test64P uint64 `flag:"int64p,6"` + TestDef uint8 `default:"66"` + }{} + + parse(&cfg, []string{ + "--int=1", "-i", "2", + "--int8=3", "-8", "4", + "--int32=5", "-3", "6", + "--int64=7", "-6", "8", + "--int16=9", "-1", "10", + }) + + if cfg.Test != 1 || cfg.TestP != 2 || cfg.Test8 != 3 || cfg.Test8P != 4 || cfg.Test32 != 5 || cfg.Test32P != 6 || cfg.Test64 != 7 || cfg.Test64P != 8 || cfg.Test16 != 9 || cfg.Test16P != 10 { + t.Errorf("One of the uint tests failed.") + } + + if cfg.TestDef != 66 { + t.Errorf("TestDef should be '66', is '%d'", cfg.TestDef) + } +} + +func TestFloat(t *testing.T) { + cfg := struct { + Test32 float32 `flag:"float32"` + Test32P float32 `flag:"float32p,3"` + Test64 float64 `flag:"float64"` + Test64P float64 `flag:"float64p,6"` + TestDef float32 `default:"66.256"` + }{} + + parse(&cfg, []string{ + "--float32=5.5", "-3", "6.6", + "--float64=7.7", "-6", "8.8", + }) + + if cfg.Test32 != 5.5 || cfg.Test32P != 6.6 || cfg.Test64 != 7.7 || cfg.Test64P != 8.8 { + t.Errorf("One of the int tests failed.") + } + + if cfg.TestDef != 66.256 { + t.Errorf("TestDef should be '66.256', is '%.3f'", cfg.TestDef) + } +} + +func TestSubStruct(t *testing.T) { + cfg := struct { + Test string `default:"blubb"` + Sub struct { + Test string `default:"Hallo"` + } + }{} + + if err := parse(&cfg, []string{}); err != nil { + t.Errorf("Test errored: %s", err) + } + + if cfg.Test != "blubb" { + t.Errorf("Test should be 'blubb', is '%s'", cfg.Test) + } + + if cfg.Sub.Test != "Hallo" { + t.Errorf("Sub.Test should be 'Hallo', is '%s'", cfg.Sub.Test) + } +} + +func TestSlice(t *testing.T) { + cfg := struct { + Int []int `default:"1,2,3" flag:"int"` + String []string `default:"a,b,c" flag:"string"` + IntP []int `default:"1,2,3" flag:"intp,i"` + StringP []string `default:"a,b,c" flag:"stringp,s"` + }{} + + if err := parse(&cfg, []string{ + "--int=4,5", "-s", "hallo,welt", + }); err != nil { + t.Errorf("Test errored: %s", err) + } + + if len(cfg.Int) != 2 || cfg.Int[0] != 4 || cfg.Int[1] != 5 { + t.Errorf("Int should be '4,5', is '%+v'", cfg.Int) + } + + if len(cfg.String) != 3 || cfg.String[0] != "a" || cfg.String[1] != "b" { + t.Errorf("String should be 'a,b,c', is '%+v'", cfg.String) + } + + if len(cfg.StringP) != 2 || cfg.StringP[0] != "hallo" || cfg.StringP[1] != "welt" { + t.Errorf("StringP should be 'hallo,welt', is '%+v'", cfg.StringP) + } +} + +func TestErrors(t *testing.T) { + if err := parse(&struct { + A int `default:"a"` + }{}, []string{}); err == nil { + t.Errorf("Test should have errored") + } + + if err := parse(&struct { + A float32 `default:"a"` + }{}, []string{}); err == nil { + t.Errorf("Test should have errored") + } + + if err := parse(&struct { + A uint `default:"a"` + }{}, []string{}); err == nil { + t.Errorf("Test should have errored") + } + + if err := parse(&struct { + B struct { + A uint `default:"a"` + } + }{}, []string{}); err == nil { + t.Errorf("Test should have errored") + } + + if err := parse(&struct { + A []int `default:"a,bn"` + }{}, []string{}); err == nil { + t.Errorf("Test should have errored") + } +} + +func TestOSArgs(t *testing.T) { + os.Args = []string{"--a=bar"} + + cfg := struct { + A string `default:"a" flag:"a"` + }{} + + Parse(&cfg) + + if cfg.A != "bar" { + t.Errorf("A should be 'bar', is '%s'", cfg.A) + } +} + +func TestNonPointer(t *testing.T) { + cfg := struct { + A string `default:"a"` + }{} + + if err := parse(cfg, []string{}); err == nil { + t.Errorf("Test should have errored") + } +} + +func TestOtherType(t *testing.T) { + cfg := "test" + + if err := parse(&cfg, []string{}); err == nil { + t.Errorf("Test should have errored") + } +} diff --git a/example_test.go b/example_test.go new file mode 100644 index 0000000..0a65b2f --- /dev/null +++ b/example_test.go @@ -0,0 +1,37 @@ +package rconfig + +import ( + "fmt" + "os" +) + +func ExampleParse() { + // We're building an example configuration with a sub-struct to be filled + // by the Parse command. + config := struct { + Username string `default:"unknown" flag:"user,u" description:"Your name"` + Details struct { + Age int `default:"25" flag:"age" description:"Your age"` + } + }{} + + // To have more relieable results we're setting os.Args to a known value. + // In real-life use cases you wouldn't do this but parse the original + // commandline arguments. + os.Args = []string{ + "example", + "--user=Luzifer", + } + + Parse(&config) + + fmt.Printf("Hello %s, happy birthday for your %dth birthday.", + config.Username, + config.Details.Age) + + // You can also show an usage message for your user + Usage() + + // Output: + // Hello Luzifer, happy birthday for your 25th birthday. +}