From 8007e3cabb5737d663af7db5e775ec430f0934a0 Mon Sep 17 00:00:00 2001 From: MarilynFranklin Date: Sun, 27 Sep 2020 19:21:46 -0500 Subject: [PATCH] Support nested variable expansion Signed-off-by: MarilynFranklin --- template/template.go | 32 ++++++++++++++------- template/template_test.go | 60 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/template/template.go b/template/template.go index 26e9b36a..ecb7358b 100644 --- a/template/template.go +++ b/template/template.go @@ -26,7 +26,7 @@ import ( var delimiter = "\\$" var substitutionNamed = "[_a-z][_a-z0-9]*" -var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?" +var substitutionBraced = "[_a-z][_a-z0-9]*(?::?[-?](.*}|[^}]*))?" var patternString = fmt.Sprintf( "%s(?i:(?P%s)|(?P%s)|{(?P%s)}|(?P))", @@ -35,14 +35,6 @@ var patternString = fmt.Sprintf( var defaultPattern = regexp.MustCompile(patternString) -// DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli -var DefaultSubstituteFuncs = []SubstituteFunc{ - softDefault, - hardDefault, - requiredNonEmpty, - required, -} - // InvalidTemplateError is returned when a variable template is not in a valid // format type InvalidTemplateError struct { @@ -67,6 +59,14 @@ type SubstituteFunc func(string, Mapping) (string, bool, error) // SubstituteWith substitute variables in the string with their values. // It accepts additional substitute function. func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) { + if len(subsFuncs) == 0 { + subsFuncs = []SubstituteFunc{ + softDefault, + hardDefault, + requiredNonEmpty, + required, + } + } var err error result := pattern.ReplaceAllStringFunc(template, func(substring string) string { matches := pattern.FindStringSubmatch(substring) @@ -116,7 +116,7 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su // Substitute variables in the string with their values func Substitute(template string, mapping Mapping) (string, error) { - return SubstituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...) + return SubstituteWith(template, mapping, defaultPattern) } // 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) { return "", false, nil } name, defaultValue := partition(substitution, sep) + defaultValue, err := Substitute(defaultValue, mapping) + if err != nil { + return "", false, err + } value, ok := mapping(name) if !ok || value == "" { return defaultValue, true, nil @@ -229,6 +233,10 @@ func hardDefault(substitution string, mapping Mapping) (string, bool, error) { return "", false, nil } name, defaultValue := partition(substitution, sep) + defaultValue, err := Substitute(defaultValue, mapping) + if err != nil { + return "", false, err + } value, ok := mapping(name) if !ok { return defaultValue, true, nil @@ -249,6 +257,10 @@ func withRequired(substitution string, mapping Mapping, sep string, valid func(s return "", false, nil } name, errorMessage := partition(substitution, sep) + errorMessage, err := Substitute(errorMessage, mapping) + if err != nil { + return "", false, err + } value, ok := mapping(name) if !ok || !valid(value) { return "", true, &InvalidTemplateError{ diff --git a/template/template_test.go b/template/template_test.go index 3c6b61e8..ef8f1073 100644 --- a/template/template_test.go +++ b/template/template_test.go @@ -119,6 +119,44 @@ func TestNonAlphanumericDefault(t *testing.T) { assert.Check(t, is.Equal("ok /non:-alphanumeric", result)) } +func TestDefaultsWithNestedExpansion(t *testing.T) { + testCases := []struct { + template string + expected string + }{ + { + template: "ok ${UNSET_VAR-$FOO}", + expected: "ok first", + }, + { + template: "ok ${UNSET_VAR-${FOO}}", + expected: "ok first", + }, + { + template: "ok ${UNSET_VAR-${FOO} ${FOO}}", + expected: "ok first first", + }, + { + template: "ok ${BAR:-$FOO}", + expected: "ok first", + }, + { + template: "ok ${BAR:-${FOO}}", + expected: "ok first", + }, + { + template: "ok ${BAR:-${FOO} ${FOO}}", + expected: "ok first first", + }, + } + + for _, tc := range testCases { + result, err := Substitute(tc.template, defaultMapping) + assert.NilError(t, err) + assert.Check(t, is.Equal(tc.expected, result)) + } +} + func TestMandatoryVariableErrors(t *testing.T) { testCases := []struct { template string @@ -153,6 +191,28 @@ func TestMandatoryVariableErrors(t *testing.T) { } } +func TestMandatoryVariableErrorsWithNestedExpansion(t *testing.T) { + testCases := []struct { + template string + expectedError string + }{ + { + template: "not ok ${UNSET_VAR:?Mandatory Variable ${FOO}}", + expectedError: "required variable UNSET_VAR is missing a value: Mandatory Variable first", + }, + { + template: "not ok ${UNSET_VAR?Mandatory Variable ${FOO}}", + expectedError: "required variable UNSET_VAR is missing a value: Mandatory Variable first", + }, + } + + for _, tc := range testCases { + _, err := Substitute(tc.template, defaultMapping) + assert.ErrorContains(t, err, tc.expectedError) + assert.ErrorType(t, err, reflect.TypeOf(&InvalidTemplateError{})) + } +} + func TestDefaultsForMandatoryVariables(t *testing.T) { testCases := []struct { template string