summaryrefslogtreecommitdiffhomepage
path: root/modules/caddyhttp
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r--modules/caddyhttp/celmatcher.go51
-rw-r--r--modules/caddyhttp/celmatcher_test.go133
-rw-r--r--modules/caddyhttp/fileserver/matcher.go10
-rw-r--r--modules/caddyhttp/matchers.go4
-rw-r--r--modules/caddyhttp/templates/templates.go6
-rw-r--r--modules/caddyhttp/templates/tplcontext.go1
-rw-r--r--modules/caddyhttp/vars.go89
7 files changed, 242 insertions, 52 deletions
diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go
index a5565eb98..2a03ebba7 100644
--- a/modules/caddyhttp/celmatcher.go
+++ b/modules/caddyhttp/celmatcher.go
@@ -126,6 +126,10 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
// light (and possibly naïve) syntactic sugar
m.expandedExpr = placeholderRegexp.ReplaceAllString(m.Expr, placeholderExpansion)
+ // as a second pass, we'll strip the escape character from an escaped
+ // placeholder, so that it can be used as an input to other CEL functions
+ m.expandedExpr = escapedPlaceholderRegexp.ReplaceAllString(m.expandedExpr, escapedPlaceholderExpansion)
+
// our type adapter expands CEL's standard type support
m.ta = celTypeAdapter{}
@@ -159,14 +163,17 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
// create the CEL environment
env, err := cel.NewEnv(
- cel.Function(placeholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload(
- placeholderFuncName+"_httpRequest_string",
+ cel.Function(CELPlaceholderFuncName, cel.SingletonBinaryBinding(m.caddyPlaceholderFunc), cel.Overload(
+ CELPlaceholderFuncName+"_httpRequest_string",
[]*cel.Type{httpRequestObjectType, cel.StringType},
cel.AnyType,
)),
- cel.Variable("request", httpRequestObjectType),
+ cel.Variable(CELRequestVarName, httpRequestObjectType),
cel.CustomTypeAdapter(m.ta),
ext.Strings(),
+ ext.Bindings(),
+ ext.Lists(),
+ ext.Math(),
matcherLib,
)
if err != nil {
@@ -247,7 +254,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
return types.NewErr(
"invalid request of type '%v' to %s(request, placeholderVarName)",
lhs.Type(),
- placeholderFuncName,
+ CELPlaceholderFuncName,
)
}
phStr, ok := rhs.(types.String)
@@ -255,7 +262,7 @@ func (m MatchExpression) caddyPlaceholderFunc(lhs, rhs ref.Val) ref.Val {
return types.NewErr(
"invalid placeholder variable name of type '%v' to %s(request, placeholderVarName)",
rhs.Type(),
- placeholderFuncName,
+ CELPlaceholderFuncName,
)
}
@@ -275,7 +282,7 @@ var httpRequestCELType = cel.ObjectType("http.Request", traits.ReceiverType)
type celHTTPRequest struct{ *http.Request }
func (cr celHTTPRequest) ResolveName(name string) (any, bool) {
- if name == "request" {
+ if name == CELRequestVarName {
return cr, true
}
return nil, false
@@ -457,15 +464,15 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int
callArgs := call.Args()
reqAttr, ok := callArgs[0].(interpreter.InterpretableAttribute)
if !ok {
- return nil, errors.New("missing 'request' argument")
+ return nil, errors.New("missing 'req' argument")
}
nsAttr, ok := reqAttr.Attr().(interpreter.NamespacedAttribute)
if !ok {
- return nil, errors.New("missing 'request' argument")
+ return nil, errors.New("missing 'req' argument")
}
varNames := nsAttr.CandidateVariableNames()
- if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != "request" {
- return nil, errors.New("missing 'request' argument")
+ if len(varNames) != 1 || len(varNames) == 1 && varNames[0] != CELRequestVarName {
+ return nil, errors.New("missing 'req' argument")
}
matcherData, ok := callArgs[1].(interpreter.InterpretableConst)
if !ok {
@@ -524,7 +531,7 @@ func celMatcherStringListMacroExpander(funcName string) cel.MacroFactory {
return nil, eh.NewError(arg.ID(), "matcher arguments must be string constants")
}
}
- return eh.NewCall(funcName, eh.NewIdent("request"), eh.NewList(matchArgs...)), nil
+ return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), eh.NewList(matchArgs...)), nil
}
}
@@ -538,7 +545,7 @@ func celMatcherStringMacroExpander(funcName string) parser.MacroExpander {
return nil, eh.NewError(0, "matcher requires one argument")
}
if isCELStringExpr(args[0]) {
- return eh.NewCall(funcName, eh.NewIdent("request"), args[0]), nil
+ return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), args[0]), nil
}
return nil, eh.NewError(args[0].ID(), "matcher argument must be a string literal")
}
@@ -572,7 +579,7 @@ func celMatcherJSONMacroExpander(funcName string) parser.MacroExpander {
return nil, eh.NewError(entry.AsMapEntry().Value().ID(), "matcher map values must be string or list literals")
}
}
- return eh.NewCall(funcName, eh.NewIdent("request"), arg), nil
+ return eh.NewCall(funcName, eh.NewIdent(CELRequestVarName), arg), nil
case ast.UnspecifiedExprKind, ast.CallKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.SelectKind:
// appeasing the linter :)
}
@@ -646,7 +653,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool {
switch e.Kind() {
case ast.CallKind:
call := e.AsCall()
- if call.FunctionName() == "caddyPlaceholder" {
+ if call.FunctionName() == CELPlaceholderFuncName {
return true
}
case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind:
@@ -701,8 +708,15 @@ func isCELStringListLiteral(e ast.Expr) bool {
// expressions with a proper CEL function call; this is
// just for syntactic sugar.
var (
- placeholderRegexp = regexp.MustCompile(`{([a-zA-Z][\w.-]+)}`)
- placeholderExpansion = `caddyPlaceholder(request, "${1}")`
+ // The placeholder may not be preceded by a backslash; the expansion
+ // will include the preceding character if it is not a backslash.
+ placeholderRegexp = regexp.MustCompile(`([^\\]|^){([a-zA-Z][\w.-]+)}`)
+ placeholderExpansion = `${1}ph(req, "${2}")`
+
+ // As a second pass, we need to strip the escape character in front of
+ // the placeholder, if it exists.
+ escapedPlaceholderRegexp = regexp.MustCompile(`\\{([a-zA-Z][\w.-]+)}`)
+ escapedPlaceholderExpansion = `{${1}}`
CELTypeJSON = cel.MapType(cel.StringType, cel.DynType)
)
@@ -710,7 +724,10 @@ var (
var httpRequestObjectType = cel.ObjectType("http.Request")
// The name of the CEL function which accesses Replacer values.
-const placeholderFuncName = "caddyPlaceholder"
+const CELPlaceholderFuncName = "ph"
+
+// The name of the CEL request variable.
+const CELRequestVarName = "req"
const MatcherNameCtxKey = "matcher_name"
diff --git a/modules/caddyhttp/celmatcher_test.go b/modules/caddyhttp/celmatcher_test.go
index 25fdcf45e..26491b7ca 100644
--- a/modules/caddyhttp/celmatcher_test.go
+++ b/modules/caddyhttp/celmatcher_test.go
@@ -70,13 +70,36 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
wantResult: true,
},
{
- name: "header error (MatchHeader)",
+ name: "header matches an escaped placeholder value (MatchHeader)",
expression: &MatchExpression{
- Expr: `header('foo')`,
+ Expr: `header({'Field': '\\\{foobar}'})`,
},
urlTarget: "https://example.com/foo",
- httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
- wantErr: true,
+ httpHeader: &http.Header{"Field": []string{"{foobar}"}},
+ wantResult: true,
+ },
+ {
+ name: "header matches an placeholder replaced during the header matcher (MatchHeader)",
+ expression: &MatchExpression{
+ Expr: `header({'Field': '\{http.request.uri.path}'})`,
+ },
+ urlTarget: "https://example.com/foo",
+ httpHeader: &http.Header{"Field": []string{"/foo"}},
+ wantResult: true,
+ },
+ {
+ name: "header error, invalid escape sequence (MatchHeader)",
+ expression: &MatchExpression{
+ Expr: `header({'Field': '\\{foobar}'})`,
+ },
+ wantErr: true,
+ },
+ {
+ name: "header error, needs to be JSON syntax with field as key (MatchHeader)",
+ expression: &MatchExpression{
+ Expr: `header('foo')`,
+ },
+ wantErr: true,
},
{
name: "header_regexp matches (MatchHeaderRE)",
@@ -110,9 +133,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `header_regexp('foo')`,
},
- urlTarget: "https://example.com/foo",
- httpHeader: &http.Header{"Field": []string{"foo", "bar"}},
- wantErr: true,
+ wantErr: true,
},
{
name: "host matches localhost (MatchHost)",
@@ -143,8 +164,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `host(80)`,
},
- urlTarget: "http://localhost:80",
- wantErr: true,
+ wantErr: true,
},
{
name: "method does not match (MatchMethod)",
@@ -169,9 +189,7 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `method()`,
},
- urlTarget: "https://foo.example.com",
- httpMethod: "PUT",
- wantErr: true,
+ wantErr: true,
},
{
name: "path matches substring (MatchPath)",
@@ -266,24 +284,21 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `protocol()`,
},
- urlTarget: "https://example.com",
- wantErr: true,
+ wantErr: true,
},
{
name: "protocol invocation error too many args (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol('grpc', 'https')`,
},
- urlTarget: "https://example.com",
- wantErr: true,
+ wantErr: true,
},
{
name: "protocol invocation error wrong arg type (MatchProtocol)",
expression: &MatchExpression{
Expr: `protocol(true)`,
},
- urlTarget: "https://example.com",
- wantErr: true,
+ wantErr: true,
},
{
name: "query does not match against a specific value (MatchQuery)",
@@ -330,40 +345,35 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
expression: &MatchExpression{
Expr: `query({1: "1"})`,
},
- urlTarget: "https://example.com/foo",
- wantErr: true,
+ wantErr: true,
},
{
name: "query error typed struct instead of map (MatchQuery)",
expression: &MatchExpression{
Expr: `query(Message{field: "1"})`,
},
- urlTarget: "https://example.com/foo",
- wantErr: true,
+ wantErr: true,
},
{
name: "query error bad map value type (MatchQuery)",
expression: &MatchExpression{
Expr: `query({"debug": 1})`,
},
- urlTarget: "https://example.com/foo/?debug=1",
- wantErr: true,
+ wantErr: true,
},
{
name: "query error no args (MatchQuery)",
expression: &MatchExpression{
Expr: `query()`,
},
- urlTarget: "https://example.com/foo/?debug=1",
- wantErr: true,
+ wantErr: true,
},
{
name: "remote_ip error no args (MatchRemoteIP)",
expression: &MatchExpression{
Expr: `remote_ip()`,
},
- urlTarget: "https://example.com/foo",
- wantErr: true,
+ wantErr: true,
},
{
name: "remote_ip single IP match (MatchRemoteIP)",
@@ -373,6 +383,67 @@ eqp31wM9il1n+guTNyxJd+FzVAH+hCZE5K+tCgVDdVFUlDEHHbS/wqb2PSIoouLV
urlTarget: "https://example.com/foo",
wantResult: true,
},
+ {
+ name: "vars value (VarsMatcher)",
+ expression: &MatchExpression{
+ Expr: `vars({'foo': 'bar'})`,
+ },
+ urlTarget: "https://example.com/foo",
+ wantResult: true,
+ },
+ {
+ name: "vars matches placeholder, needs escape (VarsMatcher)",
+ expression: &MatchExpression{
+ Expr: `vars({'\{http.request.uri.path}': '/foo'})`,
+ },
+ urlTarget: "https://example.com/foo",
+ wantResult: true,
+ },
+ {
+ name: "vars error wrong syntax (VarsMatcher)",
+ expression: &MatchExpression{
+ Expr: `vars('foo', 'bar')`,
+ },
+ wantErr: true,
+ },
+ {
+ name: "vars error no args (VarsMatcher)",
+ expression: &MatchExpression{
+ Expr: `vars()`,
+ },
+ wantErr: true,
+ },
+ {
+ name: "vars_regexp value (MatchVarsRE)",
+ expression: &MatchExpression{
+ Expr: `vars_regexp('foo', 'ba?r')`,
+ },
+ urlTarget: "https://example.com/foo",
+ wantResult: true,
+ },
+ {
+ name: "vars_regexp value with name (MatchVarsRE)",
+ expression: &MatchExpression{
+ Expr: `vars_regexp('name', 'foo', 'ba?r')`,
+ },
+ urlTarget: "https://example.com/foo",
+ wantResult: true,
+ },
+ {
+ name: "vars_regexp matches placeholder, needs escape (MatchVarsRE)",
+ expression: &MatchExpression{
+ Expr: `vars_regexp('\{http.request.uri.path}', '/fo?o')`,
+ },
+ urlTarget: "https://example.com/foo",
+ wantResult: true,
+ },
+ {
+ name: "vars_regexp error no args (MatchVarsRE)",
+ expression: &MatchExpression{
+ Expr: `vars_regexp()`,
+ },
+ wantErr: true,
+ },
}
)
@@ -396,6 +467,9 @@ func TestMatchExpressionMatch(t *testing.T) {
}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
+ ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
+ "foo": "bar",
+ })
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
@@ -436,6 +510,9 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
}
repl := caddy.NewReplacer()
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
+ ctx = context.WithValue(ctx, VarsCtxKey, map[string]any{
+ "foo": "bar",
+ })
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
if tc.clientCertificate != nil {
diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go
index 6ab2180ad..71de1db29 100644
--- a/modules/caddyhttp/fileserver/matcher.go
+++ b/modules/caddyhttp/fileserver/matcher.go
@@ -225,7 +225,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
return func(eh parser.ExprHelper, target ast.Expr, args []ast.Expr) (ast.Expr, *common.Error) {
if len(args) == 0 {
return eh.NewCall("file",
- eh.NewIdent("request"),
+ eh.NewIdent(caddyhttp.CELRequestVarName),
eh.NewMap(),
), nil
}
@@ -233,7 +233,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
arg := args[0]
if isCELStringLiteral(arg) || isCELCaddyPlaceholderCall(arg) {
return eh.NewCall("file",
- eh.NewIdent("request"),
+ eh.NewIdent(caddyhttp.CELRequestVarName),
eh.NewMap(eh.NewMapEntry(
eh.NewLiteral(types.String("try_files")),
eh.NewList(arg),
@@ -242,7 +242,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
), nil
}
if isCELTryFilesLiteral(arg) {
- return eh.NewCall("file", eh.NewIdent("request"), arg), nil
+ return eh.NewCall("file", eh.NewIdent(caddyhttp.CELRequestVarName), arg), nil
}
return nil, &common.Error{
Location: eh.OffsetLocation(arg.ID()),
@@ -259,7 +259,7 @@ func celFileMatcherMacroExpander() parser.MacroExpander {
}
}
return eh.NewCall("file",
- eh.NewIdent("request"),
+ eh.NewIdent(caddyhttp.CELRequestVarName),
eh.NewMap(eh.NewMapEntry(
eh.NewLiteral(types.String("try_files")),
eh.NewList(args...),
@@ -634,7 +634,7 @@ func isCELCaddyPlaceholderCall(e ast.Expr) bool {
switch e.Kind() {
case ast.CallKind:
call := e.AsCall()
- if call.FunctionName() == "caddyPlaceholder" {
+ if call.FunctionName() == caddyhttp.CELPlaceholderFuncName {
return true
}
case ast.UnspecifiedExprKind, ast.ComprehensionKind, ast.IdentKind, ast.ListKind, ast.LiteralKind, ast.MapKind, ast.SelectKind, ast.StructKind:
diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index a0bc6d63a..93a36237b 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -1562,8 +1562,8 @@ var (
_ CELLibraryProducer = (*MatchHeader)(nil)
_ CELLibraryProducer = (*MatchHeaderRE)(nil)
_ CELLibraryProducer = (*MatchProtocol)(nil)
- // _ CELLibraryProducer = (*VarsMatcher)(nil)
- // _ CELLibraryProducer = (*MatchVarsRE)(nil)
+ _ CELLibraryProducer = (*VarsMatcher)(nil)
+ _ CELLibraryProducer = (*MatchVarsRE)(nil)
_ json.Marshaler = (*MatchNot)(nil)
_ json.Unmarshaler = (*MatchNot)(nil)
diff --git a/modules/caddyhttp/templates/templates.go b/modules/caddyhttp/templates/templates.go
index 0eba4870f..eb6488659 100644
--- a/modules/caddyhttp/templates/templates.go
+++ b/modules/caddyhttp/templates/templates.go
@@ -81,6 +81,12 @@ func init() {
// {{placeholder "http.error.status_code"}}
// ```
//
+// As a shortcut, `ph` is an alias for `placeholder`.
+//
+// ```
+// {{ph "http.request.method"}}
+// ```
+//
// ##### `.Host`
//
// Returns the hostname portion (no port) of the Host header of the HTTP request.
diff --git a/modules/caddyhttp/templates/tplcontext.go b/modules/caddyhttp/templates/tplcontext.go
index 2324586be..1b1020f1b 100644
--- a/modules/caddyhttp/templates/tplcontext.go
+++ b/modules/caddyhttp/templates/tplcontext.go
@@ -88,6 +88,7 @@ func (c *TemplateContext) NewTemplate(tplName string) *template.Template {
"fileStat": c.funcFileStat,
"env": c.funcEnv,
"placeholder": c.funcPlaceholder,
+ "ph": c.funcPlaceholder, // shortcut
"fileExists": c.funcFileExists,
"httpError": c.funcHTTPError,
"humanize": c.funcHumanize,
diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go
index 9e86dd716..77e06e3cb 100644
--- a/modules/caddyhttp/vars.go
+++ b/modules/caddyhttp/vars.go
@@ -18,8 +18,12 @@ import (
"context"
"fmt"
"net/http"
+ "reflect"
"strings"
+ "github.com/google/cel-go/cel"
+ "github.com/google/cel-go/common/types/ref"
+
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)
@@ -203,6 +207,28 @@ func (m VarsMatcher) Match(r *http.Request) bool {
return false
}
+// CELLibrary produces options that expose this matcher for use in CEL
+// expression matchers.
+//
+// Example:
+//
+// expression vars({'{magic_number}': ['3', '5']})
+// expression vars({'{foo}': 'single_value'})
+func (VarsMatcher) CELLibrary(_ caddy.Context) (cel.Library, error) {
+ return CELMatcherImpl(
+ "vars",
+ "vars_matcher_request_map",
+ []*cel.Type{CELTypeJSON},
+ func(data ref.Val) (RequestMatcher, error) {
+ mapStrListStr, err := CELValueToMapStrList(data)
+ if err != nil {
+ return nil, err
+ }
+ return VarsMatcher(mapStrListStr), nil
+ },
+ )
+}
+
// MatchVarsRE matches the value of the context variables by a given regular expression.
//
// Upon a match, it adds placeholders to the request: `{http.regexp.name.capture_group}`
@@ -302,6 +328,69 @@ func (m MatchVarsRE) Match(r *http.Request) bool {
return false
}
+// CELLibrary produces options that expose this matcher for use in CEL
+// expression matchers.
+//
+// Example:
+//
+// expression vars_regexp('foo', '{magic_number}', '[0-9]+')
+// expression vars_regexp('{magic_number}', '[0-9]+')
+func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
+ unnamedPattern, err := CELMatcherImpl(
+ "vars_regexp",
+ "vars_regexp_request_string_string",
+ []*cel.Type{cel.StringType, cel.StringType},
+ func(data ref.Val) (RequestMatcher, error) {
+ refStringList := reflect.TypeOf([]string{})
+ params, err := data.ConvertToNative(refStringList)
+ if err != nil {
+ return nil, err
+ }
+ strParams := params.([]string)
+ matcher := MatchVarsRE{}
+ matcher[strParams[0]] = &MatchRegexp{
+ Pattern: strParams[1],
+ Name: ctx.Value(MatcherNameCtxKey).(string),
+ }
+ err = matcher.Provision(ctx)
+ return matcher, err
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+ namedPattern, err := CELMatcherImpl(
+ "vars_regexp",
+ "vars_regexp_request_string_string_string",
+ []*cel.Type{cel.StringType, cel.StringType, cel.StringType},
+ func(data ref.Val) (RequestMatcher, error) {
+ refStringList := reflect.TypeOf([]string{})
+ params, err := data.ConvertToNative(refStringList)
+ if err != nil {
+ return nil, err
+ }
+ strParams := params.([]string)
+ name := strParams[0]
+ if name == "" {
+ name = ctx.Value(MatcherNameCtxKey).(string)
+ }
+ matcher := MatchVarsRE{}
+ matcher[strParams[1]] = &MatchRegexp{
+ Pattern: strParams[2],
+ Name: name,
+ }
+ err = matcher.Provision(ctx)
+ return matcher, err
+ },
+ )
+ if err != nil {
+ return nil, err
+ }
+ envOpts := append(unnamedPattern.CompileOptions(), namedPattern.CompileOptions()...)
+ prgOpts := append(unnamedPattern.ProgramOptions(), namedPattern.ProgramOptions()...)
+ return NewMatcherCELLibrary(envOpts, prgOpts), nil
+}
+
// Validate validates m's regular expressions.
func (m MatchVarsRE) Validate() error {
for _, rm := range m {