From a16e7f652cc84d2047583dd5c0655a1910686866 Mon Sep 17 00:00:00 2001 From: Vladimir Saraikin Date: Mon, 13 Jul 2026 19:47:46 +0200 Subject: [PATCH] fix: escape regex metacharacters in WildcardMatch WildcardMatch built a regex from the task name translating only "*", then called regexp.MustCompile on it. A task name containing a regex metacharacter either panicked (e.g. "c++" -> "invalid nested repetition operator") or matched too loosely ("a.b" matched "axb"). Since FindMatchingTasks calls WildcardMatch on every task, one such name breaks matching for the whole Taskfile. Escape the name with regexp.QuoteMeta, keeping "*" as the only wildcard. --- taskfile/ast/task.go | 5 ++++- taskfile/ast/task_test.go | 41 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 taskfile/ast/task_test.go diff --git a/taskfile/ast/task.go b/taskfile/ast/task.go index 9465c77770..d86ff0159d 100644 --- a/taskfile/ast/task.go +++ b/taskfile/ast/task.go @@ -87,7 +87,10 @@ func (t *Task) WildcardMatch(name string) (bool, []string) { names := append([]string{t.Task}, t.Aliases...) for _, taskName := range names { - regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(taskName, "*", "(.*)")) + // Escape regex metacharacters in the task name so a name like "c++" or + // "a.b" is matched literally (and doesn't panic in MustCompile); only + // "*" is a wildcard. + regexStr := fmt.Sprintf("^%s$", strings.ReplaceAll(regexp.QuoteMeta(taskName), `\*`, "(.*)")) regex := regexp.MustCompile(regexStr) wildcards := regex.FindStringSubmatch(name) diff --git a/taskfile/ast/task_test.go b/taskfile/ast/task_test.go new file mode 100644 index 0000000000..0083a537a9 --- /dev/null +++ b/taskfile/ast/task_test.go @@ -0,0 +1,41 @@ +package ast_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/go-task/task/v3/taskfile/ast" +) + +func TestTaskWildcardMatch(t *testing.T) { + t.Parallel() + + tests := []struct { + taskName string + call string + wantMatch bool + wantWildcards []string + }{ + {"build-*", "build-foo", true, []string{"foo"}}, + {"build-*-*", "build-foo-bar", true, []string{"foo", "bar"}}, + {"build-*", "test-foo", false, nil}, + // Regex metacharacters in the task name must be matched literally, not + // interpreted as regex (and must not panic in MustCompile). + {"c++", "c++", true, []string{}}, + {"c++", "cxx", false, nil}, + {"a.b", "axb", false, nil}, // '.' must not act as a wildcard + {"a.b", "a.b", true, []string{}}, + {"deploy.prod", "deploy-prod", false, nil}, + } + + for _, tt := range tests { + t.Run(tt.taskName+"/"+tt.call, func(t *testing.T) { + t.Parallel() + task := &ast.Task{Task: tt.taskName} + match, wildcards := task.WildcardMatch(tt.call) + assert.Equal(t, tt.wantMatch, match) + assert.Equal(t, tt.wantWildcards, wildcards) + }) + } +}