Skip to content

Commit 23d116a

Browse files
authored
Merge pull request #58 from lucastetreault/master
feat(Expand Variables): Custom variable expansion instead of Go's os.Expand
2 parents 1709ab1 + 2d8b3aa commit 23d116a

File tree

2 files changed

+104
-17
lines changed

2 files changed

+104
-17
lines changed

godotenv.go

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -270,12 +270,19 @@ func parseValue(value string, envMap map[string]string) string {
270270

271271
// check if we've got quoted values or possible escapes
272272
if len(value) > 1 {
273-
first := string(value[0:1])
274-
last := string(value[len(value)-1:])
275-
if first == last && strings.ContainsAny(first, `"'`) {
273+
rs := regexp.MustCompile(`\A'(.*)'\z`)
274+
singleQuotes := rs.FindStringSubmatch(value)
275+
276+
rd := regexp.MustCompile(`\A"(.*)"\z`)
277+
doubleQuotes := rd.FindStringSubmatch(value)
278+
279+
if singleQuotes != nil || doubleQuotes != nil {
276280
// pull the quotes off the edges
277281
value = value[1 : len(value)-1]
278-
// handle escapes
282+
}
283+
284+
if doubleQuotes != nil {
285+
// expand newlines
279286
escapeRegex := regexp.MustCompile(`\\.`)
280287
value = escapeRegex.ReplaceAllStringFunc(value, func(match string) string {
281288
c := strings.TrimPrefix(match, `\`)
@@ -285,23 +292,38 @@ func parseValue(value string, envMap map[string]string) string {
285292
case "r":
286293
return "\r"
287294
default:
288-
return c
295+
return match
289296
}
290297
})
298+
// unescape characters
299+
e := regexp.MustCompile(`\\([^$])`)
300+
value = e.ReplaceAllString(value, "$1")
301+
}
302+
303+
if singleQuotes == nil {
304+
value = expandVariables(value, envMap)
291305
}
292306
}
293307

294-
// expand variables
295-
value = os.Expand(value, func(key string) string {
296-
if val, ok := envMap[key]; ok {
297-
return val
308+
return value
309+
}
310+
311+
func expandVariables(v string, m map[string]string) string {
312+
r := regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
313+
314+
return r.ReplaceAllStringFunc(v, func(s string) string {
315+
submatch := r.FindStringSubmatch(s)
316+
317+
if submatch == nil {
318+
return s
298319
}
299-
if val, ok := os.LookupEnv(key); ok {
300-
return val
320+
if submatch[1] == "\\" || submatch[2] == "(" {
321+
return submatch[0][1:]
322+
} else if submatch[4] != "" {
323+
return m[submatch[4]]
301324
}
302-
return ""
325+
return s
303326
})
304-
return value
305327
}
306328

307329
func isIgnoredLine(line string) bool {

godotenv_test.go

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"reflect"
88
"testing"
9+
"strings"
910
)
1011

1112
var noopPresets = make(map[string]string)
@@ -161,7 +162,7 @@ func TestLoadExportedEnv(t *testing.T) {
161162
envFileName := "fixtures/exported.env"
162163
expectedValues := map[string]string{
163164
"OPTION_A": "2",
164-
"OPTION_B": "\n",
165+
"OPTION_B": "\\n",
165166
}
166167

167168
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
@@ -182,7 +183,7 @@ func TestLoadQuotedEnv(t *testing.T) {
182183
"OPTION_A": "1",
183184
"OPTION_B": "2",
184185
"OPTION_C": "",
185-
"OPTION_D": "\n",
186+
"OPTION_D": "\\n",
186187
"OPTION_E": "1",
187188
"OPTION_F": "2",
188189
"OPTION_G": "",
@@ -193,7 +194,7 @@ func TestLoadQuotedEnv(t *testing.T) {
193194
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
194195
}
195196

196-
func TestSubstituitions(t *testing.T) {
197+
func TestSubstitutions(t *testing.T) {
197198
envFileName := "fixtures/substitutions.env"
198199
expectedValues := map[string]string{
199200
"OPTION_A": "1",
@@ -206,6 +207,70 @@ func TestSubstituitions(t *testing.T) {
206207
loadEnvAndCompareValues(t, Load, envFileName, expectedValues, noopPresets)
207208
}
208209

210+
func TestExpanding(t *testing.T) {
211+
tests := []struct {
212+
name string
213+
input string
214+
expected map[string]string
215+
}{
216+
{
217+
"expands variables found in values",
218+
"FOO=test\nBAR=$FOO",
219+
map[string]string{"FOO": "test", "BAR": "test"},
220+
},
221+
{
222+
"parses variables wrapped in brackets",
223+
"FOO=test\nBAR=${FOO}bar",
224+
map[string]string{"FOO": "test", "BAR": "testbar"},
225+
},
226+
{
227+
"expands undefined variables to an empty string",
228+
"BAR=$FOO",
229+
map[string]string{"BAR": ""},
230+
},
231+
{
232+
"expands variables in double quoted strings",
233+
"FOO=test\nBAR=\"quote $FOO\"",
234+
map[string]string{"FOO": "test", "BAR": "quote test"},
235+
},
236+
{
237+
"does not expand variables in single quoted strings",
238+
"BAR='quote $FOO'",
239+
map[string]string{"BAR": "quote $FOO"},
240+
},
241+
{
242+
"does not expand escaped variables",
243+
`FOO="foo\$BAR"`,
244+
map[string]string{"FOO": "foo$BAR"},
245+
},
246+
{
247+
"does not expand escaped variables",
248+
`FOO="foo\${BAR}"`,
249+
map[string]string{"FOO": "foo${BAR}"},
250+
},
251+
{
252+
"does not expand escaped variables",
253+
"FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"",
254+
map[string]string{"FOO": "test", "BAR": "foo${FOO} test"},
255+
},
256+
}
257+
258+
for _, tt := range tests {
259+
t.Run(tt.name, func(t *testing.T) {
260+
env, err := Parse(strings.NewReader(tt.input))
261+
if err != nil {
262+
t.Errorf("Error: %s", err.Error())
263+
}
264+
for k, v := range tt.expected {
265+
if strings.Compare(env[k], v) != 0 {
266+
t.Errorf("Expected: %s, Actual: %s", v, env[k])
267+
}
268+
}
269+
})
270+
}
271+
272+
}
273+
209274
func TestActualEnvVarsAreLeftAlone(t *testing.T) {
210275
os.Clearenv()
211276
os.Setenv("OPTION_A", "actualenv")
@@ -247,7 +312,7 @@ func TestParsing(t *testing.T) {
247312

248313
// parses export keyword
249314
parseAndCompare(t, "export OPTION_A=2", "OPTION_A", "2")
250-
parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\n")
315+
parseAndCompare(t, `export OPTION_B='\n'`, "OPTION_B", "\\n")
251316

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

0 commit comments

Comments
 (0)