Compare commits

..

11 Commits

Author SHA1 Message Date
23d116af35 Merge pull request #58 from lucastetreault/master
feat(Expand Variables): Custom variable expansion instead of Go's os.Expand
2018-09-11 18:04:31 +10:00
2d8b3aab88 feat(Expand Variables): Custom variable expansion instead of Go's os.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
2018-09-11 00:55:10 -06:00
1709ab122c Merge pull request #54 from egorse/master
The value expand fallback to actual ENV values
2018-04-05 15:36:34 +10:00
8ad714e304 The value expand fallback to actual ENV values 2018-03-31 23:18:36 +03:00
6bb0851667 Merge pull request #47 from sachaos/feature-expand-variables-on-value
Support variable substitution in dotenv files
2018-01-15 13:49:21 +11:00
06e67b5ef3 Update my homepage link 2017-12-23 10:35:39 +11:00
2707e9ff66 Fix test, $ should not be escaped 2017-11-20 23:41:46 +09:00
50c29652a0 Expand variables on parseValue 2017-11-20 23:20:38 +09:00
33977c2d8d Add test for substitutions 2017-11-20 23:20:19 +09:00
9be76b3741 Pass envMap to parseLine & parseValue 2017-11-20 22:21:39 +09:00
6d367c18ed Merge pull request #46 from joho/fix_quoting_parser_bug
WIP Parsing bug with nested quotes
2017-11-10 12:03:15 +11:00
4 changed files with 131 additions and 16 deletions

View File

@ -160,4 +160,4 @@ Linux: [![Build Status](https://travis-ci.org/joho/godotenv.svg?branch=master)](
## Who? ## Who?
The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](http://whoisjohnbarton.com) based off the tests/fixtures in the original library. The original library [dotenv](https://github.com/bkeepers/dotenv) was written by [Brandon Keepers](http://opensoul.org/), and this port was done by [John Barton](https://johnbarton.co/) based off the tests/fixtures in the original library.

View File

@ -0,0 +1,5 @@
OPTION_A=1
OPTION_B=${OPTION_A}
OPTION_C=$OPTION_B
OPTION_D=${OPTION_A}${OPTION_B}
OPTION_E=${OPTION_NOT_DEFINED}

View File

@ -112,7 +112,7 @@ func Parse(r io.Reader) (envMap map[string]string, err error) {
for _, fullLine := range lines { for _, fullLine := range lines {
if !isIgnoredLine(fullLine) { if !isIgnoredLine(fullLine) {
var key, value string var key, value string
key, value, err = parseLine(fullLine) key, value, err = parseLine(fullLine, envMap)
if err != nil { if err != nil {
return return
@ -209,7 +209,7 @@ func readFile(filename string) (envMap map[string]string, err error) {
return Parse(file) return Parse(file)
} }
func parseLine(line string) (key string, value string, err error) { func parseLine(line string, envMap map[string]string) (key string, value string, err error) {
if len(line) == 0 { if len(line) == 0 {
err = errors.New("zero length string") err = errors.New("zero length string")
return return
@ -259,23 +259,30 @@ func parseLine(line string) (key string, value string, err error) {
key = strings.Trim(key, " ") key = strings.Trim(key, " ")
// Parse the value // Parse the value
value = parseValue(splitString[1]) value = parseValue(splitString[1], envMap)
return return
} }
func parseValue(value string) string { func parseValue(value string, envMap map[string]string) string {
// trim // trim
value = strings.Trim(value, " ") value = strings.Trim(value, " ")
// check if we've got quoted values or possible escapes // check if we've got quoted values or possible escapes
if len(value) > 1 { if len(value) > 1 {
first := string(value[0:1]) rs := regexp.MustCompile(`\A'(.*)'\z`)
last := string(value[len(value)-1:]) singleQuotes := rs.FindStringSubmatch(value)
if first == last && strings.ContainsAny(first, `"'`) {
rd := regexp.MustCompile(`\A"(.*)"\z`)
doubleQuotes := rd.FindStringSubmatch(value)
if singleQuotes != nil || doubleQuotes != nil {
// pull the quotes off the edges // pull the quotes off the edges
value = value[1 : len(value)-1] value = value[1 : len(value)-1]
// handle escapes }
if doubleQuotes != nil {
// expand newlines
escapeRegex := regexp.MustCompile(`\\.`) escapeRegex := regexp.MustCompile(`\\.`)
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string { value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
c := strings.TrimPrefix(match, `\`) c := strings.TrimPrefix(match, `\`)
@ -285,15 +292,40 @@ func parseValue(value string) string {
case "r": case "r":
return "\r" return "\r"
default: default:
return c return match
} }
}) })
// unescape characters
e := regexp.MustCompile(`\\([^$])`)
value = e.ReplaceAllString(value, "$1")
}
if singleQuotes == nil {
value = expandVariables(value, envMap)
} }
} }
return value 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 submatch[1] == "\\" || submatch[2] == "(" {
return submatch[0][1:]
} else if submatch[4] != "" {
return m[submatch[4]]
}
return s
})
}
func isIgnoredLine(line string) bool { func isIgnoredLine(line string) bool {
trimmedLine := strings.Trim(line, " \n\t") trimmedLine := strings.Trim(line, " \n\t")
return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#") return len(trimmedLine) == 0 || strings.HasPrefix(trimmedLine, "#")

View File

@ -6,12 +6,13 @@ import (
"os" "os"
"reflect" "reflect"
"testing" "testing"
"strings"
) )
var noopPresets = make(map[string]string) var noopPresets = make(map[string]string)
func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) { func parseAndCompare(t *testing.T, rawEnvLine string, expectedKey string, expectedValue string) {
key, value, _ := parseLine(rawEnvLine) key, value, _ := parseLine(rawEnvLine, noopPresets)
if key != expectedKey || value != expectedValue { if key != expectedKey || value != expectedValue {
t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, key, value) t.Errorf("Expected '%v' to parse as '%v' => '%v', got '%v' => '%v' instead", rawEnvLine, expectedKey, expectedValue, key, value)
} }
@ -161,7 +162,7 @@ func TestLoadExportedEnv(t *testing.T) {
envFileName := "fixtures/exported.env" envFileName := "fixtures/exported.env"
expectedValues := map[string]string{ expectedValues := map[string]string{
"OPTION_A": "2", "OPTION_A": "2",
"OPTION_B": "\n", "OPTION_B": "\\n",
} }
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
@ -182,7 +183,7 @@ func TestLoadQuotedEnv(t *testing.T) {
"OPTION_A": "1", "OPTION_A": "1",
"OPTION_B": "2", "OPTION_B": "2",
"OPTION_C": "", "OPTION_C": "",
"OPTION_D": "\n", "OPTION_D": "\\n",
"OPTION_E": "1", "OPTION_E": "1",
"OPTION_F": "2", "OPTION_F": "2",
"OPTION_G": "", "OPTION_G": "",
@ -193,6 +194,83 @@ func TestLoadQuotedEnv(t *testing.T) {
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets) loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
} }
func TestSubstitutions(t *testing.T) {
envFileName := "fixtures/substitutions.env"
expectedValues := map[string]string{
"OPTION_A": "1",
"OPTION_B": "1",
"OPTION_C": "1",
"OPTION_D": "11",
"OPTION_E": "",
}
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) { func TestActualEnvVarsAreLeftAlone(t *testing.T) {
os.Clearenv() os.Clearenv()
os.Setenv("OPTION_A", "actualenv") os.Setenv("OPTION_A", "actualenv")
@ -234,7 +312,7 @@ func TestParsing(t *testing.T) {
// parses export keyword // parses export keyword
parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2") 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 // it 'expands newlines in quoted strings' do
// expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz") // expect(env('FOO="bar\nbaz"')).to eql('FOO' => "bar\nbaz")
@ -280,7 +358,7 @@ func TestParsing(t *testing.T) {
// it 'throws an error if line format is incorrect' do // it 'throws an error if line format is incorrect' do
// expect{env('lol$wut')}.to raise_error(Dotenv::FormatError) // expect{env('lol$wut')}.to raise_error(Dotenv::FormatError)
badlyFormattedLine := "lol$wut" badlyFormattedLine := "lol$wut"
_, _, err := parseLine(badlyFormattedLine) _, _, err := parseLine(badlyFormattedLine, noopPresets)
if err == nil { if err == nil {
t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine) t.Errorf("Expected \"%v\" to return error, but it didn't", badlyFormattedLine)
} }
@ -348,7 +426,7 @@ func TestWrite(t *testing.T) {
//but single quotes are left alone //but single quotes are left alone
writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`) writeAndCompare(`key=va'lu'e`, `key="va'lu'e"`)
// newlines, backslashes, and some other special chars are escaped // newlines, backslashes, and some other special chars are escaped
writeAndCompare(`foo="$ba\n\r\\r!"`, `foo="\$ba\n\r\\r\!"`) writeAndCompare(`foo="\n\r\\r!"`, `foo="\n\r\\r\!"`)
// lines should be sorted // lines should be sorted
writeAndCompare("foo=bar\nbaz=buzz", "baz=\"buzz\"\nfoo=\"bar\"") writeAndCompare("foo=bar\nbaz=buzz", "baz=\"buzz\"\nfoo=\"bar\"")