Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to loop over values #1220

Merged
merged 6 commits into from
Jul 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down
28 changes: 25 additions & 3 deletions docs/docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
| -------------------- | ------- | ----------------------------------------------------------------------------------------------------------------- |
Expand Down Expand Up @@ -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`. |
Expand Down Expand Up @@ -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. |

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice.


#### Precondition

| Attribute | Type | Default | Description |
Expand Down
186 changes: 180 additions & 6 deletions docs/docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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]
```

Expand Down Expand Up @@ -1000,6 +999,181 @@ 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.

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
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
Expand Down
76 changes: 75 additions & 1 deletion docs/static/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,9 @@
},
{
"$ref": "#/definitions/3/task_call"
},
{
"$ref": "#/definitions/3/for_call"
}
]
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -318,6 +323,75 @@
"additionalProperties": false,
"required": ["cmd"]
},
"for_call": {
"type": "object",
"properties": {
"for": {
"anyOf": [
{
"$ref": "#/definitions/3/for_list"
},
{
"$ref": "#/definitions/3/for_attribute"
},
{
"$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": "A list of values to iterate over",
"type": "array",
"items": {
"type": "string"
}
},
"for_attribute": {
"description": "The task attribute to iterate over",
"type": "string",
"enum": ["sources"]
},
"for_var": {
"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": {
"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": [
{
Expand Down
2 changes: 1 addition & 1 deletion internal/fingerprint/glob.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/fingerprint/sources_checksum.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down