From c6f1b3ae4f2be915e273df16b80b31f13a475c8c Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Wed, 26 Mar 2025 21:40:09 +0000 Subject: [PATCH] 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 --- internal/experiments/experiments.go | 2 +- taskfile/ast/var.go | 67 +----- website/docs/experiments/map_variables.mdx | 245 --------------------- website/docs/usage.mdx | 93 ++++---- website/static/schema.json | 2 +- 5 files changed, 56 insertions(+), 353 deletions(-) delete mode 100644 website/docs/experiments/map_variables.mdx diff --git a/internal/experiments/experiments.go b/internal/experiments/experiments.go index 6235a039..ab676022 100644 --- a/internal/experiments/experiments.go +++ b/internal/experiments/experiments.go @@ -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) } diff --git a/taskfile/ast/var.go b/taskfile/ast/var.go index abe5157d..0806e423 100644 --- a/taskfile/ast/var.go +++ b/taskfile/ast/var.go @@ -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 { diff --git a/website/docs/experiments/map_variables.mdx b/website/docs/experiments/map_variables.mdx deleted file mode 100644 index 477714d0..00000000 --- a/website/docs/experiments/map_variables.mdx +++ /dev/null @@ -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. - - - - - -:::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: - - - - - -```yaml -version: 3 - -tasks: - foo: - vars: - CALCULATED_VAR: - sh: 'echo hello' - cmds: - - 'echo {{.CALCULATED_VAR}}' -``` - - - - -```yaml -version: 3 - -tasks: - foo: - vars: - CALCULATED_VAR: '$echo hello' # <-- Prefix dynamic variable with a `$` - cmds: - - 'echo {{.CALCULATED_VAR}}' -``` - - - -### References - - - - - -```yaml -version: 3 - -tasks: - foo: - vars: - VAR: 42 - VAR_REF: - ref: '.FOO' - cmds: - - 'echo {{.VAR_REF}}' -``` - - - - -```yaml -version: 3 - -tasks: - foo: - vars: - VAR: 42 - VAR_REF: '#.FOO' # <-- Prefix reference with a `#` - cmds: - - 'echo {{.VAR_REF}}' -``` - - - -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. - - - - -:::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}}' -``` - - - -## 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. - - - - - -```yaml -version: 3 - -tasks: - foo: - vars: - MAP: {a: 1, b: 2, c: 3} - cmds: - - for: - var: MAP - cmd: 'echo "{{.KEY}}: {{.ITEM}}"' -``` - - - - -```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. - -::: - - - -{/* prettier-ignore-start */} -[enabling-experiments]: ./experiments.mdx#enabling-experiments -{/* prettier-ignore-end */} diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx index f9738596..a894df42 100644 --- a/website/docs/usage.mdx +++ b/website/docs/usage.mdx @@ -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 */} diff --git a/website/static/schema.json b/website/static/schema.json index 49af497b..1aaeaa90 100644 --- a/website/static/schema.json +++ b/website/static/schema.json @@ -276,7 +276,7 @@ "^.*$": { "anyOf": [ { - "type": ["boolean", "integer", "null", "number", "string", "object", "array"] + "type": ["boolean", "integer", "null", "number", "string", "array"] }, { "$ref": "#/definitions/var_subkey"