Skip to content

Commit 8e87de4

Browse files
authored
Merge pull request #76 from MarilynFranklin/support-nested-variable-expansion
Support nested variable expansion
2 parents e9fed32 + 8007e3c commit 8e87de4

File tree

2 files changed

+82
-10
lines changed

2 files changed

+82
-10
lines changed

template/template.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626

2727
var delimiter = "\\$"
2828
var substitutionNamed = "[_a-z][_a-z0-9]*"
29-
var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?"
29+
var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-?](.*}|[^}]*))?"
3030

3131
var patternString = fmt.Sprintf(
3232
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
@@ -35,14 +35,6 @@ var patternString = fmt.Sprintf(
3535

3636
var defaultPattern = regexp.MustCompile(patternString)
3737

38-
// DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli
39-
var DefaultSubstituteFuncs = []SubstituteFunc{
40-
softDefault,
41-
hardDefault,
42-
requiredNonEmpty,
43-
required,
44-
}
45-
4638
// InvalidTemplateError is returned when a variable template is not in a valid
4739
// format
4840
type InvalidTemplateError struct {
@@ -67,6 +59,14 @@ type SubstituteFunc func(string, Mapping) (string, bool, error)
6759
// SubstituteWith substitute variables in the string with their values.
6860
// It accepts additional substitute function.
6961
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
62+
if len(subsFuncs) == 0 {
63+
subsFuncs = []SubstituteFunc{
64+
softDefault,
65+
hardDefault,
66+
requiredNonEmpty,
67+
required,
68+
}
69+
}
7070
var err error
7171
result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
7272
matches := pattern.FindStringSubmatch(substring)
@@ -116,7 +116,7 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su
116116

117117
// Substitute variables in the string with their values
118118
func Substitute(template string, mapping Mapping) (string, error) {
119-
return SubstituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...)
119+
return SubstituteWith(template, mapping, defaultPattern)
120120
}
121121

122122
// ExtractVariables returns a map of all the variables defined in the specified
@@ -215,6 +215,10 @@ func softDefault(substitution string, mapping Mapping) (string, bool, error) {
215215
return "", false, nil
216216
}
217217
name, defaultValue := partition(substitution, sep)
218+
defaultValue, err := Substitute(defaultValue, mapping)
219+
if err != nil {
220+
return "", false, err
221+
}
218222
value, ok := mapping(name)
219223
if !ok || value == "" {
220224
return defaultValue, true, nil
@@ -229,6 +233,10 @@ func hardDefault(substitution string, mapping Mapping) (string, bool, error) {
229233
return "", false, nil
230234
}
231235
name, defaultValue := partition(substitution, sep)
236+
defaultValue, err := Substitute(defaultValue, mapping)
237+
if err != nil {
238+
return "", false, err
239+
}
232240
value, ok := mapping(name)
233241
if !ok {
234242
return defaultValue, true, nil
@@ -249,6 +257,10 @@ func withRequired(substitution string, mapping Mapping, sep string, valid func(s
249257
return "", false, nil
250258
}
251259
name, errorMessage := partition(substitution, sep)
260+
errorMessage, err := Substitute(errorMessage, mapping)
261+
if err != nil {
262+
return "", false, err
263+
}
252264
value, ok := mapping(name)
253265
if !ok || !valid(value) {
254266
return "", true, &InvalidTemplateError{

template/template_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,44 @@ func TestNonAlphanumericDefault(t *testing.T) {
119119
assert.Check(t, is.Equal("ok /non:-alphanumeric", result))
120120
}
121121

122+
func TestDefaultsWithNestedExpansion(t *testing.T) {
123+
testCases := []struct {
124+
template string
125+
expected string
126+
}{
127+
{
128+
template: "ok ${UNSET_VAR-$FOO}",
129+
expected: "ok first",
130+
},
131+
{
132+
template: "ok ${UNSET_VAR-${FOO}}",
133+
expected: "ok first",
134+
},
135+
{
136+
template: "ok ${UNSET_VAR-${FOO} ${FOO}}",
137+
expected: "ok first first",
138+
},
139+
{
140+
template: "ok ${BAR:-$FOO}",
141+
expected: "ok first",
142+
},
143+
{
144+
template: "ok ${BAR:-${FOO}}",
145+
expected: "ok first",
146+
},
147+
{
148+
template: "ok ${BAR:-${FOO} ${FOO}}",
149+
expected: "ok first first",
150+
},
151+
}
152+
153+
for _, tc := range testCases {
154+
result, err := Substitute(tc.template, defaultMapping)
155+
assert.NilError(t, err)
156+
assert.Check(t, is.Equal(tc.expected, result))
157+
}
158+
}
159+
122160
func TestMandatoryVariableErrors(t *testing.T) {
123161
testCases := []struct {
124162
template string
@@ -153,6 +191,28 @@ func TestMandatoryVariableErrors(t *testing.T) {
153191
}
154192
}
155193

194+
func TestMandatoryVariableErrorsWithNestedExpansion(t *testing.T) {
195+
testCases := []struct {
196+
template string
197+
expectedError string
198+
}{
199+
{
200+
template: "not ok ${UNSET_VAR:?Mandatory Variable ${FOO}}",
201+
expectedError: "required variable UNSET_VAR is missing a value: Mandatory Variable first",
202+
},
203+
{
204+
template: "not ok ${UNSET_VAR?Mandatory Variable ${FOO}}",
205+
expectedError: "required variable UNSET_VAR is missing a value: Mandatory Variable first",
206+
},
207+
}
208+
209+
for _, tc := range testCases {
210+
_, err := Substitute(tc.template, defaultMapping)
211+
assert.ErrorContains(t, err, tc.expectedError)
212+
assert.ErrorType(t, err, reflect.TypeOf(&InvalidTemplateError{}))
213+
}
214+
}
215+
156216
func TestDefaultsForMandatoryVariables(t *testing.T) {
157217
testCases := []struct {
158218
template string

0 commit comments

Comments
 (0)