diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index 85bf4809..e9d54932 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -3,15 +3,12 @@ package compiler import ( "bytes" "context" - "encoding/json" "fmt" "os" "path/filepath" "strings" "sync" - "gopkg.in/yaml.v3" - "github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/logger" @@ -78,18 +75,6 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool if err := cache.Err(); err != nil { return err } - // Evaluate JSON - if newVar.Json != "" { - if err := json.Unmarshal([]byte(newVar.Json), &newVar.Value); err != nil { - return err - } - } - // Evaluate YAML - if newVar.Yaml != "" { - if err := yaml.Unmarshal([]byte(newVar.Yaml), &newVar.Value); err != nil { - return err - } - } // If the variable is not dynamic, we can set it and return if newVar.Value != nil || newVar.Sh == "" { result.Set(k, ast.Var{Value: newVar.Value}) diff --git a/internal/templater/templater.go b/internal/templater/templater.go index 2e12cbb8..c25a33d0 100644 --- a/internal/templater/templater.go +++ b/internal/templater/templater.go @@ -2,6 +2,7 @@ package templater import ( "bytes" + "fmt" "maps" "strings" @@ -40,7 +41,15 @@ func ResolveRef(ref string, cache *Cache) any { cache.cacheMap = cache.Vars.ToCacheMap() } - val, err := template.ResolveRef(ref, cache.cacheMap) + if ref == "." { + return cache.cacheMap + } + t, err := template.New("resolver").Funcs(templateFuncs).Parse(fmt.Sprintf("{{%s}}", ref)) + if err != nil { + cache.err = err + return nil + } + val, err := t.ResolveRef(cache.cacheMap) if err != nil { cache.err = err return nil @@ -119,8 +128,6 @@ func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var Live: v.Live, Ref: v.Ref, Dir: v.Dir, - Json: ReplaceWithExtra(v.Json, cache, extra), - Yaml: ReplaceWithExtra(v.Yaml, cache, extra), } } diff --git a/task_test.go b/task_test.go index 2acf489f..25ce8b40 100644 --- a/task_test.go +++ b/task_test.go @@ -2426,3 +2426,48 @@ func TestWildcard(t *testing.T) { }) } } + +func TestReference(t *testing.T) { + tests := []struct { + name string + call string + expectedOutput string + }{ + { + name: "reference in command", + call: "ref-cmd", + expectedOutput: "1\n", + }, + { + name: "reference in dependency", + call: "ref-dep", + expectedOutput: "1\n", + }, + { + name: "reference using templating resolver", + call: "ref-resolver", + expectedOutput: "1\n", + }, + { + name: "reference using templating resolver and dynamic var", + call: "ref-resolver-sh", + expectedOutput: "Alice has 3 children called Bob, Charlie, and Diane\n", + }, + } + + for _, test := range tests { + t.Run(test.call, func(t *testing.T) { + var buff bytes.Buffer + e := task.Executor{ + Dir: "testdata/var_references", + Stdout: &buff, + Stderr: &buff, + Silent: true, + Force: true, + } + require.NoError(t, e.Setup()) + require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.call})) + assert.Equal(t, test.expectedOutput, buff.String()) + }) + } +} diff --git a/taskfile/ast/var.go b/taskfile/ast/var.go index ee037cbc..dce62884 100644 --- a/taskfile/ast/var.go +++ b/taskfile/ast/var.go @@ -83,8 +83,6 @@ type Var struct { Live any Sh string Ref string - Json string - Yaml string Dir string } @@ -103,6 +101,10 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error { v.Sh = str return nil } + if str, ok = strings.CutPrefix(str, "#"); ok { + v.Ref = str + return nil + } } v.Value = value return nil @@ -114,13 +116,11 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error { case yaml.MappingNode: key := node.Content[0].Value switch key { - case "sh", "ref", "map", "json", "yaml": + case "sh", "ref", "map": var m struct { - Sh string - Ref string - Map any - Json string - Yaml string + Sh string + Ref string + Map any } if err := node.Decode(&m); err != nil { return errors.NewTaskfileDecodeError(err, node) @@ -128,11 +128,9 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error { v.Sh = m.Sh v.Ref = m.Ref v.Value = m.Map - v.Json = m.Json - v.Yaml = m.Yaml return nil default: - return errors.NewTaskfileDecodeError(nil, node).WithMessage(`%q is not a valid variable type. Try "sh", "ref", "map", "json", "yaml" or using a scalar value`, key) + 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 @@ -148,17 +146,22 @@ func (v *Var) UnmarshalYAML(node *yaml.Node) error { switch node.Kind { case yaml.MappingNode: - if len(node.Content) > 2 || node.Content[0].Value != "sh" { + key := node.Content[0].Value + switch key { + case "sh", "ref": + var m struct { + Sh string + Ref string + } + if err := node.Decode(&m); err != nil { + return errors.NewTaskfileDecodeError(err, node) + } + v.Sh = m.Sh + v.Ref = m.Ref + return nil + default: return errors.NewTaskfileDecodeError(nil, node).WithMessage("maps cannot be assigned to variables") } - var sh struct { - Sh string - } - if err := node.Decode(&sh); err != nil { - return errors.NewTaskfileDecodeError(err, node) - } - v.Sh = sh.Sh - return nil default: var value any diff --git a/testdata/var_references/Taskfile.yml b/testdata/var_references/Taskfile.yml new file mode 100644 index 00000000..71621f29 --- /dev/null +++ b/testdata/var_references/Taskfile.yml @@ -0,0 +1,74 @@ +version: '3' + +vars: + GLOBAL_VAR: [1, 2, 2, 2, 3, 3, 4, 5] + +tasks: + default: + - task: ref-cmd + - task: ref-dep + - task: ref-resolver + - task: ref-resolver-sh + + ref-cmd: + vars: + VAR_REF: + ref: .GLOBAL_VAR + cmds: + - task: print-first + vars: + VAR: + ref: .VAR_REF + + ref-dep: + vars: + VAR_REF: + ref: .GLOBAL_VAR + deps: + - task: print-first + vars: + VAR: + ref: .VAR_REF + + ref-resolver: + vars: + VAR_REF: + ref: .GLOBAL_VAR + cmds: + - task: print-var + vars: + VAR: + ref: (index .VAR_REF 0) + + ref-resolver-sh: + vars: + JSON_STRING: + sh: echo '{"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]}' + JSON: + ref: "fromJson .JSON_STRING" + VAR_REF: + ref: .JSON + cmds: + - task: print-story + vars: + VAR: + ref: .VAR_REF + + print-var: + cmds: + - echo "{{.VAR}}" + + print-first: + cmds: + - echo "{{index .VAR 0}}" + + print-story: + cmds: + - >- + echo "{{.VAR.name}} has {{len .VAR.children}} children called + {{- $children := .VAR.children -}} + {{- range $i, $child := $children -}} + {{- if lt $i (sub (len $children) 1)}} {{$child.name -}}, + {{- else}} and {{$child.name -}} + {{- end -}} + {{- end -}}" diff --git a/testdata/vars/any2/Taskfile.yml b/testdata/vars/any2/Taskfile.yml index f3e8b80e..37b0206e 100644 --- a/testdata/vars/any2/Taskfile.yml +++ b/testdata/vars/any2/Taskfile.yml @@ -10,7 +10,6 @@ tasks: - task: ref-dep - task: ref-resolver - task: json - - task: yaml map: vars: @@ -93,25 +92,13 @@ tasks: JSON_STRING: sh: cat example.json JSON: - json: "{{.JSON_STRING}}" + ref: "fromJson .JSON_STRING" cmds: - task: print-story vars: VAR: ref: .JSON - yaml: - vars: - YAML_STRING: - sh: cat example.yaml - YAML: - yaml: "{{.YAML_STRING}}" - cmds: - - task: print-story - vars: - VAR: - ref: .YAML - print-var: cmds: - echo "{{.VAR}}" diff --git a/website/docs/experiments/map_variables.mdx b/website/docs/experiments/map_variables.mdx index 5be75d52..477714d0 100644 --- a/website/docs/experiments/map_variables.mdx +++ b/website/docs/experiments/map_variables.mdx @@ -43,9 +43,9 @@ To enable this experiment, set the environment variable: ::: -This proposal removes support for the `sh` keyword in favour of a new syntax for -dynamically defined variables, This allows you to define a map directly as you -would for any other type: +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 @@ -60,11 +60,26 @@ tasks: ## Migration -Taskfiles with dynamically defined variables via the `sh` subkey will no longer -work with this experiment enabled. In order to keep using dynamically defined -variables, you will need to migrate your Taskfile to use the new syntax. +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. -Previously, you might have defined a dynamic variable like this: +### 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 @@ -78,10 +93,8 @@ tasks: - 'echo {{.CALCULATED_VAR}}' ``` -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 @@ -89,14 +102,56 @@ version: 3 tasks: foo: vars: - CALCULATED_VAR: '$echo hello' + CALCULATED_VAR: '$echo hello' # <-- Prefix dynamic variable with a `$` cmds: - 'echo {{.CALCULATED_VAR}}' ``` -If your current Taskfile contains a string variable that begins with a `$`, you -will now need to escape the `$` with a backslash (`\`) to stop Task from -executing it as a command. + + +### 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. @@ -123,157 +178,12 @@ tasks: 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}}' ``` -## Parsing JSON and YAML - -In addition to the new `map` keyword, this proposal also adds support for the -`json` and `yaml` keywords for parsing JSON and YAML strings into real -objects/arrays. This is similar to the `fromJSON` template function, but means -that you only have to parse the JSON/YAML once when you declare the variable, -instead of every time you want to access a value. - - - - - -```yaml -version: 3 - -tasks: - foo: - vars: - FOO: '{"a": 1, "b": 2, "c": 3}' # <-- JSON string - cmds: - - 'echo {{(fromJSON .FOO).a}}' # <-- Parse JSON string every time you want to access a value - - 'echo {{(fromJSON .FOO).b}}' -``` - - - - -```yaml -version: 3 - -tasks: - foo: - vars: - FOO: - json: '{"a": 1, "b": 2, "c": 3}' # <-- JSON string parsed once - cmds: - - 'echo {{.FOO.a}}' # <-- Access values directly - - 'echo {{.FOO.b}}' -``` - - - -## Variables by reference - -Lastly, this proposal adds support for defining and passing variables by -reference. This is really important now that variables can be types other than a -string. - -Previously, to send a variable from one task to another, you would have to use -the templating system. Unfortunately, the templater _always_ outputs a string -and operations on the passed variable may not have behaved as expected. With -this proposal, you can now pass variables by reference using the `ref` subkey: - - - - - -```yaml -version: 3 - -tasks: - foo: - vars: - FOO: [A, B, C] # <-- FOO is defined as an array - cmds: - - task: bar - vars: - FOO: '{{.FOO}}' # <-- FOO gets converted to a string when passed to bar - bar: - cmds: - - 'echo {{index .FOO 0}}' # <-- FOO is a string so the task outputs '91' which is the ASCII code for '[' instead of the expected 'A' -``` - - - - -```yaml -version: 3 - -tasks: - foo: - vars: - FOO: [A, B, C] # <-- FOO is defined as an array - cmds: - - task: bar - vars: - FOO: - ref: .FOO # <-- FOO gets passed by reference to bar and maintains its type - bar: - cmds: - - 'echo {{index .FOO 0}}' # <-- FOO is still a map so the task outputs 'A' as expected -``` - - - -This means that the type of the variable is maintained when it is passed to -another Task. This also works the same way when calling `deps` and when defining -a variable and can be used in any combination: - -```yaml -version: 3 - -tasks: - foo: - vars: - FOO: [A, B, C] # <-- FOO is defined as an array - BAR: - ref: .FOO # <-- BAR is defined as a reference to FOO - deps: - - task: bar - vars: - BAR: - ref: .BAR # <-- BAR gets passed by reference to bar and maintains its type - bar: - cmds: - - 'echo {{index .BAR 0}}' # <-- BAR still refers to FOO so the task outputs 'A' -``` - -All references use the same templating syntax as regular templates, so in -addition to simply calling `.FOO`, you can also pass subkeys (`.FOO.BAR`) or -indexes (`index .FOO 0`) and use functions (`len .FOO`): - -```yaml -version: 3 - -tasks: - foo: - vars: - FOO: [A, B, C] # <-- FOO is defined as an array - cmds: - - task: bar - vars: - FOO: - ref: index .FOO 0 # <-- The element at index 0 is passed by reference to bar - bar: - cmds: - - 'echo {{.MYVAR}}' # <-- FOO is just the letter 'A' -``` - ## Looping over maps diff --git a/website/docs/reference/templating.mdx b/website/docs/reference/templating.mdx index 5a06582e..628b273b 100644 --- a/website/docs/reference/templating.mdx +++ b/website/docs/reference/templating.mdx @@ -153,7 +153,7 @@ itself][go-template-functions]: In addition to the built-in functions, Task also provides a set of functions imported via the [slim-sprig][slim-sprig] package. We only provide a very basic -descroption here for completeness. For detailed usage, please refer to the +description here for completeness. For detailed usage, please refer to the [slim-sprig documentation][slim-sprig]: #### [String Functions][string-functions] diff --git a/website/docs/usage.mdx b/website/docs/usage.mdx index 87b28cdb..c0d6b3e7 100644 --- a/website/docs/usage.mdx +++ b/website/docs/usage.mdx @@ -3,6 +3,9 @@ slug: /usage/ sidebar_position: 3 --- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # Usage ## Getting started @@ -964,6 +967,43 @@ Maps are not supported by default, but there is an 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] +``` + ::: Variables can be set in many places in a Taskfile. When executing @@ -1047,6 +1087,103 @@ tasks: This works for all types of variables. +### Referencing other variables + +Templating is great for referencing string values if you want to pass +a value from one task to another. However, the templating engine is only able to +output strings. If you want to pass something other than a string to another +task then you will need to use a reference (`ref`) instead. + + + + + +```yaml +version: 3 + +tasks: + foo: + vars: + FOO: [A, B, C] # <-- FOO is defined as an array + cmds: + - task: bar + vars: + FOO: '{{.FOO}}' # <-- FOO gets converted to a string when passed to bar + bar: + cmds: + - 'echo {{index .FOO 0}}' # <-- FOO is a string so the task outputs '91' which is the ASCII code for '[' instead of the expected 'A' +``` + + + + +```yaml +version: 3 + +tasks: + foo: + vars: + FOO: [A, B, C] # <-- FOO is defined as an array + cmds: + - task: bar + vars: + FOO: + ref: .FOO # <-- FOO gets passed by reference to bar and maintains its type + bar: + cmds: + - 'echo {{index .FOO 0}}' # <-- FOO is still a map so the task outputs 'A' as expected +``` + + + +This also works the same way when calling `deps` and when defining +a variable and can be used in any combination: + +```yaml +version: 3 + +tasks: + foo: + vars: + FOO: [A, B, C] # <-- FOO is defined as an array + BAR: + ref: .FOO # <-- BAR is defined as a reference to FOO + deps: + - task: bar + vars: + BAR: + ref: .BAR # <-- BAR gets passed by reference to bar and maintains its type + bar: + cmds: + - 'echo {{index .BAR 0}}' # <-- BAR still refers to FOO so the task outputs 'A' +``` + +All references use the same templating syntax as regular templates, so in +addition to simply calling `.FOO`, you can also pass subkeys (`.FOO.BAR`) or +indexes (`index .FOO 0`) and use functions (`len .FOO`) as described in the +[templating-reference][templating-reference]: + +```yaml +version: 3 + +tasks: + foo: + vars: + FOO: [A, B, C] # <-- FOO is defined as an array + cmds: + - task: bar + vars: + FOO: + ref: index .FOO 0 # <-- The element at index 0 is passed by reference to bar + bar: + cmds: + - 'echo {{.MYVAR}}' # <-- FOO is just the letter 'A' +``` + ## Looping over values As of v3.28.0, Task allows you to loop over certain values and execute a command @@ -1143,8 +1280,7 @@ tasks: cmd: cat {{.ITEM}} ``` -You can also loop over arrays directly (and maps if you have the -[maps experiment](/experiments/map-variables) enabled): +You can also loop over arrays directly and maps: ```yaml version: 3 diff --git a/website/static/schema.json b/website/static/schema.json index 8bec4461..899adff5 100644 --- a/website/static/schema.json +++ b/website/static/schema.json @@ -277,14 +277,6 @@ "map": { "type": "object", "description": "The value will be treated as a literal map type and stored in the variable" - }, - "json": { - "type": "string", - "description": "The value will parsed as a JSON string and stored in the variable" - }, - "yaml": { - "type": "string", - "description": "The value will parsed as a YAML string and stored in the variable" } }, "additionalProperties": false