diff --git a/cli/compose/template/template.go b/cli/compose/template/template.go index 8cfe17262d..23bcb1459e 100644 --- a/cli/compose/template/template.go +++ b/cli/compose/template/template.go @@ -7,6 +7,8 @@ import ( "fmt" "regexp" "strings" + + "github.com/docker/cli/internal/lazyregexp" ) const ( @@ -14,11 +16,21 @@ const ( subst = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?" ) -var defaultPattern = regexp.MustCompile(fmt.Sprintf( +var defaultPattern = lazyregexp.New(fmt.Sprintf( "%s(?i:(?P%s)|(?P%s)|{(?P%s)}|(?P))", delimiter, delimiter, subst, subst, )) +// regexper is an internal interface to allow passing a [lazyregexp.Regexp] +// in places where a custom ("regular") [regexp.Regexp] is accepted. It defines +// only the methods we currently use. +type regexper interface { + FindAllStringSubmatch(s string, n int) [][]string + FindStringSubmatch(s string) []string + ReplaceAllStringFunc(src string, repl func(string) string) string + SubexpNames() []string +} + // DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli var DefaultSubstituteFuncs = []SubstituteFunc{ softDefault, @@ -51,10 +63,16 @@ type SubstituteFunc func(string, Mapping) (string, bool, error) // SubstituteWith substitutes 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) { + return substituteWith(template, mapping, pattern, subsFuncs...) +} + +// SubstituteWith substitutes variables in the string with their values. +// It accepts additional substitute function. +func substituteWith(template string, mapping Mapping, pattern regexper, subsFuncs ...SubstituteFunc) (string, error) { var err error result := pattern.ReplaceAllStringFunc(template, func(substring string) string { matches := pattern.FindStringSubmatch(substring) - groups := matchGroups(matches, pattern) + groups := matchGroups(matches, defaultPattern) if escaped := groups["escaped"]; escaped != "" { return escaped } @@ -93,19 +111,23 @@ 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, DefaultSubstituteFuncs...) } // ExtractVariables returns a map of all the variables defined in the specified // composefile (dict representation) and their default value if any. func ExtractVariables(configDict map[string]any, pattern *regexp.Regexp) map[string]string { + return extractVariables(configDict, pattern) +} + +func extractVariables(configDict map[string]any, pattern regexper) map[string]string { if pattern == nil { pattern = defaultPattern } return recurseExtract(configDict, pattern) } -func recurseExtract(value any, pattern *regexp.Regexp) map[string]string { +func recurseExtract(value any, pattern regexper) map[string]string { m := map[string]string{} switch val := value.(type) { @@ -141,7 +163,7 @@ type extractedValue struct { value string } -func extractVariable(value any, pattern *regexp.Regexp) ([]extractedValue, bool) { +func extractVariable(value any, pattern regexper) ([]extractedValue, bool) { sValue, ok := value.(string) if !ok { return []extractedValue{}, false @@ -227,7 +249,7 @@ func withRequired(substitution string, mapping Mapping, sep string, valid func(s return value, true, nil } -func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string { +func matchGroups(matches []string, pattern regexper) map[string]string { groups := make(map[string]string) for i, name := range pattern.SubexpNames()[1:] { groups[name] = matches[i+1] diff --git a/cli/compose/template/template_test.go b/cli/compose/template/template_test.go index 92b95460eb..d3baa5cc93 100644 --- a/cli/compose/template/template_test.go +++ b/cli/compose/template/template_test.go @@ -169,15 +169,15 @@ func TestSubstituteWithCustomFunc(t *testing.T) { return value, true, nil } - result, err := SubstituteWith("ok ${FOO}", defaultMapping, defaultPattern, errIsMissing) + result, err := substituteWith("ok ${FOO}", defaultMapping, defaultPattern, errIsMissing) assert.NilError(t, err) assert.Check(t, is.Equal("ok first", result)) - result, err = SubstituteWith("ok ${BAR}", defaultMapping, defaultPattern, errIsMissing) + result, err = substituteWith("ok ${BAR}", defaultMapping, defaultPattern, errIsMissing) assert.NilError(t, err) assert.Check(t, is.Equal("ok ", result)) - _, err = SubstituteWith("ok ${NOTHERE}", defaultMapping, defaultPattern, errIsMissing) + _, err = substituteWith("ok ${NOTHERE}", defaultMapping, defaultPattern, errIsMissing) assert.Check(t, is.ErrorContains(err, "required variable")) } @@ -278,7 +278,7 @@ func TestExtractVariables(t *testing.T) { } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - actual := ExtractVariables(tc.dict, defaultPattern) + actual := extractVariables(tc.dict, defaultPattern) assert.Check(t, is.DeepEqual(actual, tc.expected)) }) }