Skip to content

feat: variable references #1654

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

Merged
merged 6 commits into from
May 16, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 0 additions & 15 deletions internal/compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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})
Expand Down
13 changes: 10 additions & 3 deletions internal/templater/templater.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package templater

import (
"bytes"
"fmt"
"maps"
"strings"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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),
}
}

Expand Down
45 changes: 45 additions & 0 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
})
}
}
43 changes: 23 additions & 20 deletions taskfile/ast/var.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@ type Var struct {
Live any
Sh string
Ref string
Json string
Yaml string
Dir string
}

Expand All @@ -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
Expand All @@ -114,25 +116,21 @@ 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)
}
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
Expand All @@ -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
Expand Down
74 changes: 74 additions & 0 deletions testdata/var_references/Taskfile.yml
Original file line number Diff line number Diff line change
@@ -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 -}}"
15 changes: 1 addition & 14 deletions testdata/vars/any2/Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ tasks:
- task: ref-dep
- task: ref-resolver
- task: json
- task: yaml

map:
vars:
Expand Down Expand Up @@ -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}}"
Expand Down
Loading