Skip to content

Commit e578f15

Browse files
authored
Merge pull request #5998 from thaJeztah/lazy_regexp
use lazyregexp to compile regexes on first use
2 parents 8633197 + 4c820d3 commit e578f15

14 files changed

Lines changed: 184 additions & 34 deletions

File tree

.golangci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ linters:
77
- dupword # Detects duplicate words.
88
- durationcheck
99
- errchkjson
10+
- forbidigo
1011
- gocritic # Metalinter; detects bugs, performance, and styling issues.
1112
- gocyclo
1213
- gofumpt # Detects whether code was gofumpt-ed.
@@ -66,6 +67,11 @@ linters-settings:
6667
desc: Use github.com/google/uuid instead.
6768
- pkg: "io/ioutil"
6869
desc: The io/ioutil package has been deprecated, see https://go.dev/doc/go1.16#ioutil
70+
forbidigo:
71+
forbid:
72+
- pkg: ^regexp$
73+
p: ^regexp\.MustCompile
74+
msg: Use internal/lazyregexp.New instead.
6975
gocyclo:
7076
min-complexity: 16
7177
gosec:

cli-plugins/manager/plugin.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,14 @@ import (
88
"os"
99
"os/exec"
1010
"path/filepath"
11-
"regexp"
1211
"strings"
1312

1413
"github.com/docker/cli/cli-plugins/metadata"
14+
"github.com/docker/cli/internal/lazyregexp"
1515
"github.com/spf13/cobra"
1616
)
1717

18-
var pluginNameRe = regexp.MustCompile("^[a-z][a-z0-9]*$")
18+
var pluginNameRe = lazyregexp.New("^[a-z][a-z0-9]*$")
1919

2020
// Plugin represents a potential plugin with all it's metadata.
2121
type Plugin struct {

cli/command/container/opts.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import (
88
"path"
99
"path/filepath"
1010
"reflect"
11-
"regexp"
1211
"strconv"
1312
"strings"
1413
"time"
1514

1615
"github.com/docker/cli/cli/compose/loader"
16+
"github.com/docker/cli/internal/lazyregexp"
1717
"github.com/docker/cli/opts"
1818
"github.com/docker/docker/api/types/container"
1919
mounttypes "github.com/docker/docker/api/types/mount"
@@ -40,7 +40,7 @@ const (
4040
seccompProfileUnconfined = "unconfined"
4141
)
4242

43-
var deviceCgroupRuleRegexp = regexp.MustCompile(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
43+
var deviceCgroupRuleRegexp = lazyregexp.New(`^[acb] ([0-9]+|\*):([0-9]+|\*) [rwm]{1,3}$`)
4444

4545
// containerOptions is a data object with all the options for creating a container
4646
type containerOptions struct {

cli/command/image/build.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"io"
1111
"os"
1212
"path/filepath"
13-
"regexp"
1413
"runtime"
1514
"strings"
1615

@@ -23,6 +22,7 @@ import (
2322
"github.com/docker/cli/cli/internal/jsonstream"
2423
"github.com/docker/cli/cli/streams"
2524
"github.com/docker/cli/cli/trust"
25+
"github.com/docker/cli/internal/lazyregexp"
2626
"github.com/docker/cli/opts"
2727
"github.com/docker/docker/api"
2828
"github.com/docker/docker/api/types"
@@ -432,7 +432,7 @@ func validateTag(rawRepo string) (string, error) {
432432
return rawRepo, nil
433433
}
434434

435-
var dockerfileFromLinePattern = regexp.MustCompile(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`)
435+
var dockerfileFromLinePattern = lazyregexp.New(`(?i)^[\s]*FROM[ \f\r\t\v]+(?P<image>[^ \f\r\t\v\n#]+)`)
436436

437437
// resolvedTag records the repository, tag, and resolved digest reference
438438
// from a Dockerfile rewrite.

cli/command/system/info.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"errors"
99
"fmt"
1010
"io"
11-
"regexp"
1211
"sort"
1312
"strings"
1413

@@ -19,6 +18,7 @@ import (
1918
"github.com/docker/cli/cli/command/formatter"
2019
"github.com/docker/cli/cli/debug"
2120
flagsHelper "github.com/docker/cli/cli/flags"
21+
"github.com/docker/cli/internal/lazyregexp"
2222
"github.com/docker/cli/templates"
2323
"github.com/docker/docker/api/types/swarm"
2424
"github.com/docker/docker/api/types/system"
@@ -142,7 +142,7 @@ func addServerInfo(ctx context.Context, dockerCli command.Cli, format string, in
142142

143143
// placeHolders does a rudimentary match for possible placeholders in a
144144
// template, matching a '.', followed by an letter (a-z/A-Z).
145-
var placeHolders = regexp.MustCompile(`\.[a-zA-Z]`)
145+
var placeHolders = lazyregexp.New(`\.[a-zA-Z]`)
146146

147147
// needsServerInfo detects if the given template uses any server information.
148148
// If only client-side information is used in the template, we can skip

cli/command/trust/key_generate.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@ import (
55
"fmt"
66
"os"
77
"path/filepath"
8-
"regexp"
98
"strings"
109

1110
"github.com/docker/cli/cli"
1211
"github.com/docker/cli/cli/command"
1312
"github.com/docker/cli/cli/trust"
13+
"github.com/docker/cli/internal/lazyregexp"
1414
"github.com/pkg/errors"
1515
"github.com/spf13/cobra"
1616
"github.com/theupdateframework/notary"
@@ -41,7 +41,7 @@ func newKeyGenerateCommand(dockerCli command.Streams) *cobra.Command {
4141
}
4242

4343
// key names can use lowercase alphanumeric + _ + - characters
44-
var validKeyName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
44+
var validKeyName = lazyregexp.New(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
4545

4646
// validate that all of the key names are unique and are alphanumeric + _ + -
4747
// and that we do not already have public key files in the target dir on disk

cli/command/trust/signer_add.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import (
66
"io"
77
"os"
88
"path"
9-
"regexp"
109
"strings"
1110

1211
"github.com/docker/cli/cli"
1312
"github.com/docker/cli/cli/command"
1413
"github.com/docker/cli/cli/command/image"
1514
"github.com/docker/cli/cli/trust"
15+
"github.com/docker/cli/internal/lazyregexp"
1616
"github.com/docker/cli/opts"
1717
"github.com/pkg/errors"
1818
"github.com/spf13/cobra"
@@ -45,7 +45,7 @@ func newSignerAddCommand(dockerCLI command.Cli) *cobra.Command {
4545
return cmd
4646
}
4747

48-
var validSignerName = regexp.MustCompile(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
48+
var validSignerName = lazyregexp.New(`^[a-z0-9][a-z0-9\_\-]*$`).MatchString
4949

5050
func addSigner(ctx context.Context, dockerCLI command.Cli, options signerAddOptions) error {
5151
signerName := options.signer

cli/compose/template/template.go

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,30 @@ import (
77
"fmt"
88
"regexp"
99
"strings"
10+
11+
"github.com/docker/cli/internal/lazyregexp"
1012
)
1113

1214
const (
1315
delimiter = "\\$"
1416
subst = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?"
1517
)
1618

17-
var defaultPattern = regexp.MustCompile(fmt.Sprintf(
19+
var defaultPattern = lazyregexp.New(fmt.Sprintf(
1820
"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
1921
delimiter, delimiter, subst, subst,
2022
))
2123

24+
// regexper is an internal interface to allow passing a [lazyregexp.Regexp]
25+
// in places where a custom ("regular") [regexp.Regexp] is accepted. It defines
26+
// only the methods we currently use.
27+
type regexper interface {
28+
FindAllStringSubmatch(s string, n int) [][]string
29+
FindStringSubmatch(s string) []string
30+
ReplaceAllStringFunc(src string, repl func(string) string) string
31+
SubexpNames() []string
32+
}
33+
2234
// DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli
2335
var DefaultSubstituteFuncs = []SubstituteFunc{
2436
softDefault,
@@ -51,10 +63,16 @@ type SubstituteFunc func(string, Mapping) (string, bool, error)
5163
// SubstituteWith substitutes variables in the string with their values.
5264
// It accepts additional substitute function.
5365
func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
66+
return substituteWith(template, mapping, pattern, subsFuncs...)
67+
}
68+
69+
// SubstituteWith substitutes variables in the string with their values.
70+
// It accepts additional substitute function.
71+
func substituteWith(template string, mapping Mapping, pattern regexper, subsFuncs ...SubstituteFunc) (string, error) {
5472
var err error
5573
result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
5674
matches := pattern.FindStringSubmatch(substring)
57-
groups := matchGroups(matches, pattern)
75+
groups := matchGroups(matches, defaultPattern)
5876
if escaped := groups["escaped"]; escaped != "" {
5977
return escaped
6078
}
@@ -93,38 +111,42 @@ func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, su
93111

94112
// Substitute variables in the string with their values
95113
func Substitute(template string, mapping Mapping) (string, error) {
96-
return SubstituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...)
114+
return substituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...)
97115
}
98116

99117
// ExtractVariables returns a map of all the variables defined in the specified
100118
// composefile (dict representation) and their default value if any.
101119
func ExtractVariables(configDict map[string]any, pattern *regexp.Regexp) map[string]string {
120+
return extractVariables(configDict, pattern)
121+
}
122+
123+
func extractVariables(configDict map[string]any, pattern regexper) map[string]string {
102124
if pattern == nil {
103125
pattern = defaultPattern
104126
}
105127
return recurseExtract(configDict, pattern)
106128
}
107129

108-
func recurseExtract(value any, pattern *regexp.Regexp) map[string]string {
130+
func recurseExtract(value any, pattern regexper) map[string]string {
109131
m := map[string]string{}
110132

111-
switch value := value.(type) {
133+
switch val := value.(type) {
112134
case string:
113-
if values, is := extractVariable(value, pattern); is {
135+
if values, is := extractVariable(val, pattern); is {
114136
for _, v := range values {
115137
m[v.name] = v.value
116138
}
117139
}
118140
case map[string]any:
119-
for _, elem := range value {
141+
for _, elem := range val {
120142
submap := recurseExtract(elem, pattern)
121-
for key, value := range submap {
122-
m[key] = value
143+
for k, v := range submap {
144+
m[k] = v
123145
}
124146
}
125147

126148
case []any:
127-
for _, elem := range value {
149+
for _, elem := range val {
128150
if values, is := extractVariable(elem, pattern); is {
129151
for _, v := range values {
130152
m[v.name] = v.value
@@ -141,7 +163,7 @@ type extractedValue struct {
141163
value string
142164
}
143165

144-
func extractVariable(value any, pattern *regexp.Regexp) ([]extractedValue, bool) {
166+
func extractVariable(value any, pattern regexper) ([]extractedValue, bool) {
145167
sValue, ok := value.(string)
146168
if !ok {
147169
return []extractedValue{}, false
@@ -227,7 +249,7 @@ func withRequired(substitution string, mapping Mapping, sep string, valid func(s
227249
return value, true, nil
228250
}
229251

230-
func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string {
252+
func matchGroups(matches []string, pattern regexper) map[string]string {
231253
groups := make(map[string]string)
232254
for i, name := range pattern.SubexpNames()[1:] {
233255
groups[name] = matches[i+1]

cli/compose/template/template_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,15 +169,15 @@ func TestSubstituteWithCustomFunc(t *testing.T) {
169169
return value, true, nil
170170
}
171171

172-
result, err := SubstituteWith("ok ${FOO}", defaultMapping, defaultPattern, errIsMissing)
172+
result, err := substituteWith("ok ${FOO}", defaultMapping, defaultPattern, errIsMissing)
173173
assert.NilError(t, err)
174174
assert.Check(t, is.Equal("ok first", result))
175175

176-
result, err = SubstituteWith("ok ${BAR}", defaultMapping, defaultPattern, errIsMissing)
176+
result, err = substituteWith("ok ${BAR}", defaultMapping, defaultPattern, errIsMissing)
177177
assert.NilError(t, err)
178178
assert.Check(t, is.Equal("ok ", result))
179179

180-
_, err = SubstituteWith("ok ${NOTHERE}", defaultMapping, defaultPattern, errIsMissing)
180+
_, err = substituteWith("ok ${NOTHERE}", defaultMapping, defaultPattern, errIsMissing)
181181
assert.Check(t, is.ErrorContains(err, "required variable"))
182182
}
183183

@@ -278,7 +278,7 @@ func TestExtractVariables(t *testing.T) {
278278
}
279279
for _, tc := range testCases {
280280
t.Run(tc.name, func(t *testing.T) {
281-
actual := ExtractVariables(tc.dict, defaultPattern)
281+
actual := extractVariables(tc.dict, defaultPattern)
282282
assert.Check(t, is.DeepEqual(actual, tc.expected))
283283
})
284284
}

cli/context/store/store.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ import (
1414
"net/http"
1515
"path"
1616
"path/filepath"
17-
"regexp"
1817
"strings"
1918

19+
"github.com/docker/cli/internal/lazyregexp"
2020
"github.com/docker/docker/errdefs"
2121
"github.com/opencontainers/go-digest"
2222
"github.com/pkg/errors"
2323
)
2424

2525
const restrictedNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9_.+-]+$"
2626

27-
var restrictedNameRegEx = regexp.MustCompile(restrictedNamePattern)
27+
var restrictedNameRegEx = lazyregexp.New(restrictedNamePattern)
2828

2929
// Store provides a context store for easily remembering endpoints configuration
3030
type Store interface {

0 commit comments

Comments
 (0)