From 281d259e6ebd201b0f0d104a9674df39c6be4f9c Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Mon, 2 Sep 2024 20:29:00 +0100 Subject: [PATCH] feat: loop over a matrix (#1767) --- task_test.go | 15 +++++++++++ taskfile/ast/for.go | 35 ++++++++++++++---------- testdata/for/cmds/Taskfile.yml | 8 ++++++ testdata/for/deps/Taskfile.yml | 10 +++++++ variables.go | 49 +++++++++++++++++++++++++++++++++- website/docs/usage.mdx | 37 ++++++++++++++++++++++--- website/static/schema.json | 9 +++++++ 7 files changed, 145 insertions(+), 18 deletions(-) diff --git a/task_test.go b/task_test.go index 4ca92fb1..689cf621 100644 --- a/task_test.go +++ b/task_test.go @@ -2374,6 +2374,10 @@ func TestForCmds(t *testing.T) { name: "loop-explicit", expectedOutput: "a\nb\nc\n", }, + { + name: "loop-matrix", + expectedOutput: "windows/amd64\nwindows/arm64\nlinux/amd64\nlinux/arm64\ndarwin/amd64\ndarwin/arm64\n", + }, { name: "loop-sources", expectedOutput: "bar\nfoo\n", @@ -2431,6 +2435,17 @@ func TestForDeps(t *testing.T) { name: "loop-explicit", expectedOutputContains: []string{"a\n", "b\n", "c\n"}, }, + { + name: "loop-matrix", + expectedOutputContains: []string{ + "windows/amd64\n", + "windows/arm64\n", + "linux/amd64\n", + "linux/arm64\n", + "darwin/amd64\n", + "darwin/arm64\n", + }, + }, { name: "loop-sources", expectedOutputContains: []string{"bar\n", "foo\n"}, diff --git a/taskfile/ast/for.go b/taskfile/ast/for.go index 8505fbb3..5a87acf2 100644 --- a/taskfile/ast/for.go +++ b/taskfile/ast/for.go @@ -8,11 +8,12 @@ import ( ) type For struct { - From string - List []any - Var string - Split string - As string + From string + List []any + Matrix map[string][]any + Var string + Split string + As string } func (f *For) UnmarshalYAML(node *yaml.Node) error { @@ -36,16 +37,21 @@ func (f *For) UnmarshalYAML(node *yaml.Node) error { case yaml.MappingNode: var forStruct struct { - Var string - Split string - As string + Matrix map[string][]any + Var string + Split string + As string } if err := node.Decode(&forStruct); err != nil { return errors.NewTaskfileDecodeError(err, node) } - if forStruct.Var == "" { + if forStruct.Var == "" && forStruct.Matrix == nil { return errors.NewTaskfileDecodeError(nil, node).WithMessage("invalid keys in for") } + if forStruct.Var != "" && forStruct.Matrix != nil { + return errors.NewTaskfileDecodeError(nil, node).WithMessage("cannot use both var and matrix in for") + } + f.Matrix = forStruct.Matrix f.Var = forStruct.Var f.Split = forStruct.Split f.As = forStruct.As @@ -60,10 +66,11 @@ func (f *For) DeepCopy() *For { return nil } return &For{ - From: f.From, - List: deepcopy.Slice(f.List), - Var: f.Var, - Split: f.Split, - As: f.As, + From: f.From, + List: deepcopy.Slice(f.List), + Matrix: deepcopy.Map(f.Matrix), + Var: f.Var, + Split: f.Split, + As: f.As, } } diff --git a/testdata/for/cmds/Taskfile.yml b/testdata/for/cmds/Taskfile.yml index 576f684d..a436a4a3 100644 --- a/testdata/for/cmds/Taskfile.yml +++ b/testdata/for/cmds/Taskfile.yml @@ -7,6 +7,14 @@ tasks: - for: ["a", "b", "c"] cmd: echo "{{.ITEM}}" + loop-matrix: + cmds: + - for: + matrix: + OS: ["windows", "linux", "darwin"] + ARCH: ["amd64", "arm64"] + cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" + # Loop over the task's sources loop-sources: sources: diff --git a/testdata/for/deps/Taskfile.yml b/testdata/for/deps/Taskfile.yml index e3c67598..1d70f50d 100644 --- a/testdata/for/deps/Taskfile.yml +++ b/testdata/for/deps/Taskfile.yml @@ -9,6 +9,16 @@ tasks: vars: TEXT: "{{.ITEM}}" + loop-matrix: + deps: + - for: + matrix: + OS: ["windows", "linux", "darwin"] + ARCH: ["amd64", "arm64"] + task: echo + vars: + TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}" + # Loop over the task's sources loop-sources: sources: diff --git a/variables.go b/variables.go index db2a3fc4..22e47aa9 100644 --- a/variables.go +++ b/variables.go @@ -271,9 +271,13 @@ func itemsFromFor( ) ([]any, []string, error) { var keys []string // The list of keys to loop over (only if looping over a map) var values []any // The list of values to loop over + // Get the list from a matrix + if f.Matrix != nil { + return asAnySlice(product(f.Matrix)), nil, nil + } // Get the list from the explicit for list if len(f.List) > 0 { - values = f.List + return f.List, nil, nil } // Get the list from the task sources if f.From == "sources" { @@ -322,3 +326,46 @@ func itemsFromFor( } return values, keys, nil } + +// product generates the cartesian product of the input map of slices. +func product(inputMap map[string][]any) []map[string]any { + if len(inputMap) == 0 { + return nil + } + + // Extract the keys and corresponding slices + keys := make([]string, 0, len(inputMap)) + slices := make([][]any, 0, len(inputMap)) + for key, slice := range inputMap { + keys = append(keys, key) + slices = append(slices, slice) + } + + // Start with an empty product result + result := []map[string]any{{}} + + // Iterate over each slice in the slices + for i, slice := range slices { + var newResult []map[string]any + + // For each combination in the current result + for _, combination := range result { + // Append each element from the current slice to the combinations + for _, item := range slice { + newComb := make(map[string]any, len(combination)) + // Copy the existing combination + for k, v := range combination { + newComb[k] = v + } + // Add the current item with the corresponding key + newComb[keys[i]] = item + newResult = append(newResult, newComb) + } + } + + // Update result with the new combinations + result = newResult + } + + return result +} diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx index 77c80c89..e05f7268 100644 --- a/website/docs/usage.mdx +++ b/website/docs/usage.mdx @@ -1297,9 +1297,9 @@ tasks: ## Looping over values -As of v3.28.0, Task allows you to loop over certain values and execute a command -for each. There are a number of ways to do this depending on the type of value -you want to loop over. +Task allows you to loop over certain values and execute a command for each. +There are a number of ways to do this depending on the type of value you want to +loop over. ### Looping over a static list @@ -1316,6 +1316,37 @@ tasks: cmd: cat {{ .ITEM }} ``` +### Looping over a matrix + +If you need to loop over all permutations of multiple lists, you can use the +`matrix` property. This should be familiar to anyone who has used a matrix in a +CI/CD pipeline. + +```yaml +version: '3' + +tasks: + default: + silent: true + cmds: + - for: + matrix: + OS: ["windows", "linux", "darwin"] + ARCH: ["amd64", "arm64"] + cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}" +``` + +This will output: + +```txt +windows/amd64 +windows/arm64 +linux/amd64 +linux/arm64 +darwin/amd64 +darwin/arm64 +``` + ### Looping over your task's sources You are also able to loop over the sources of your task: diff --git a/website/static/schema.json b/website/static/schema.json index 317213b4..f87511f5 100644 --- a/website/static/schema.json +++ b/website/static/schema.json @@ -431,6 +431,9 @@ }, { "$ref": "#/definitions/for_var" + }, + { + "$ref": "#/definitions/for_matrix" } ] }, @@ -467,6 +470,12 @@ "additionalProperties": false, "required": ["var"] }, + "for_matrix": { + "description": "A matrix of values to iterate over", + "type": "object", + "additionalProperties": true, + "required": ["matrix"] + }, "precondition": { "anyOf": [ {