Skip to content

Commit

Permalink
fix escaping and expose expansion with custom values
Browse files Browse the repository at this point in the history
  • Loading branch information
choffmeister committed Dec 11, 2021
1 parent ebc29e0 commit 609ab77
Show file tree
Hide file tree
Showing 2 changed files with 77 additions and 23 deletions.
33 changes: 24 additions & 9 deletions expandenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,33 +8,48 @@ import (
"strings"
)

func environMap() map[string]string {
result := map[string]string{}
for _, entry := range os.Environ() {
splitted := strings.SplitN(entry, "=", 2)
key := splitted[0]
value := splitted[1]
result[key] = value
}
return result
}

func ExpandEnv(input interface{}) (interface{}, error) {
return Expand(input, environMap())
}

func Expand(input interface{}, values map[string]string) (interface{}, error) {
singleRegex := regexp.MustCompile(`^\$\{[^\}]+\}$`)
detectRegex := regexp.MustCompile(`(?:^|[^\\])\$\{[^\}]+\}`)
detectRegex := regexp.MustCompile(`\\?\$\{[^\}]+\}`)
var recursion func(current interface{}) (interface{}, []error)
recursion = func(current interface{}) (interface{}, []error) {
if current, ok := current.(string); ok {
p := singleRegex.FindStringSubmatch(current)
if p != nil {
expanded, err := expandEnvValue(current)
expanded, err := expandEnvValue(current, values)
if err != nil {
return current, []error{err}
}
return expanded, nil
}
errs := []error{}
expanded := detectRegex.ReplaceAllStringFunc(current, func(str string) string {
index := strings.IndexAny(str, "$")
prefix := str[:index]
value := str[index:]
if strings.HasPrefix(str, "\\") {
return str[1:]
}

expanded, err := expandEnvValue(value)
expanded, err := expandEnvValue(str, values)
if err != nil {
errs = append(errs, err)
return str
}

return fmt.Sprintf("%s%v", prefix, expanded)
return fmt.Sprintf("%v", expanded)
})
return expanded, errs
}
Expand Down Expand Up @@ -75,7 +90,7 @@ func ExpandEnv(input interface{}) (interface{}, error) {
return output, nil
}

func expandEnvValue(str string) (interface{}, error) {
func expandEnvValue(str string, values map[string]string) (interface{}, error) {
regex := regexp.MustCompile(`^\$\{(?P<name>[^:]+)(?P<hasFormat>:(?P<format>number|boolean|string))?(?P<hasFallback>:-(?P<fallback>.*))?\}$`)
p := regex.FindStringSubmatch(str)
if p == nil {
Expand All @@ -85,7 +100,7 @@ func expandEnvValue(str string) (interface{}, error) {
format := p[regex.SubexpIndex("format")]
hasFallback := p[regex.SubexpIndex("hasFallback")] != ""
fallback := p[regex.SubexpIndex("fallback")]
value, ok := os.LookupEnv(name)
value, ok := values[name]
if !ok {
if !hasFallback {
return nil, fmt.Errorf("environment variable %s is missing", name)
Expand Down
67 changes: 53 additions & 14 deletions expandenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import (
"gopkg.in/yaml.v3"
)

func TestExpandEnv(t *testing.T) {
os.Setenv("ENV_A", "a")
os.Setenv("ENV_B", "b")
os.Setenv("ENV_42", "42")
os.Setenv("ENV_42_5", "42.5")
os.Setenv("ENV_YES", "yes")
os.Setenv("ENV_MULTI_LINE", "line1\nline2")
func TestExpand(t *testing.T) {
values := map[string]string{
"ENV_A": "a",
"ENV_B": "b",
"ENV_42": "42",
"ENV_42_5": "42.5",
"ENV_YES": "yes",
"ENV_MULTI_LINE": "line1\nline2",
}

testCases := []struct {
input interface{}
Expand Down Expand Up @@ -71,12 +73,12 @@ func TestExpandEnv(t *testing.T) {
},
{
input: "\\${ENV_A}",
output: "\\${ENV_A}",
output: "${ENV_A}",
label: "variabled-escacped-string",
},
{
input: "prefix \\${ENV_A} suffix",
output: "prefix \\${ENV_A} suffix",
output: "prefix ${ENV_A} suffix",
label: "variabled-escacped-string-2",
},
{
Expand Down Expand Up @@ -134,7 +136,7 @@ func TestExpandEnv(t *testing.T) {
}

for _, testCase := range testCases {
output, err := ExpandEnv(testCase.input)
output, err := Expand(testCase.input, values)
if testCase.error == nil {
assert.NoError(t, err, testCase.label)
} else {
Expand All @@ -144,9 +146,44 @@ func TestExpandEnv(t *testing.T) {
}
}

func TestExpandEnvWithYaml(t *testing.T) {
func TestExpandEnv(t *testing.T) {
os.Setenv("ENV_A", "a")
os.Setenv("ENV_MULTI_LINE", "line1\nline2")
os.Setenv("ENV_B", "b")

testCases := []struct {
input interface{}
output interface{}
label string
error error
}{
{
input: "${ENV_A}",
output: "a",
label: "variabled-string",
},
{
input: "prefix ${ENV_B} suffix",
output: "prefix b suffix",
label: "variabled-string-2",
},
}

for _, testCase := range testCases {
output, err := ExpandEnv(testCase.input)
if testCase.error == nil {
assert.NoError(t, err, testCase.label)
} else {
assert.EqualError(t, err, testCase.error.Error(), testCase.label)
}
assert.Equal(t, testCase.output, output, testCase.label)
}
}

func TestExpandWithYaml(t *testing.T) {
values := map[string]string{
"ENV_A": "a",
"ENV_MULTI_LINE": "line1\nline2",
}

yamlBytes := []byte(`
a: ${ENV_A}
Expand All @@ -157,11 +194,12 @@ c:
d: ${ENV_MULTI_LINE}
e: ${ENV_UNKNOWN}
f: \${ENV_ESCAPED}
g: \\${ENV_ESCAPED}
`)
var yamlRaw interface{}
err := yaml.Unmarshal(yamlBytes, &yamlRaw)
assert.NoError(t, err)
yamlRaw, err = ExpandEnv(yamlRaw)
yamlRaw, err = Expand(yamlRaw, values)
assert.EqualError(t, err, "environment variable ENV_UNKNOWN is missing")
yamlBytes, err = yaml.Marshal(yamlRaw)
assert.NoError(t, err)
Expand All @@ -174,6 +212,7 @@ d: |-
line1
line2
e: ${ENV_UNKNOWN}
f: \${ENV_ESCAPED}
f: ${ENV_ESCAPED}
g: \${ENV_ESCAPED}
`, string(yamlBytes))
}

0 comments on commit 609ab77

Please sign in to comment.