1
0
Fork 0
mirror of https://github.com/Luzifer/rconfig.git synced 2024-12-30 08:01:19 +00:00

Initial version of rconfig

This commit is contained in:
Knut Ahlers 2015-07-12 11:51:19 +02:00
commit 0c78105a26
5 changed files with 664 additions and 0 deletions

13
LICENSE Normal file
View file

@ -0,0 +1,13 @@
Copyright 2015 Knut Ahlers <knut@ahlers.me>
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.

58
README.md Normal file
View file

@ -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.

282
config.go Normal file
View file

@ -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
}

274
config_test.go Normal file
View file

@ -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")
}
}

37
example_test.go Normal file
View file

@ -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.
}