From f2a0efe97cd500703f83cee2b576c019dc8fdf63 Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Thu, 2 Aug 2018 12:02:13 +0200 Subject: [PATCH] Add AutoEnv feature to automatically derive environment variable names from struct field names Signed-off-by: Knut Ahlers --- autoenv.go | 64 +++++++++++++++++++++++++++++++++++++++++++++++++ autoenv_test.go | 29 ++++++++++++++++++++++ config.go | 17 +++++++++++-- 3 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 autoenv.go create mode 100644 autoenv_test.go diff --git a/autoenv.go b/autoenv.go new file mode 100644 index 0000000..70ab679 --- /dev/null +++ b/autoenv.go @@ -0,0 +1,64 @@ +package rconfig + +import "strings" + +type characterClass [2]rune + +func (c characterClass) Contains(r rune) bool { + return c[0] <= r && c[1] >= r +} + +type characterClasses []characterClass + +func (c characterClasses) Contains(r rune) bool { + for _, cc := range c { + if cc.Contains(r) { + return true + } + } + return false +} + +var ( + charGroupUpperLetter = characterClass{'A', 'Z'} + charGroupLowerLetter = characterClass{'a', 'z'} + charGroupNumber = characterClass{'0', '9'} + charGroupLowerNumber = characterClasses{charGroupLowerLetter, charGroupNumber} +) + +func deriveEnvVarName(s string) string { + var ( + words []string + word []rune + ) + + for _, l := range s { + switch { + case charGroupUpperLetter.Contains(l): + if len(word) > 0 && charGroupLowerNumber.Contains(word[len(word)-1]) { + words = append(words, string(word)) + word = []rune{} + } + word = append(word, l) + + case charGroupLowerLetter.Contains(l): + if len(word) > 1 && charGroupUpperLetter.Contains(word[len(word)-1]) { + words = append(words, string(word[0:len(word)-1])) + word = word[len(word)-1:] + } + word = append(word, l) + + case charGroupNumber.Contains(l): + word = append(word, l) + + default: + if len(word) > 0 { + words = append(words, string(word)) + } + word = []rune{} + } + } + words = append(words, string(word)) + + return strings.ToUpper(strings.Join(words, "_")) +} diff --git a/autoenv_test.go b/autoenv_test.go new file mode 100644 index 0000000..47db714 --- /dev/null +++ b/autoenv_test.go @@ -0,0 +1,29 @@ +package rconfig + +import ( + "testing" +) + +func TestDeriveEnvVarName(t *testing.T) { + for test, expect := range map[string]string{ + "1Foobar": "1_FOOBAR", + "BC1": "BC1", + "BIGCase1": "BIG_CASE1", + "BIGCase": "BIG_CASE", + "Camel1": "CAMEL1", + "camel": "CAMEL", + "Camel": "CAMEL", + "CAMEL": "CAMEL", + "CamelCase": "CAMEL_CASE", + "_foobar": "FOOBAR", + "ILoveGoAndJSONSoMuch": "I_LOVE_GO_AND_JSON_SO_MUCH", + "mrT": "MR_T", + "my_case1": "MY_CASE1", + "MyFieldName": "MY_FIELD_NAME", + "SmallCASE": "SMALL_CASE", + } { + if d := deriveEnvVarName(test); d != expect { + t.Errorf("Derived variable %q did not match expectation %q", d, expect) + } + } +} diff --git a/config.go b/config.go index ee9301e..eedde00 100644 --- a/config.go +++ b/config.go @@ -17,6 +17,7 @@ import ( ) var ( + autoEnv bool fs *pflag.FlagSet variableDefaults map[string]string ) @@ -60,6 +61,13 @@ func Args() []string { return fs.Args() } +// AutoEnv enables or disables automated env variable guessing. If no `env` struct +// tag was set and AutoEnv is enabled the env variable name is derived from the +// name of the field: `MyFieldName` will get `MY_FIELD_NAME` +func AutoEnv(enable bool) { + autoEnv = enable +} + // 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() { @@ -116,7 +124,7 @@ func execTags(in interface{}, fs *pflag.FlagSet) error { } value := varDefault(typeField.Tag.Get("vardefault"), typeField.Tag.Get("default")) - value = envDefault(typeField.Tag.Get("env"), value) + value = envDefault(typeField, value) parts := strings.Split(typeField.Tag.Get("flag"), ",") switch typeField.Type { @@ -334,9 +342,14 @@ func registerFlagUint(t reflect.Kind, fs *pflag.FlagSet, field interface{}, part } } -func envDefault(env, def string) string { +func envDefault(field reflect.StructField, def string) string { value := def + env := field.Tag.Get("env") + if env == "" && autoEnv { + env = deriveEnvVarName(field.Name) + } + if env != "" { if e := os.Getenv(env); e != "" { value = e