1
0
mirror of https://github.com/go-task/task.git synced 2025-05-13 22:16:31 +02:00

feat: make map variables experiment (prop 2) generally available (#2081)

* feat: make map variables experiment (prop 2) generally available

* docs: remove map variables experiment page and update usage to include map variable info
This commit is contained in:
Pete Davison 2025-03-26 21:40:09 +00:00 committed by GitHub
parent cb14a4f3a1
commit c6f1b3ae4f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 56 additions and 353 deletions

View File

@ -44,7 +44,7 @@ func init() {
GentleForce = New("GENTLE_FORCE", 1)
RemoteTaskfiles = New("REMOTE_TASKFILES", 1)
AnyVariables = New("ANY_VARIABLES")
MapVariables = New("MAP_VARIABLES", 1, 2)
MapVariables = New("MAP_VARIABLES")
EnvPrecedence = New("ENV_PRECEDENCE", 1)
}

View File

@ -1,12 +1,9 @@
package ast
import (
"strings"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/experiments"
)
// Var represents either a static or dynamic variable.
@ -19,82 +16,26 @@ type Var struct {
}
func (v *Var) UnmarshalYAML(node *yaml.Node) error {
if experiments.MapVariables.Enabled() {
// This implementation is not backwards-compatible and replaces the 'sh' key with map variables
if experiments.MapVariables.Value == 1 {
var value any
if err := node.Decode(&value); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
// If the value is a string and it starts with $, then it's a shell command
if str, ok := value.(string); ok {
if str, ok = strings.CutPrefix(str, "$"); ok {
v.Sh = &str
return nil
}
if str, ok = strings.CutPrefix(str, "#"); ok {
v.Ref = str
return nil
}
}
v.Value = value
return nil
}
// This implementation IS backwards-compatible and keeps the 'sh' key and allows map variables to be added under the `map` key
if experiments.MapVariables.Value == 2 {
switch node.Kind {
case yaml.MappingNode:
key := node.Content[0].Value
switch key {
case "sh", "ref", "map":
var m struct {
Sh *string
Ref string
Map any
}
if err := node.Decode(&m); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Sh = m.Sh
v.Ref = m.Ref
v.Value = m.Map
return nil
default:
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
}
default:
var value any
if err := node.Decode(&value); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Value = value
return nil
}
}
}
switch node.Kind {
case yaml.MappingNode:
key := node.Content[0].Value
switch key {
case "sh", "ref":
case "sh", "ref", "map":
var m struct {
Sh *string
Ref string
Map any
}
if err := node.Decode(&m); err != nil {
return errors.NewTaskfileDecodeError(err, node)
}
v.Sh = m.Sh
v.Ref = m.Ref
v.Value = m.Map
return nil
default:
return errors.NewTaskfileDecodeError(nil, node).WithMessage("maps cannot be assigned to variables")
return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map" or using a scalar value`, key)
}
default:
var value any
if err := node.Decode(&value); err != nil {

View File

@ -1,245 +0,0 @@
---
slug: /experiments/map-variables/
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Map Variables (#1585)
:::caution
All experimental features are subject to breaking changes and/or removal _at any
time_. We strongly recommend that you do not use these features in a production
environment. They are intended for testing and feedback only.
:::
Currently, Task supports all variable types except for maps. This experiment
adds two different proposals for map variables. Click on the tabs below to
switch between them.
<Tabs defaultValue="1" queryString="proposal"
values={[
{label: 'Proposal 1', value: '1'},
{label: 'Proposal 2', value: '2'}
]}>
<TabItem value="1">
:::warning
This experiment proposal breaks the following functionality:
- Dynamically defined variables (using the `sh` keyword)
:::
:::info
To enable this experiment, set the environment variable:
`TASK_X_MAP_VARIABLES=1`. Check out [our guide to enabling experiments
][enabling-experiments] for more information.
:::
This proposal removes support for the `sh` and `ref` keywords in favour of a new
syntax for dynamically defined variables and references. This allows you to
define a map directly as you would for any other type:
```yaml
version: 3
tasks:
foo:
vars:
FOO: {a: 1, b: 2, c: 3} # <-- Directly defined map on the `FOO` key
cmds:
- 'echo {{.FOO.a}}'
```
## Migration
Taskfiles with dynamically defined variables via the `sh` subkey or references
defined with `ref` will no longer work with this experiment enabled. In order to
keep using these features, you will need to migrate your Taskfile to use the new
syntax.
### Dynamic Variables
Previously, you had to define dynamic variables using the `sh` subkey. With this
experiment enabled, you will need to remove the `sh` subkey and define your
command as a string that begins with a `$`. This will instruct Task to interpret
the string as a command instead of a literal value and the variable will be
populated with the output of the command. For example:
<Tabs defaultValue="2"
values={[
{label: 'Before', value: '1'},
{label: 'After', value: '2'}
]}>
<TabItem value="1">
```yaml
version: 3
tasks:
foo:
vars:
CALCULATED_VAR:
sh: 'echo hello'
cmds:
- 'echo {{.CALCULATED_VAR}}'
```
</TabItem>
<TabItem value="2">
```yaml
version: 3
tasks:
foo:
vars:
CALCULATED_VAR: '$echo hello' # <-- Prefix dynamic variable with a `$`
cmds:
- 'echo {{.CALCULATED_VAR}}'
```
</TabItem></Tabs>
### References
<Tabs defaultValue="2"
values={[
{label: 'Before', value: '1'},
{label: 'After', value: '2'}
]}>
<TabItem value="1">
```yaml
version: 3
tasks:
foo:
vars:
VAR: 42
VAR_REF:
ref: '.FOO'
cmds:
- 'echo {{.VAR_REF}}'
```
</TabItem>
<TabItem value="2">
```yaml
version: 3
tasks:
foo:
vars:
VAR: 42
VAR_REF: '#.FOO' # <-- Prefix reference with a `#`
cmds:
- 'echo {{.VAR_REF}}'
```
</TabItem></Tabs>
If your current Taskfile contains a string variable that begins with a `$` or a
`#`, you will now need to escape it with a backslash (`\`) to stop Task from
interpreting it as a command or reference.
</TabItem>
<TabItem value="2">
:::info
To enable this experiment, set the environment variable:
`TASK_X_MAP_VARIABLES=2`. Check out [our guide to enabling experiments
][enabling-experiments] for more information.
:::
This proposal maintains backwards-compatibility and the `sh` subkey and adds
another new `map` subkey for defining map variables:
```yaml
version: 3
tasks:
foo:
vars:
FOO:
map: {a: 1, b: 2, c: 3} # <-- Defined using the `map' subkey instead of directly on 'FOO'
BAR: true # <-- Other types of variables are still defined directly on the key
BAZ:
sh: 'echo Hello Task' # <-- The `sh` subkey is still supported
QUX:
ref: '.BAZ' # <-- The `ref` subkey is still supported
cmds:
- 'echo {{.FOO.a}}'
```
</TabItem></Tabs>
## Looping over maps
This experiment also adds support for looping over maps using the `for` keyword,
just like arrays. In addition to the `{{.ITEM}}` variable being populated when
looping over a map, we also make an additional `{{.KEY}}` variable available
that holds the string value of the map key.
<Tabs defaultValue="1" queryString="proposal"
values={[
{label: 'Proposal 1', value: '1'},
{label: 'Proposal 2', value: '2'}
]}>
<TabItem value="1">
```yaml
version: 3
tasks:
foo:
vars:
MAP: {a: 1, b: 2, c: 3}
cmds:
- for:
var: MAP
cmd: 'echo "{{.KEY}}: {{.ITEM}}"'
```
</TabItem>
<TabItem value="2">
```yaml
version: 3
tasks:
foo:
vars:
map:
MAP: {a: 1, b: 2, c: 3}
cmds:
- for:
var: MAP
cmd: 'echo "{{.KEY}}: {{.ITEM}}"'
```
:::note
Remember that maps are unordered, so
the order in which the items are looped over is random.
:::
</TabItem></Tabs>
{/* prettier-ignore-start */}
[enabling-experiments]: ./experiments.mdx#enabling-experiments
{/* prettier-ignore-end */}

View File

@ -1113,53 +1113,38 @@ variable types are supported:
- `int`
- `float`
- `array`
- `map`
:::note
Maps are not supported by default, but there is an
[experiment][map-variables] that can be enabled to add support. If
you're interested in this functionality, we would appreciate your feedback.
:pray:
In the meantime, it is technically possible to define a map using a `ref` resolver and a templating function. For example:
```yaml
version: '3'
tasks:
task-with-map:
vars:
FOO:
ref: dict "a" "1" "b" "2" "c" "3"
cmds:
- echo {{.FOO}}
```
```txt
map[a:1 b:2 c:3]
```
OR by using the same technique with JSON:
```yaml
version: '3'
tasks:
task-with-map:
vars:
JSON: '{"a": 1, "b": 2, "c": 3}'
FOO:
ref: "fromJson .JSON"
cmds:
- echo {{.FOO}}
```
```txt
map[a:1 b:2 c:3]
```
Defining a map requires that you use a special `map` subkey (see example below).
:::
```yaml
version: 3
tasks:
foo:
vars:
STRING: 'Hello, World!'
BOOL: true
INT: 42
FLOAT: 3.14
ARRAY: [1, 2, 3]
MAP:
map: {A: 1, B: 2, C: 3}
cmds:
- 'echo {{.STRING}}' # Hello, World!
- 'echo {{.BOOL}}' # true
- 'echo {{.INT}}' # 42
- 'echo {{.FLOAT}}' # 3.14
- 'echo {{.ARRAY}}' # [1 2 3]
- 'echo {{.ARRAY.0}}' # 1
- 'echo {{.MAP}}' # map[A:1 B:2 C:3]
- 'echo {{.MAP.A}}' # 1
```
Variables can be set in many places in a Taskfile. When executing
[templates][templating-reference], Task will look for variables in the order
listed below (most important first):
@ -1360,6 +1345,29 @@ tasks:
- 'echo {{.FOO}}' # <-- FOO is just the letter 'A'
```
### Parsing JSON/YAML into map variables
If you have a raw JSON or YAML string that you want to process in Task, you can
use a combination of the `ref` keyword and the `fromJson` or `fromYaml`
templating functions to parse the string into a map variable. For example:
```yaml
version: '3'
tasks:
task-with-map:
vars:
JSON: '{"a": 1, "b": 2, "c": 3}'
FOO:
ref: "fromJson .JSON"
cmds:
- echo {{.FOO}}
```
```txt
map[a:1 b:2 c:3]
```
## Looping over values
Task allows you to loop over certain values and execute a command for each.
@ -1508,7 +1516,7 @@ tasks:
cmd: cat {{.ITEM}}
```
You can also loop over arrays directly and maps:
You can also loop over arrays and maps directly:
```yaml
version: 3
@ -2319,6 +2327,5 @@ if called by another task, either directly or as a dependency.
{/* prettier-ignore-start */}
[gotemplate]: https://golang.org/pkg/text/template/
[map-variables]: ./experiments/map_variables.mdx
[templating-reference]: ./reference/templating.mdx
{/* prettier-ignore-end */}

View File

@ -276,7 +276,7 @@
"^.*$": {
"anyOf": [
{
"type": ["boolean", "integer", "null", "number", "string", "object", "array"]
"type": ["boolean", "integer", "null", "number", "string", "array"]
},
{
"$ref": "#/definitions/var_subkey"