cli/compose/template: use lazyregexp to compile regexes on first use

This package needed an (internal) interface to abstract the lazy-regexp.
For this, I split the implementation from the exported implementation; this
also revealed that some functions are not used (at least not in our code
base), and we could consider deprecating these.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
This commit is contained in:
Sebastiaan van Stijn 2025-04-10 11:58:16 +02:00
parent 0b0fc106dc
commit ced66f22d6
No known key found for this signature in database
GPG Key ID: 76698F39D527CE8C
2 changed files with 32 additions and 10 deletions

View File

@ -7,6 +7,8 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"github.com/docker/cli/internal/lazyregexp"
) )
const ( const (
@ -14,11 +16,21 @@ const (
subst = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?" subst = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?"
) )
var defaultPattern = regexp.MustCompile(fmt.Sprintf( var defaultPattern = lazyregexp.New(fmt.Sprintf(
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))", "%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
delimiter, delimiter, subst, subst, 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 // DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli
var DefaultSubstituteFuncs = []SubstituteFunc{ var DefaultSubstituteFuncs = []SubstituteFunc{
softDefault, softDefault,
@ -51,10 +63,16 @@ type SubstituteFunc func(string, Mapping) (string, bool, error)
// SubstituteWith substitutes variables in the string with their values. // SubstituteWith substitutes variables in the string with their values.
// It accepts additional substitute function. // It accepts additional substitute function.
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) { 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 var err error
result := pattern.ReplaceAllStringFunc(template, func(substring string) string { result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
matches := pattern.FindStringSubmatch(substring) matches := pattern.FindStringSubmatch(substring)
groups := matchGroups(matches, pattern) groups := matchGroups(matches, defaultPattern)
if escaped := groups["escaped"]; escaped != "" { if escaped := groups["escaped"]; escaped != "" {
return 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 // Substitute variables in the string with their values
func Substitute(template string, mapping Mapping) (string, error) { 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 // ExtractVariables returns a map of all the variables defined in the specified
// composefile (dict representation) and their default value if any. // composefile (dict representation) and their default value if any.
func ExtractVariables(configDict map[string]any, pattern *regexp.Regexp) map[string]string { 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 { if pattern == nil {
pattern = defaultPattern pattern = defaultPattern
} }
return recurseExtract(configDict, pattern) 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{} m := map[string]string{}
switch val := value.(type) { switch val := value.(type) {
@ -141,7 +163,7 @@ type extractedValue struct {
value string value string
} }
func extractVariable(value any, pattern *regexp.Regexp) ([]extractedValue, bool) { func extractVariable(value any, pattern regexper) ([]extractedValue, bool) {
sValue, ok := value.(string) sValue, ok := value.(string)
if !ok { if !ok {
return []extractedValue{}, false return []extractedValue{}, false
@ -227,7 +249,7 @@ func withRequired(substitution string, mapping Mapping, sep string, valid func(s
return value, true, nil 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) groups := make(map[string]string)
for i, name := range pattern.SubexpNames()[1:] { for i, name := range pattern.SubexpNames()[1:] {
groups[name] = matches[i+1] groups[name] = matches[i+1]

View File

@ -169,15 +169,15 @@ func TestSubstituteWithCustomFunc(t *testing.T) {
return value, true, nil 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.NilError(t, err)
assert.Check(t, is.Equal("ok first", result)) 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.NilError(t, err)
assert.Check(t, is.Equal("ok ", result)) 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")) assert.Check(t, is.ErrorContains(err, "required variable"))
} }
@ -278,7 +278,7 @@ func TestExtractVariables(t *testing.T) {
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { 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)) assert.Check(t, is.DeepEqual(actual, tc.expected))
}) })
} }