From df4f4d402216a5fbde1f63924cd31ac3a1e616da Mon Sep 17 00:00:00 2001 From: Robert Coleman Date: Sat, 16 Mar 2024 17:52:27 +1300 Subject: [PATCH] cfgparser: add optional splitting of ENV variable values on comma via env_split --- docs/reference/config-syntax.md | 12 +++++++-- framework/cfgparser/env.go | 39 +++++++++++++++++++++++----- framework/cfgparser/parse_test.go | 42 +++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/docs/reference/config-syntax.md b/docs/reference/config-syntax.md index 72e18d41..62a0e349 100644 --- a/docs/reference/config-syntax.md +++ b/docs/reference/config-syntax.md @@ -87,6 +87,16 @@ directive0 {env:VAR} Parse is forgiving and incomplete variable placeholder (e.g. '{env:VAR') will be left as-is. Variables are expanded inside quotes too. +Environment values that are comma separated can optionally be split to space separated strings using {env_split:COMMA_SEPARATED_VARIABLE_NAME} + +``` +# SEP_VAR=foo,bar,baz + +directive1 {env_split:SEP_VAR} +``` + +In this usage the value of `directive1` would be `foo bar baz` (and `{env:SEP_VAR}` would be `foo,bar,baz`) + ## Snippets & imports You can reuse blocks of configuration by defining them as "snippets". Snippet @@ -196,5 +206,3 @@ No-op module. It doesn't need to be configured explicitly and can be referenced using "dummy" name. It can act as a delivery target or auth. provider. In the latter case, it will accept any credentials, allowing any client to authenticate using any username and password (use with care!). - - diff --git a/framework/cfgparser/env.go b/framework/cfgparser/env.go index c7ad03cf..9d79dcab 100644 --- a/framework/cfgparser/env.go +++ b/framework/cfgparser/env.go @@ -31,13 +31,19 @@ func expandEnvironment(nodes []Node) []Node { return nil } - replacer := buildEnvReplacer() + envMap := buildEnvMap() + replacer := buildEnvReplacerFromMap(envMap) newNodes := make([]Node, 0, len(nodes)) for _, node := range nodes { node.Name = removeUnexpandedEnvvars(replacer.Replace(node.Name)) newArgs := make([]string, 0, len(node.Args)) for _, arg := range node.Args { - newArgs = append(newArgs, removeUnexpandedEnvvars(replacer.Replace(arg))) + expArg := replacer.Replace(arg) + if splitArgs, ok := handleEnvSplitWithMap(expArg, envMap); ok { + newArgs = append(newArgs, splitArgs...) + } else { + newArgs = append(newArgs, removeUnexpandedEnvvars(expArg)) + } } node.Args = newArgs node.Children = expandEnvironment(node.Children) @@ -47,21 +53,42 @@ func expandEnvironment(nodes []Node) []Node { } var unixEnvvarRe = regexp.MustCompile(`{env:([^\$]+)}`) +var unixEnvSplitRe = regexp.MustCompile(`{env_split:([^\$]+)}`) func removeUnexpandedEnvvars(s string) string { s = unixEnvvarRe.ReplaceAllString(s, "") return s } -func buildEnvReplacer() *strings.Replacer { +func buildEnvMap() map[string]string { env := os.Environ() - pairs := make([]string, 0, len(env)*4) + envMap := make(map[string]string, len(env)) for _, entry := range env { parts := strings.SplitN(entry, "=", 2) - key := parts[0] - value := parts[1] + if len(parts) == 2 { + envMap[parts[0]] = parts[1] + } + } + return envMap +} +func buildEnvReplacerFromMap(envMap map[string]string) *strings.Replacer { + pairs := make([]string, 0, len(envMap)*4) + for key, value := range envMap { pairs = append(pairs, "{env:"+key+"}", value) } return strings.NewReplacer(pairs...) } + +func handleEnvSplitWithMap(arg string, envMap map[string]string) ([]string, bool) { + matches := unixEnvSplitRe.FindStringSubmatch(arg) + if len(matches) == 2 { + value, exists := envMap[matches[1]] + if !exists { + return nil, false + } + splitValues := strings.Split(value, ",") + return splitValues, true + } + return nil, false +} diff --git a/framework/cfgparser/parse_test.go b/framework/cfgparser/parse_test.go index 9488e580..3c57b163 100644 --- a/framework/cfgparser/parse_test.go +++ b/framework/cfgparser/parse_test.go @@ -342,6 +342,47 @@ var cases = []struct { }, false, }, + { + "environment variable split at comma expansion", + `a {env_split:TESTING_VARIABLE_SPLIT}`, + []Node{ + { + Name: "a", + Args: []string{"value1", "value2", "value3"}, + Children: nil, + File: "test", + Line: 1, + }, + }, + false, + }, + { + "environment variable mixed usage", + `mixed {env:TESTING_VARIABLE} {env_split:TESTING_VARIABLE_SPLIT}`, + []Node{ + { + Name: "mixed", + Args: []string{"ABCDEF", "value1", "value2", "value3"}, + Children: nil, + File: "test", + Line: 1, + }, + }, + false, + }, + { + "environment variable not split on comma", + `not_split {env:TESTING_VARIABLE_SPLIT}`, + []Node{ + { + Name: "not_split", + Args: []string{"value1,value2,value3"}, + File: "test", + Line: 1, + }, + }, + false, + }, { "snippet expansion", `(foo) { a } @@ -581,6 +622,7 @@ func printTree(t *testing.T, root Node, indent int) { func TestRead(t *testing.T) { os.Setenv("TESTING_VARIABLE", "ABCDEF") os.Setenv("TESTING_VARIABLE2", "ABC2 DEF2") + os.Setenv("TESTING_VARIABLE_SPLIT", "value1,value2,value3") for _, case_ := range cases { case_ := case_