Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Expand Variables): Custom variable expansion instead of Go's os.Expand #58

Merged
merged 1 commit into from
Sep 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions godotenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,12 +270,19 @@ func parseValue(value string, envMap map[string]string) string {

// check if we've got quoted values or possible escapes
if len(value) > 1 {
first := string(value[0:1])
last := string(value[len(value)-1:])
if first == last && strings.ContainsAny(first, `"'`) {
rs := regexp.MustCompile(`\A'(.*)'\z`)
singleQuotes := rs.FindStringSubmatch(value)

rd := regexp.MustCompile(`\A"(.*)"\z`)
doubleQuotes := rd.FindStringSubmatch(value)

if singleQuotes != nil || doubleQuotes != nil {
// pull the quotes off the edges
value = value[1 : len(value)-1]
// handle escapes
}

if doubleQuotes != nil {
// expand newlines
escapeRegex := regexp.MustCompile(`\\.`)
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
c := strings.TrimPrefix(match, `\`)
Expand All @@ -285,23 +292,38 @@ func parseValue(value string, envMap map[string]string) string {
case "r":
return "\r"
default:
return c
return match
}
})
// unescape characters
e := regexp.MustCompile(`\\([^$])`)
value = e.ReplaceAllString(value, "$1")
}

if singleQuotes == nil {
value = expandVariables(value, envMap)
}
}

// expand variables
value = os.Expand(value, func(key string) string {
if val, ok := envMap[key]; ok {
return val
return value
}

func expandVariables(v string, m map[string]string) string {
r := regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isn't a (\))? missing at the end of this regex?


return r.ReplaceAllStringFunc(v, func(s string) string {
submatch := r.FindStringSubmatch(s)

if submatch == nil {
return s
}
if val, ok := os.LookupEnv(key); ok {
return val
if submatch[1] == "\\" || submatch[2] == "(" {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can the second submatch be ( shoun't this be submatch[1] == "\\" || submatch[2] == "(" ?
But I don't really get the (\()? it wouuld make sense for supporting $(VAR) but in that case a (\))? is missing at the end of the regex and submatch[2] == "(" isn't needed either.

return submatch[0][1:]
} else if submatch[4] != "" {
return m[submatch[4]]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the var was defined outide of the env file? shouln't this fallback to os.Geteenv(submatch[4]) if submatch[4] is not present in the map? Or at least just return s in that case...

}
return ""
return s
})
return value
}

func isIgnoredLine(line string) bool {
Expand Down
73 changes: 69 additions & 4 deletions godotenv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"reflect"
"testing"
"strings"
)

var noopPresets = make(map[string]string)
Expand Down Expand Up @@ -161,7 +162,7 @@ func TestLoadExportedEnv(t *testing.T) {
envFileName := "fixtures/exported.env"
expectedValues := map[string]string{
"OPTION_A": "2",
"OPTION_B": "\n",
"OPTION_B": "\\n",
}

loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
Expand All @@ -182,7 +183,7 @@ func TestLoadQuotedEnv(t *testing.T) {
"OPTION_A": "1",
"OPTION_B": "2",
"OPTION_C": "",
"OPTION_D": "\n",
"OPTION_D": "\\n",
"OPTION_E": "1",
"OPTION_F": "2",
"OPTION_G": "",
Expand All @@ -193,7 +194,7 @@ func TestLoadQuotedEnv(t *testing.T) {
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}

func TestSubstituitions(t *testing.T) {
func TestSubstitutions(t *testing.T) {
envFileName := "fixtures/substitutions.env"
expectedValues := map[string]string{
"OPTION_A": "1",
Expand All @@ -206,6 +207,70 @@ func TestSubstituitions(t *testing.T) {
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
}

func TestExpanding(t *testing.T) {
tests := []struct {
name string
input string
expected map[string]string
}{
{
"expands variables found in values",
"FOO=test\nBAR=$FOO",
map[string]string{"FOO": "test", "BAR": "test"},
},
{
"parses variables wrapped in brackets",
"FOO=test\nBAR=${FOO}bar",
map[string]string{"FOO": "test", "BAR": "testbar"},
},
{
"expands undefined variables to an empty string",
"BAR=$FOO",
map[string]string{"BAR": ""},
},
{
"expands variables in double quoted strings",
"FOO=test\nBAR=\"quote $FOO\"",
map[string]string{"FOO": "test", "BAR": "quote test"},
},
{
"does not expand variables in single quoted strings",
"BAR='quote $FOO'",
map[string]string{"BAR": "quote $FOO"},
},
{
"does not expand escaped variables",
`FOO="foo\$BAR"`,
map[string]string{"FOO": "foo$BAR"},
},
{
"does not expand escaped variables",
`FOO="foo\${BAR}"`,
map[string]string{"FOO": "foo${BAR}"},
},
{
"does not expand escaped variables",
"FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"",
map[string]string{"FOO": "test", "BAR": "foo${FOO} test"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
env, err := Parse(strings.NewReader(tt.input))
if err != nil {
t.Errorf("Error: %s", err.Error())
}
for k, v := range tt.expected {
if strings.Compare(env[k], v) != 0 {
t.Errorf("Expected: %s, Actual: %s", v, env[k])
}
}
})
}

}

func TestActualEnvVarsAreLeftAlone(t *testing.T) {
os.Clearenv()
os.Setenv("OPTION_A", "actualenv")
Expand Down Expand Up @@ -247,7 +312,7 @@ func TestParsing(t *testing.T) {

// parses export keyword
parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2")
parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\n")
parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\\n")

// it 'expands newlines in quoted strings' do
// expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz")
Expand Down