Skip to content

Commit

Permalink
feat(Expand Variables): Custom variable expansion instead of Go's os.…
Browse files Browse the repository at this point in the history
…Expand

Copy over the tests from https://github.com/bkeepers/dotenv/blob/master/spec/dotenv/parser_spec.rb
related to expanding variables and implement the required changes. I also realized as part of this
that this implementation was not handling values in single quotes properly (e.g.: not the same was
as the ruby package mentionned) so that has been fixed as well along with the related tests.

Fixes: #52
  • Loading branch information
lucastetreault committed Sep 11, 2018
1 parent 1709ab1 commit 2d8b3aa
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 17 deletions.
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_]+)?\}?`)

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] == "(" {
return submatch[0][1:]
} else if submatch[4] != "" {
return m[submatch[4]]
}
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

0 comments on commit 2d8b3aa

Please sign in to comment.