Skip to content

Commit 9308df1

Browse files
authored
Merge pull request #276 from compose-spec/fix-default-values
Fix environment variable expansion
2 parents c45b40b + 3844ce4 commit 9308df1

File tree

6 files changed

+189
-37
lines changed

6 files changed

+189
-37
lines changed

ci/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ FROM golang:1.18
1616

1717
WORKDIR /go/src
1818

19-
ARG GOLANGCILINT_VERSION=v1.44.1
19+
ARG GOLANGCILINT_VERSION=v1.46.2
2020
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin ${GOLANGCILINT_VERSION}
2121
RUN go install github.com/kunalkushwaha/ltag@latest && rm -rf /go/src/github.com/kunalkushwaha
2222

dotenv/godotenv.go

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"sort"
2525
"strconv"
2626
"strings"
27+
28+
"github.com/compose-spec/compose-go/template"
2729
)
2830

2931
const doubleQuoteSpecialChars = "\\\n\r\"!$`"
@@ -322,42 +324,24 @@ func parseValue(value string, envMap map[string]string, lookupFn LookupFn) strin
322324
}
323325

324326
if singleQuotes == nil {
325-
value = expandVariables(value, envMap, lookupFn)
327+
value, _ = expandVariables(value, envMap, lookupFn)
326328
}
327329
}
328330

329331
return value
330332
}
331333

332-
var expandVarRegex = regexp.MustCompile(`(\\)?(\$)(\()?\{?([A-Z0-9_]+)?\}?`)
333-
334-
func expandVariables(v string, envMap map[string]string, lookupFn LookupFn) string {
335-
return expandVarRegex.ReplaceAllStringFunc(v, func(s string) string {
336-
submatch := expandVarRegex.FindStringSubmatch(s)
337-
338-
if submatch == nil {
339-
return s
334+
func expandVariables(value string, envMap map[string]string, lookupFn LookupFn) (string, error) {
335+
retVal, err := template.Substitute(value, func(k string) (string, bool) {
336+
if v, ok := envMap[k]; ok {
337+
return v, ok
340338
}
341-
if submatch[1] == "\\" || submatch[2] == "(" {
342-
return submatch[0][1:]
343-
} else if submatch[4] != "" {
344-
// first check if we have defined this already earlier
345-
if envMap[submatch[4]] != "" {
346-
return envMap[submatch[4]]
347-
}
348-
if lookupFn == nil {
349-
return ""
350-
}
351-
// if we have not defined it, check the lookup function provided
352-
// by the user
353-
s2, ok := lookupFn(submatch[4])
354-
if ok {
355-
return s2
356-
}
357-
return ""
358-
}
359-
return s
339+
return lookupFn(k)
360340
})
341+
if err != nil {
342+
return value, err
343+
}
344+
return retVal, nil
361345
}
362346

363347
func doubleQuoteEscape(line string) string {

dotenv/godotenv_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -257,17 +257,17 @@ func TestExpanding(t *testing.T) {
257257
map[string]string{"BAR": "quote $FOO"},
258258
},
259259
{
260-
"does not expand escaped variables",
260+
"does not expand escaped variables 1",
261261
`FOO="foo\$BAR"`,
262262
map[string]string{"FOO": "foo$BAR"},
263263
},
264264
{
265-
"does not expand escaped variables",
265+
"does not expand escaped variables 2",
266266
`FOO="foo\${BAR}"`,
267267
map[string]string{"FOO": "foo${BAR}"},
268268
},
269269
{
270-
"does not expand escaped variables",
270+
"does not expand escaped variables 3",
271271
"FOO=test\nBAR=\"foo\\${FOO} ${FOO}\"",
272272
map[string]string{"FOO": "test", "BAR": "foo${FOO} test"},
273273
},
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package dotenv
2+
3+
import (
4+
"testing"
5+
6+
"github.com/compose-spec/compose-go/template"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
var envMap = map[string]string{
11+
// UNSET_VAR: <Cannot be here :D>
12+
"EMPTY_VAR": "",
13+
"TEST_VAR": "Test Value",
14+
}
15+
16+
var notFoundLookup = func(s string) (string, bool) {
17+
return "", false
18+
}
19+
20+
func TestExpandIfEmptyOrUnset(t *testing.T) {
21+
templateResults := []struct {
22+
name string
23+
input string
24+
result string
25+
}{
26+
{
27+
"Expand if empty or unset: UNSET_VAR",
28+
"RESULT=${UNSET_VAR:-Default Value}",
29+
"RESULT=Default Value",
30+
},
31+
{
32+
"Expand if empty or unset: EMPTY_VAR",
33+
"RESULT=${EMPTY_VAR:-Default Value}",
34+
"RESULT=Default Value",
35+
},
36+
{
37+
"Expand if empty or unset: TEST_VAR",
38+
"RESULT=${TEST_VAR:-Default Value}",
39+
"RESULT=Test Value",
40+
},
41+
}
42+
43+
for _, expected := range templateResults {
44+
t.Run(expected.name, func(t *testing.T) {
45+
result, err := expandVariables(expected.input, envMap, notFoundLookup)
46+
assert.Nil(t, err)
47+
assert.Equal(t, result, expected.result)
48+
})
49+
}
50+
}
51+
52+
func TestExpandIfUnset(t *testing.T) {
53+
templateResults := []struct {
54+
name string
55+
input string
56+
result string
57+
}{
58+
{
59+
"Expand if unset: UNSET_VAR",
60+
"RESULT=${UNSET_VAR-Default Value}",
61+
"RESULT=Default Value",
62+
},
63+
{
64+
"Expand if unset: EMPTY_VAR",
65+
"RESULT=${EMPTY_VAR-Default Value}",
66+
"RESULT=",
67+
},
68+
{
69+
"Expand if unset: TEST_VAR",
70+
"RESULT=${TEST_VAR-Default Value}",
71+
"RESULT=Test Value",
72+
},
73+
}
74+
75+
for _, expected := range templateResults {
76+
t.Run(expected.name, func(t *testing.T) {
77+
result, err := expandVariables(expected.input, envMap, notFoundLookup)
78+
assert.Nil(t, err)
79+
assert.Equal(t, result, expected.result)
80+
})
81+
}
82+
}
83+
84+
func TestErrorIfEmptyOrUnset(t *testing.T) {
85+
templateResults := []struct {
86+
name string
87+
input string
88+
result string
89+
err error
90+
}{
91+
{
92+
"Error empty or unset: UNSET_VAR",
93+
"RESULT=${UNSET_VAR:?Test error}",
94+
"RESULT=${UNSET_VAR:?Test error}",
95+
&template.InvalidTemplateError{Template: "required variable UNSET_VAR is missing a value: Test error"},
96+
},
97+
{
98+
"Error empty or unset: EMPTY_VAR",
99+
"RESULT=${EMPTY_VAR:?Test error}",
100+
"RESULT=${EMPTY_VAR:?Test error}",
101+
&template.InvalidTemplateError{Template: "required variable EMPTY_VAR is missing a value: Test error"},
102+
},
103+
{
104+
"Error empty or unset: TEST_VAR",
105+
"RESULT=${TEST_VAR:?Default Value}",
106+
"RESULT=Test Value",
107+
nil,
108+
},
109+
}
110+
111+
for _, expected := range templateResults {
112+
t.Run(expected.name, func(t *testing.T) {
113+
result, err := expandVariables(expected.input, envMap, notFoundLookup)
114+
assert.Equal(t, expected.err, err)
115+
assert.Equal(t, expected.result, result)
116+
})
117+
}
118+
}
119+
120+
func TestErrorIfUnset(t *testing.T) {
121+
templateResults := []struct {
122+
name string
123+
input string
124+
result string
125+
err error
126+
}{
127+
{
128+
"Error on unset: UNSET_VAR",
129+
"RESULT=${UNSET_VAR?Test error}",
130+
"RESULT=${UNSET_VAR?Test error}",
131+
&template.InvalidTemplateError{Template: "required variable UNSET_VAR is missing a value: Test error"},
132+
},
133+
{
134+
"Error on unset: EMPTY_VAR",
135+
"RESULT=${EMPTY_VAR?Test error}",
136+
"RESULT=",
137+
nil,
138+
},
139+
{
140+
"Error on unset: TEST_VAR",
141+
"RESULT=${TEST_VAR?Default Value}",
142+
"RESULT=Test Value",
143+
nil,
144+
},
145+
}
146+
147+
for _, expected := range templateResults {
148+
t.Run(expected.name, func(t *testing.T) {
149+
result, err := expandVariables(expected.input, envMap, notFoundLookup)
150+
assert.Equal(t, expected.err, err)
151+
assert.Equal(t, expected.result, result)
152+
})
153+
}
154+
}

dotenv/parser.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ loop:
129129
}
130130

131131
// extractVarValue extracts variable value and returns rest of slice
132-
func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (value string, rest []byte, err error) {
132+
func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (string, []byte, error) {
133133
quote, isQuoted := hasQuotePrefix(src)
134134
if !isQuoted {
135135
// unquoted value - read until new line
@@ -138,13 +138,17 @@ func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (v
138138
if end < 0 {
139139
value := strings.Split(string(src), "#")[0] // Remove inline comments on unquoted lines
140140
value = strings.TrimRightFunc(value, unicode.IsSpace)
141-
return expandVariables(value, envMap, lookupFn), nil, nil
141+
142+
retVal, err := expandVariables(value, envMap, lookupFn)
143+
return retVal, nil, err
142144
}
143145

144146
value := strings.Split(string(src[0:end]), "#")[0]
145147
value = strings.TrimRightFunc(value, unicode.IsSpace)
146148
rest = src[end:]
147-
return expandVariables(value, envMap, lookupFn), rest, nil
149+
150+
retVal, err := expandVariables(value, envMap, lookupFn)
151+
return retVal, rest, err
148152
}
149153

150154
// lookup quoted string terminator
@@ -160,11 +164,16 @@ func extractVarValue(src []byte, envMap map[string]string, lookupFn LookupFn) (v
160164

161165
// trim quotes
162166
trimFunc := isCharFunc(rune(quote))
163-
value = string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc))
167+
value := string(bytes.TrimLeftFunc(bytes.TrimRightFunc(src[0:i], trimFunc), trimFunc))
164168
if quote == prefixDoubleQuote {
165169
// unescape newlines for double quote (this is compat feature)
166170
// and expand environment variables
167-
value = expandVariables(expandEscapes(value), envMap, lookupFn)
171+
172+
retVal, err := expandVariables(expandEscapes(value), envMap, lookupFn)
173+
if err != nil {
174+
return "", nil, err
175+
}
176+
value = retVal
168177
}
169178

170179
return value, src[i+1:], nil
@@ -187,6 +196,8 @@ func expandEscapes(str string) string {
187196
return "\n"
188197
case "r":
189198
return "\r"
199+
case "$":
200+
return "$$"
190201
default:
191202
return match
192203
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ require (
1313
github.com/opencontainers/go-digest v1.0.0
1414
github.com/pkg/errors v0.9.1
1515
github.com/sirupsen/logrus v1.8.1
16+
github.com/stretchr/testify v1.5.1
1617
github.com/xeipuuv/gojsonschema v1.2.0
1718
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
1819
gopkg.in/yaml.v2 v2.4.0
1920
gotest.tools/v3 v3.3.0
2021
)
2122

2223
require (
24+
github.com/davecgh/go-spew v1.1.1 // indirect
25+
github.com/pmezard/go-difflib v1.0.0 // indirect
2326
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
2427
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
2528
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 // indirect

0 commit comments

Comments
 (0)