From 98c537a90d1d2ef7c7c6112e6de574bba57cef5f Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Thu, 15 Jun 2023 15:04:03 +0000 Subject: [PATCH 1/6] feat: for --- docs/static/schema.json | 76 +++++++++++++++++- internal/fingerprint/glob.go | 2 +- internal/fingerprint/sources_checksum.go | 2 +- internal/fingerprint/sources_timestamp.go | 6 +- internal/templater/templater.go | 32 +++++++- task_test.go | 56 ++++++++++++++ taskfile/cmd.go | 10 ++- taskfile/for.go | 68 +++++++++++++++++ testdata/for/Taskfile.yml | 93 +++++++++++++++++++++++ testdata/for/bar.txt | 1 + testdata/for/foo.txt | 1 + variables.go | 52 ++++++++++++- 12 files changed, 387 insertions(+), 12 deletions(-) create mode 100644 taskfile/for.go create mode 100644 testdata/for/Taskfile.yml create mode 100644 testdata/for/bar.txt create mode 100644 testdata/for/foo.txt diff --git a/docs/static/schema.json b/docs/static/schema.json index f4e7ee0778..5641c03565 100644 --- a/docs/static/schema.json +++ b/docs/static/schema.json @@ -207,6 +207,9 @@ }, { "$ref": "#/definitions/3/task_call" + }, + { + "$ref": "#/definitions/3/for_call" } ] }, @@ -272,7 +275,9 @@ "description": "Hides task name and command from output. The command's output will still be redirected to `STDOUT` and `STDERR`.", "type": "boolean" } - } + }, + "additionalProperties": false, + "required": ["task"] }, "cmd_call": { "type": "object", @@ -318,6 +323,75 @@ "additionalProperties": false, "required": ["cmd"] }, + "for_call": { + "type": "object", + "properties": { + "for": { + "anyOf": [ + { + "$ref": "#/definitions/3/for_list" + }, + { + "$ref": "#/definitions/3/for_source" + }, + { + "$ref": "#/definitions/3/for_var" + } + ] + }, + "cmd": { + "description": "Command to run", + "type": "string" + }, + "task": { + "description": "Task to run", + "type": "string" + }, + "vars": { + "description": "Values passed to the task called", + "$ref": "#/definitions/3/vars" + } + }, + "oneOf": [ + {"required": ["cmd"]}, + {"required": ["task"]} + ], + "additionalProperties": false, + "required": ["for"] + }, + "for_list": { + "description": "List of values to iterate over", + "type": "array", + "items": { + "type": "string" + } + }, + "for_source": { + "description": "List of values to iterate over", + "type": "string", + "enum": ["source"] + }, + "for_var": { + "description": "List of values to iterate over", + "type": "object", + "properties": { + "var": { + "description": "Name of the variable to iterate over", + "type": "string" + }, + "split": { + "description": "String to split the variable on", + "type": "string" + }, + "as": { + "description": "What the loop variable should be named", + "default": "ITEM", + "type": "string" + } + }, + "additionalProperties": false, + "required": ["var"] + }, "precondition": { "anyOf": [ { diff --git a/internal/fingerprint/glob.go b/internal/fingerprint/glob.go index 0b7fc83715..ecf0e54c68 100644 --- a/internal/fingerprint/glob.go +++ b/internal/fingerprint/glob.go @@ -10,7 +10,7 @@ import ( "github.com/go-task/task/v3/internal/filepathext" ) -func globs(dir string, globs []string) ([]string, error) { +func Globs(dir string, globs []string) ([]string, error) { files := make([]string, 0) for _, g := range globs { f, err := Glob(dir, g) diff --git a/internal/fingerprint/sources_checksum.go b/internal/fingerprint/sources_checksum.go index f38903ca47..e9fb29806a 100644 --- a/internal/fingerprint/sources_checksum.go +++ b/internal/fingerprint/sources_checksum.go @@ -84,7 +84,7 @@ func (*ChecksumChecker) Kind() string { } func (c *ChecksumChecker) checksum(t *taskfile.Task) (string, error) { - sources, err := globs(t.Dir, t.Sources) + sources, err := Globs(t.Dir, t.Sources) if err != nil { return "", err } diff --git a/internal/fingerprint/sources_timestamp.go b/internal/fingerprint/sources_timestamp.go index 2d53d78ff6..aafb9bca05 100644 --- a/internal/fingerprint/sources_timestamp.go +++ b/internal/fingerprint/sources_timestamp.go @@ -28,11 +28,11 @@ func (checker *TimestampChecker) IsUpToDate(t *taskfile.Task) (bool, error) { return false, nil } - sources, err := globs(t.Dir, t.Sources) + sources, err := Globs(t.Dir, t.Sources) if err != nil { return false, nil } - generates, err := globs(t.Dir, t.Generates) + generates, err := Globs(t.Dir, t.Generates) if err != nil { return false, nil } @@ -90,7 +90,7 @@ func (checker *TimestampChecker) Kind() string { // Value implements the Checker Interface func (checker *TimestampChecker) Value(t *taskfile.Task) (any, error) { - sources, err := globs(t.Dir, t.Sources) + sources, err := Globs(t.Dir, t.Sources) if err != nil { return time.Now(), err } diff --git a/internal/templater/templater.go b/internal/templater/templater.go index 46ff5eb04d..c5486281e1 100644 --- a/internal/templater/templater.go +++ b/internal/templater/templater.go @@ -5,6 +5,8 @@ import ( "strings" "text/template" + "golang.org/x/exp/maps" + "github.com/go-task/task/v3/taskfile" ) @@ -25,6 +27,14 @@ func (r *Templater) ResetCache() { } func (r *Templater) Replace(str string) string { + return r.replace(str, nil) +} + +func (r *Templater) ReplaceWithExtra(str string, extra map[string]any) string { + return r.replace(str, extra) +} + +func (r *Templater) replace(str string, extra map[string]any) string { if r.err != nil || str == "" { return "" } @@ -40,7 +50,15 @@ func (r *Templater) Replace(str string) string { } var b bytes.Buffer - if err = templ.Execute(&b, r.cacheMap); err != nil { + if extra == nil { + err = templ.Execute(&b, r.cacheMap) + } else { + // Copy the map to avoid modifying the cached map + m := maps.Clone(r.cacheMap) + maps.Copy(m, extra) + err = templ.Execute(&b, m) + } + if err != nil { r.err = err return "" } @@ -63,6 +81,14 @@ func (r *Templater) ReplaceSlice(strs []string) []string { } func (r *Templater) ReplaceVars(vars *taskfile.Vars) *taskfile.Vars { + return r.replaceVars(vars, nil) +} + +func (r *Templater) ReplaceVarsWithExtra(vars *taskfile.Vars, extra map[string]any) *taskfile.Vars { + return r.replaceVars(vars, extra) +} + +func (r *Templater) replaceVars(vars *taskfile.Vars, extra map[string]any) *taskfile.Vars { if r.err != nil || vars.Len() == 0 { return nil } @@ -70,9 +96,9 @@ func (r *Templater) ReplaceVars(vars *taskfile.Vars) *taskfile.Vars { var new taskfile.Vars _ = vars.Range(func(k string, v taskfile.Var) error { new.Set(k, taskfile.Var{ - Static: r.Replace(v.Static), + Static: r.ReplaceWithExtra(v.Static, extra), Live: v.Live, - Sh: r.Replace(v.Sh), + Sh: r.ReplaceWithExtra(v.Sh, extra), }) return nil }) diff --git a/task_test.go b/task_test.go index c3b2bc3e86..a682723a52 100644 --- a/task_test.go +++ b/task_test.go @@ -2189,3 +2189,59 @@ func TestForce(t *testing.T) { }) } } + +func TestFor(t *testing.T) { + tests := []struct { + name string + expectedOutput string + }{ + { + name: "loop-explicit", + expectedOutput: "a\nb\nc\n", + }, + { + name: "loop-sources", + expectedOutput: "bar\nfoo\n", + }, + { + name: "loop-sources-glob", + expectedOutput: "bar\nfoo\n", + }, + { + name: "loop-vars", + expectedOutput: "foo\nbar\n", + }, + { + name: "loop-vars-sh", + expectedOutput: "bar\nfoo\n", + }, + { + name: "loop-task", + expectedOutput: "foo\nbar\n", + }, + { + name: "loop-task-as", + expectedOutput: "foo\nbar\n", + }, + { + name: "loop-different-tasks", + expectedOutput: "1\n2\n3\n", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var buff bytes.Buffer + e := task.Executor{ + Dir: "testdata/for", + Stdout: &buff, + Stderr: &buff, + Silent: true, + Force: true, + } + require.NoError(t, e.Setup()) + require.NoError(t, e.Run(context.Background(), taskfile.Call{Task: test.name, Direct: true})) + assert.Equal(t, test.expectedOutput, buff.String()) + }) + } +} diff --git a/taskfile/cmd.go b/taskfile/cmd.go index 5bb544216c..4157a3a6f6 100644 --- a/taskfile/cmd.go +++ b/taskfile/cmd.go @@ -11,8 +11,9 @@ import ( // Cmd is a task command type Cmd struct { Cmd string - Silent bool Task string + For *For + Silent bool Set []string Shopt []string Vars *Vars @@ -27,8 +28,9 @@ func (c *Cmd) DeepCopy() *Cmd { } return &Cmd{ Cmd: c.Cmd, - Silent: c.Silent, Task: c.Task, + For: c.For.DeepCopy(), + Silent: c.Silent, Set: deepcopy.Slice(c.Set), Shopt: deepcopy.Slice(c.Shopt), Vars: c.Vars.DeepCopy(), @@ -54,6 +56,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error { // A command with additional options var cmdStruct struct { Cmd string + For *For Silent bool Set []string Shopt []string @@ -62,6 +65,7 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error { } if err := node.Decode(&cmdStruct); err == nil && cmdStruct.Cmd != "" { c.Cmd = cmdStruct.Cmd + c.For = cmdStruct.For c.Silent = cmdStruct.Silent c.Set = cmdStruct.Set c.Shopt = cmdStruct.Shopt @@ -95,10 +99,12 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error { var taskCall struct { Task string Vars *Vars + For *For } if err := node.Decode(&taskCall); err == nil && taskCall.Task != "" { c.Task = taskCall.Task c.Vars = taskCall.Vars + c.For = taskCall.For c.Silent = cmdStruct.Silent return nil } diff --git a/taskfile/for.go b/taskfile/for.go new file mode 100644 index 0000000000..6f609deae4 --- /dev/null +++ b/taskfile/for.go @@ -0,0 +1,68 @@ +package taskfile + +import ( + "fmt" + + "gopkg.in/yaml.v3" + + "github.com/go-task/task/v3/internal/deepcopy" +) + +type For struct { + From string + List []string + Var string + Split string + As string +} + +func (f *For) UnmarshalYAML(node *yaml.Node) error { + switch node.Kind { + + case yaml.ScalarNode: + var from string + if err := node.Decode(&from); err != nil { + return err + } + f.From = from + return nil + + case yaml.SequenceNode: + var list []string + if err := node.Decode(&list); err != nil { + return err + } + f.List = list + return nil + + case yaml.MappingNode: + var forStruct struct { + Var string + Split string + As string + } + if err := node.Decode(&forStruct); err == nil && forStruct.Var != "" { + f.Var = forStruct.Var + f.Split = forStruct.Split + f.As = forStruct.As + return nil + } + + return fmt.Errorf("yaml: line %d: invalid keys in for", node.Line) + } + + return fmt.Errorf("yaml: line %d: cannot unmarshal %s into for", node.Line, node.ShortTag()) +} + +func (f *For) DeepCopy() *For { + if f == nil { + return nil + } + return &For{ + From: f.From, + List: deepcopy.Slice(f.List), + Var: f.Var, + Split: f.Split, + As: f.As, + } +} diff --git a/testdata/for/Taskfile.yml b/testdata/for/Taskfile.yml new file mode 100644 index 0000000000..0693ed71b4 --- /dev/null +++ b/testdata/for/Taskfile.yml @@ -0,0 +1,93 @@ +version: "3" + +tasks: + # Loop over a list of values + loop-explicit: + cmds: + - for: ["a", "b", "c"] + cmd: echo "{{.ITEM}}" + + # Loop over the task's sources + loop-sources: + sources: + - foo.txt + - bar.txt + cmds: + - for: source + cmd: cat "{{.ITEM}}" + + # Loop over the task's sources when globbed + loop-sources-glob: + sources: + - "*.txt" + cmds: + - for: source + cmd: cat "{{.ITEM}}" + + # Loop over the contents of a variable + loop-vars: + vars: + FOO: foo.txt,bar.txt + cmds: + - for: + var: FOO + split: "," + cmd: cat "{{.ITEM}}" + + # Loop over the output of a command (auto splits on " ") + loop-vars-sh: + vars: + FOO: + sh: ls *.txt + cmds: + - for: + var: FOO + cmd: cat "{{.ITEM}}" + + # Loop over another task + loop-task: + vars: + FOO: foo.txt bar.txt + cmds: + - for: + var: FOO + task: looped-task + vars: + FILE: "{{.ITEM}}" + + # Loop over another task with the variable named differently + loop-task-as: + vars: + FOO: foo.txt bar.txt + cmds: + - for: + var: FOO + as: FILE + task: looped-task + vars: + FILE: "{{.FILE}}" + + # Loop over different tasks using the variable + loop-different-tasks: + vars: + FOO: "1 2 3" + cmds: + - for: + var: FOO + task: task-{{.ITEM}} + + looped-task: + internal: true + cmd: cat "{{.FILE}}" + + task-1: + internal: true + cmd: echo "1" + + task-2: + internal: true + cmd: echo "2" + + task-3: + internal: true + cmd: echo "3" diff --git a/testdata/for/bar.txt b/testdata/for/bar.txt new file mode 100644 index 0000000000..5716ca5987 --- /dev/null +++ b/testdata/for/bar.txt @@ -0,0 +1 @@ +bar diff --git a/testdata/for/foo.txt b/testdata/for/foo.txt new file mode 100644 index 0000000000..257cc5642c --- /dev/null +++ b/testdata/for/foo.txt @@ -0,0 +1 @@ +foo diff --git a/variables.go b/variables.go index 5962d6e49a..5d80ebd4f0 100644 --- a/variables.go +++ b/variables.go @@ -124,10 +124,60 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf if cmd == nil { continue } + if cmd.For != nil { + var list []string + // Get the list from the explicit forh list + if cmd.For.List != nil && len(cmd.For.List) > 0 { + list = cmd.For.List + } + // Get the list from the task sources + if cmd.For.From == "source" { + list, err = fingerprint.Globs(new.Dir, new.Sources) + if err != nil { + return nil, err + } + } + // Get the list from a variable and split it up + if cmd.For.Var != "" { + if vars != nil { + v := vars.Get(cmd.For.Var) + if cmd.For.Split != "" { + list = strings.Split(v.Static, cmd.For.Split) + } else { + list = strings.Fields(v.Static) + } + } + } + // Name the iterator variable + var as string + if cmd.For.As != "" { + as = cmd.For.As + } else { + as = "ITEM" + } + // Create a new command for each item in the list + for _, loopValue := range list { + extra := map[string]any{ + as: loopValue, + } + new.Cmds = append(new.Cmds, &taskfile.Cmd{ + Cmd: r.ReplaceWithExtra(cmd.Cmd, extra), + Task: r.ReplaceWithExtra(cmd.Task, extra), + Silent: cmd.Silent, + Set: cmd.Set, + Shopt: cmd.Shopt, + Vars: r.ReplaceVarsWithExtra(cmd.Vars, extra), + IgnoreError: cmd.IgnoreError, + Defer: cmd.Defer, + Platforms: cmd.Platforms, + }) + } + continue + } new.Cmds = append(new.Cmds, &taskfile.Cmd{ Cmd: r.Replace(cmd.Cmd), - Silent: cmd.Silent, Task: r.Replace(cmd.Task), + Silent: cmd.Silent, Set: cmd.Set, Shopt: cmd.Shopt, Vars: r.ReplaceVars(cmd.Vars), From 34839bc1a41d1d572611ae263460e5061b97ba58 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Thu, 15 Jun 2023 16:13:23 +0000 Subject: [PATCH 2/6] docs: for --- CHANGELOG.md | 3 + docs/docs/api_reference.md | 28 ++++++- docs/docs/usage.md | 155 +++++++++++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 170d5a24ec..ce5d6ca470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Unreleased +- Added the ability to + [loop over commands and tasks](https://taskfile.dev/usage/#looping-over-values) + using `for` (#82, #1220 by @pd93). - Fixed variable propagation in multi-level includes (#778, #996, #1256 by @hudclark). diff --git a/docs/docs/api_reference.md b/docs/docs/api_reference.md index 61e54aac6d..06dd4e2181 100644 --- a/docs/docs/api_reference.md +++ b/docs/docs/api_reference.md @@ -126,12 +126,13 @@ There are some special variables that is available on the templating system: | `TASKFILE_DIR` | The absolute path of the included Taskfile. | | `USER_WORKING_DIR` | The absolute path of the directory `task` was called from. | | `CHECKSUM` | The checksum of the files listed in `sources`. Only available within the `status` prop and if method is set to `checksum`. | -| `TIMESTAMP` | The date object of the greatest timestamp of the files listes in `sources`. Only available within the `status` prop and if method is set to `timestamp`. | +| `TIMESTAMP` | The date object of the greatest timestamp of the files listed in `sources`. Only available within the `status` prop and if method is set to `timestamp`. | | `TASK_VERSION` | The current version of task. | +| `ITEM` | The value of the current iteration when using the `for` property. | ## ENV -Some environment variables can be overriden to adjust Task behavior. +Some environment variables can be overridden to adjust Task behavior. | ENV | Default | Description | | -------------------- | ------- | ----------------------------------------------------------------------------------------------------------------- | @@ -262,8 +263,9 @@ tasks: | Attribute | Type | Default | Description | | -------------- | ---------------------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `cmd` | `string` | | The shell command to be executed. | -| `silent` | `bool` | `false` | Skips some output for this command. Note that STDOUT and STDERR of the commands will still be redirected. | | `task` | `string` | | Set this to trigger execution of another task instead of running a command. This cannot be set together with `cmd`. | +| `for` | [`For`](#for) | | Runs the command once for each given value. | +| `silent` | `bool` | `false` | Skips some output for this command. Note that STDOUT and STDERR of the commands will still be redirected. | | `vars` | [`map[string]Variable`](#variable) | | Optional additional variables to be passed to the referenced task. Only relevant when setting `task` instead of `cmd`. | | `ignore_error` | `bool` | `false` | Continue execution if errors happen while executing the command. | | `defer` | `string` | | Alternative to `cmd`, but schedules the command to be executed at the end of this task instead of immediately. This cannot be used together with `cmd`. | @@ -306,6 +308,26 @@ tasks: ::: +#### For + +The `for` parameter can be defined as a string, a list of strings or a map. If +it is defined as a string, you can give it any of the following values: + +- `source` - Will run the command for each source file defined on the task. + (Glob patterns will be resolved, so `*.go` will run for every Go file that + matches). + +If it is defined as a list of strings, the command will be run for each value. + +Finally, the `for` parameter can be defined as a map when you want to use a +variable to define the values to loop over: + +| Attribute | Type | Default | Description | +| --------- | -------- | ---------------- | -------------------------------------------- | +| `var` | `string` | | The name of the variable to use as an input. | +| `split` | `string` | (any whitespace) | What string the variable should be split on. | +| `as` | `string` | `ITEM` | The name of the iterator variable. | + #### Precondition | Attribute | Type | Default | Description | diff --git a/docs/docs/usage.md b/docs/docs/usage.md index b34dd4159f..3dcb978b8c 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -1000,6 +1000,161 @@ tasks: This works for all types of variables. +## Looping over values + +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 + +The simplest kind of loop is an explicit one. This is useful when you want to +loop over a set of values that are known ahead of time. + +```yaml +version: '3' + +tasks: + default: + cmds: + - for: ['foo.txt', 'bar.txt'] + cmd: cat {{ .ITEM }} +``` + +### Looping over your task's sources + +You are also able to loop over the sources of your task: + +```yaml +version: '3' + +tasks: + default: + sources: + - foo.txt + - bar.txt + cmds: + - for: sources + cmd: cat {{ .ITEM }} +``` + +This will also work if you use globbing syntax in your sources. For example, if +you specify a source for `*.txt`, the loop will iterate over all files that +match that glob. + +### Looping over variables + +To loop over the contents of a variable, you simply need to specify the variable +you want to loop over. By default, variables will be split on any whitespace +characters. + +```yaml +version: '3' + +tasks: + default: + vars: + my_var: foo.txt bar.txt + cmds: + - for: + var: my_var + cmd: cat {{ .ITEM }} +``` + +If you need to split on a different character, you can do this by specifying the +`split` property: + +```yaml +version: '3' + +tasks: + default: + vars: + my_var: foo.txt,bar.txt + cmds: + - for: + var: my_var + split: ',' + cmd: cat {{ .ITEM }} +``` + +All of this also works with dynamic variables! + +```yaml +version: '3' + +tasks: + default: + vars: + my_var: + sh: find -type f -name '*.txt' + cmds: + - for: + var: my_var + cmd: cat {{ .ITEM }} +``` + +### Renaming variables + +If you want to rename the iterator variable to make it clearer what the value +contains, you can do so by specifying the `as` property: + +```yaml +version: '3' + +tasks: + default: + vars: + my_var: foo.txt bar.txt + cmds: + - for: + var: my_var + as: FILE + cmd: cat {{ .FILE }} +``` + +### Looping over tasks + +Because the `for` property is defined at the `cmds` level, you can also use it +alongside the `task` keyword to run tasks multiple times with different +variables. + +```yaml +version: '3' + +tasks: + default: + cmds: + - for: [foo, bar] + task: my-task + vars: + FILE: '{{ .ITEM }}' + + my-task: + cmds: + - echo '{{ .FILE }}' +``` + +Or if you want to run different tasks depending on the value of the loop: + +```yaml +version: '3' + +tasks: + default: + cmds: + - for: [foo, bar] + task: task-{{ .ITEM }} + + task-foo: + cmds: + - echo 'foo' + + task-bar: + cmds: + - echo 'bar' +``` + ## Forwarding CLI arguments to commands If `--` is given in the CLI, all following parameters are added to a special From cd4dc0d48a8f326e4df27b4a1de7e86addcc038f Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Thu, 15 Jun 2023 16:13:26 +0000 Subject: [PATCH 3/6] fix: silent should be processed via taskCall --- taskfile/cmd.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/taskfile/cmd.go b/taskfile/cmd.go index 4157a3a6f6..26e916b2af 100644 --- a/taskfile/cmd.go +++ b/taskfile/cmd.go @@ -97,15 +97,16 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error { // A task call var taskCall struct { - Task string - Vars *Vars - For *For + Task string + Vars *Vars + For *For + Silent bool } if err := node.Decode(&taskCall); err == nil && taskCall.Task != "" { c.Task = taskCall.Task c.Vars = taskCall.Vars c.For = taskCall.For - c.Silent = cmdStruct.Silent + c.Silent = taskCall.Silent return nil } From dd06391d151a404c173fcb524c326469a2b99310 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Tue, 27 Jun 2023 14:14:29 +0000 Subject: [PATCH 4/6] chore: sources instead of source --- docs/static/schema.json | 12 ++++++------ testdata/for/Taskfile.yml | 4 ++-- variables.go | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/static/schema.json b/docs/static/schema.json index 5641c03565..70d49a2847 100644 --- a/docs/static/schema.json +++ b/docs/static/schema.json @@ -332,7 +332,7 @@ "$ref": "#/definitions/3/for_list" }, { - "$ref": "#/definitions/3/for_source" + "$ref": "#/definitions/3/for_attribute" }, { "$ref": "#/definitions/3/for_var" @@ -360,19 +360,19 @@ "required": ["for"] }, "for_list": { - "description": "List of values to iterate over", + "description": "A list of values to iterate over", "type": "array", "items": { "type": "string" } }, - "for_source": { - "description": "List of values to iterate over", + "for_attribute": { + "description": "The task attribute to iterate over", "type": "string", - "enum": ["source"] + "enum": ["sources"] }, "for_var": { - "description": "List of values to iterate over", + "description": "Which variables to iterate over. The variable will be split using any whitespace character by default. This can be changed by using the `split` attribute.", "type": "object", "properties": { "var": { diff --git a/testdata/for/Taskfile.yml b/testdata/for/Taskfile.yml index 0693ed71b4..576f684dfa 100644 --- a/testdata/for/Taskfile.yml +++ b/testdata/for/Taskfile.yml @@ -13,7 +13,7 @@ tasks: - foo.txt - bar.txt cmds: - - for: source + - for: sources cmd: cat "{{.ITEM}}" # Loop over the task's sources when globbed @@ -21,7 +21,7 @@ tasks: sources: - "*.txt" cmds: - - for: source + - for: sources cmd: cat "{{.ITEM}}" # Loop over the contents of a variable diff --git a/variables.go b/variables.go index 5d80ebd4f0..d97a40b670 100644 --- a/variables.go +++ b/variables.go @@ -126,12 +126,12 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf } if cmd.For != nil { var list []string - // Get the list from the explicit forh list + // Get the list from the explicit for list if cmd.For.List != nil && len(cmd.For.List) > 0 { list = cmd.For.List } // Get the list from the task sources - if cmd.For.From == "source" { + if cmd.For.From == "sources" { list, err = fingerprint.Globs(new.Dir, new.Sources) if err != nil { return nil, err From ba4eb16cc2b0e1572678081eb41c93e28777586d Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Tue, 27 Jun 2023 16:51:54 +0000 Subject: [PATCH 5/6] chore: make sources paths relative --- variables.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/variables.go b/variables.go index d97a40b670..9038d4d302 100644 --- a/variables.go +++ b/variables.go @@ -2,6 +2,7 @@ package task import ( "os" + "path/filepath" "strings" "github.com/joho/godotenv" @@ -136,6 +137,12 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf if err != nil { return nil, err } + // Make the paths relative to the task dir + for i, v := range list { + if list[i], err = filepath.Rel(new.Dir, v); err != nil { + return nil, err + } + } } // Get the list from a variable and split it up if cmd.For.Var != "" { From 1718b6bed8fce313dc6139a02915d8a6870b5761 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Wed, 19 Jul 2023 21:26:55 +0000 Subject: [PATCH 6/6] feat: added joinPath and relPath functions --- docs/docs/usage.md | 31 +++++++++++++++++++++++++------ internal/templater/funcs.go | 9 ++++++++- 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/docs/docs/usage.md b/docs/docs/usage.md index 3dcb978b8c..774be62689 100644 --- a/docs/docs/usage.md +++ b/docs/docs/usage.md @@ -883,17 +883,16 @@ you can use `requires`. This is useful when might not be clear to users which variables are needed, or if you want clear message about what is required. Also some tasks could have dangerous side effects if run with un-set variables. -Using `requires` you specify an array of strings in the `vars` sub-section -under `requires`, these strings are variable names which are checked prior to -running the task. If any variables are un-set the the task will error and not -run. +Using `requires` you specify an array of strings in the `vars` sub-section under +`requires`, these strings are variable names which are checked prior to running +the task. If any variables are un-set the the task will error and not run. Environmental variables are also checked. Syntax: ```yaml -requires: +requires: vars: [] # Array of strings ``` @@ -914,7 +913,7 @@ tasks: - 'docker build . -t {{.IMAGE_NAME}}:{{.IMAGE_TAG}}' # Make sure these variables are set before running - requires: + requires: vars: [IMAGE_NAME, IMAGE_TAG] ``` @@ -1042,6 +1041,26 @@ This will also work if you use globbing syntax in your sources. For example, if you specify a source for `*.txt`, the loop will iterate over all files that match that glob. +Source paths will always be returned as paths relative to the task directory. If +you need to convert this to an absolute path, you can use the built-in +`joinPath` function: + +```yaml +version: '3' + +tasks: + default: + vars: + MY_DIR: /path/to/dir + dir: '{{.MY_DIR}}' + sources: + - foo.txt + - bar.txt + cmds: + - for: sources + cmd: cat {{ joinPath .MY_DIR .ITEM }} +``` + ### Looping over variables To loop over the contents of a variable, you simply need to specify the variable diff --git a/internal/templater/funcs.go b/internal/templater/funcs.go index 21eaf8fc57..d548d6a76b 100644 --- a/internal/templater/funcs.go +++ b/internal/templater/funcs.go @@ -6,9 +6,10 @@ import ( "strings" "text/template" - sprig "github.com/go-task/slim-sprig" "mvdan.cc/sh/v3/shell" "mvdan.cc/sh/v3/syntax" + + sprig "github.com/go-task/slim-sprig" ) var templateFuncs template.FuncMap @@ -45,6 +46,12 @@ func init() { }, // IsSH is deprecated. "IsSH": func() bool { return true }, + "joinPath": func(elem ...string) string { + return filepath.Join(elem...) + }, + "relPath": func(basePath, targetPath string) (string, error) { + return filepath.Rel(basePath, targetPath) + }, } // Deprecated aliases for renamed functions. taskFuncs["FromSlash"] = taskFuncs["fromSlash"]