diff --git a/CHANGELOG.md b/CHANGELOG.md index bc921400..80323fc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ (#2184 by @jubr). - Fixed `joinUrl` collapsing the `//` in a URL scheme (e.g. producing `http:/localhost` instead of `http://localhost`) (#2915 by @vsaraikin). +- Fixed `.MATCH` not being available in the `vars` of an include declared with a + wildcard namespace (e.g. `'stack:*'`), so `{{index .MATCH 0}}` can now be used + to feed captured segments into the included Taskfile (#2732 by @vmaerten). ## v3.52.0 - 2026-07-02 diff --git a/compiler.go b/compiler.go index 2c2e56f6..6128b5ae 100644 --- a/compiler.go +++ b/compiler.go @@ -115,6 +115,15 @@ func (c *Compiler) getVariables(t *ast.Task, call *Call, evaluateShVars bool) (* return nil, err } } + // MATCH is populated by GetTask from the wildcard match. Expose it early so + // that include vars coming from a wildcard include namespace can reference + // the captured segments, e.g. `{{index .MATCH 0}}`. It is re-set identically + // when call.Vars is ranged below, which is harmless. + if call != nil && call.Vars != nil { + if match, ok := call.Vars.Get("MATCH"); ok { + result.Set("MATCH", match) + } + } if t != nil { for k, v := range t.IncludeVars.All() { if err := rangeFunc(k, v); err != nil { diff --git a/task_test.go b/task_test.go index b56930e7..951573f8 100644 --- a/task_test.go +++ b/task_test.go @@ -3040,6 +3040,55 @@ func TestWildcard(t *testing.T) { } } +func TestIncludesWildcardVars(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + call string + expectedOutput string + wantErr bool + }{ + { + name: "match feeds include var", + call: "stack:prod:deploy", + expectedOutput: "Deploying to prod\n", + }, + { + name: "different match", + call: "stack:staging:deploy", + expectedOutput: "Deploying to staging\n", + }, + { + name: "no match", + call: "stack:deploy", + wantErr: true, + }, + } + + for _, test := range tests { + t.Run(test.call, func(t *testing.T) { + t.Parallel() + + var buff bytes.Buffer + e := task.NewExecutor( + task.WithDir("testdata/includes_wildcard_vars"), + task.WithStdout(&buff), + task.WithStderr(&buff), + task.WithSilent(true), + task.WithForce(true), + ) + require.NoError(t, e.Setup()) + if test.wantErr { + require.Error(t, e.Run(t.Context(), &task.Call{Task: test.call})) + return + } + require.NoError(t, e.Run(t.Context(), &task.Call{Task: test.call})) + assert.Equal(t, test.expectedOutput, buff.String()) + }) + } +} + // enableExperimentForTest enables the experiment behind pointer e for the duration of test t and sub-tests, // with the experiment being restored to its previous state when tests complete. // diff --git a/testdata/includes_wildcard_vars/Taskfile.yml b/testdata/includes_wildcard_vars/Taskfile.yml new file mode 100644 index 00000000..c89c66b8 --- /dev/null +++ b/testdata/includes_wildcard_vars/Taskfile.yml @@ -0,0 +1,7 @@ +version: "3" + +includes: + 'stack:*': + taskfile: ./stack/Taskfile.yml + vars: + ENV: '{{index .MATCH 0}}' diff --git a/testdata/includes_wildcard_vars/stack/Taskfile.yml b/testdata/includes_wildcard_vars/stack/Taskfile.yml new file mode 100644 index 00000000..158b1ce4 --- /dev/null +++ b/testdata/includes_wildcard_vars/stack/Taskfile.yml @@ -0,0 +1,6 @@ +version: "3" + +tasks: + deploy: + cmds: + - echo "Deploying to {{.ENV}}" diff --git a/website/src/docs/guide.md b/website/src/docs/guide.md index 8a06d38a..95d7721b 100644 --- a/website/src/docs/guide.md +++ b/website/src/docs/guide.md @@ -461,6 +461,31 @@ includes: DOCKER_IMAGE: frontend_image ``` +You can also use a [wildcard](#wildcard-arguments) in the include's namespace and +reference the captured segments through the `.MATCH` variable when setting the +include vars. This lets a single include be reused across many namespaces without +the included Taskfile needing to know how the value is extracted: + +```yaml +version: '3' + +includes: + 'stack:*': + taskfile: ./taskfiles/Stack.yml + vars: + ENV: '{{index .MATCH 0}}' +``` + +Running `task stack:prod:deploy` sets `ENV` to `prod` for the included Taskfile. + +::: info + +`.MATCH` is only available in the include's `vars`. The `taskfile` and `dir` +fields are resolved when Taskfiles are loaded and merged, before any task is +called, so no wildcard has been matched yet at that point. + +::: + ### Namespace aliases When including a Taskfile, you can give the namespace a list of `aliases`. This