From 630e58767b1da15b7705d3487438b3cb72acd48e Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Wed, 24 Apr 2024 20:47:24 +0100 Subject: [PATCH] feat: ability to resolve refs using templating syntax (#1612) * feat: resolve references using templating syntax * refactor: moved when references are resolved to one place * fix: linter * docs: update map variables doc --- .golangci.yml | 2 +- go.mod | 3 +- go.sum | 2 + internal/compiler/compiler.go | 4 -- internal/templater/funcs.go | 4 +- internal/templater/templater.go | 24 +++++++++++- testdata/vars/any2/Taskfile.yml | 43 +++++++++++++++------- variables.go | 22 ----------- website/docs/experiments/map_variables.mdx | 27 ++++++++++++-- 9 files changed, 84 insertions(+), 47 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index c23b1bef..cc3bc235 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,7 +11,7 @@ linters: linters-settings: goimports: - local-prefixes: github.com/go-task/task + local-prefixes: github.com/go-task gofmt: rewrite-rules: - pattern: 'interface{}' diff --git a/go.mod b/go.mod index 4d26524e..c83c2481 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/go-task/task/v3 -go 1.21 +go 1.21.0 require ( github.com/Masterminds/semver/v3 v3.2.1 @@ -8,6 +8,7 @@ require ( github.com/dominikbraun/graph v0.23.0 github.com/fatih/color v1.16.0 github.com/go-task/slim-sprig/v3 v3.0.0 + github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90 github.com/joho/godotenv v1.5.1 github.com/mattn/go-zglob v0.0.4 github.com/mitchellh/hashstructure/v2 v2.0.2 diff --git a/go.sum b/go.sum index 97e941ec..6e834b09 100644 --- a/go.sum +++ b/go.sum @@ -12,6 +12,8 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90 h1:JBbiZ2CXIZ9Upe3O2yI5+3ksWoa7hNVNi4BINs8TIrs= +github.com/go-task/template v0.0.0-20240422130016-8f6b279b1e90/go.mod h1:RgwRaZK+kni/hJJ7/AaOE2lPQFPbAdji/DyhC6pxo4k= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go index c3dfaee0..649f0ff7 100644 --- a/internal/compiler/compiler.go +++ b/internal/compiler/compiler.go @@ -62,10 +62,6 @@ func (c *Compiler) getVariables(t *ast.Task, call *ast.Call, evaluateShVars bool cache := &templater.Cache{Vars: result} // Replace values newVar := templater.ReplaceVar(v, cache) - // If the variable is a reference, we can resolve it - if newVar.Ref != "" { - newVar.Value = result.Get(newVar.Ref).Value - } // If the variable should not be evaluated, but is nil, set it to an empty string // This stops empty interface errors when using the templater to replace values later if !evaluateShVars && newVar.Value == nil { diff --git a/internal/templater/funcs.go b/internal/templater/funcs.go index 4a4e412e..1d9d9068 100644 --- a/internal/templater/funcs.go +++ b/internal/templater/funcs.go @@ -4,13 +4,13 @@ import ( "path/filepath" "runtime" "strings" - "text/template" "github.com/davecgh/go-spew/spew" "mvdan.cc/sh/v3/shell" "mvdan.cc/sh/v3/syntax" sprig "github.com/go-task/slim-sprig/v3" + "github.com/go-task/template" ) var templateFuncs template.FuncMap @@ -82,7 +82,7 @@ func init() { taskFuncs["ToSlash"] = taskFuncs["toSlash"] taskFuncs["ExeExt"] = taskFuncs["exeExt"] - templateFuncs = sprig.TxtFuncMap() + templateFuncs = template.FuncMap(sprig.TxtFuncMap()) for k, v := range taskFuncs { templateFuncs[k] = v } diff --git a/internal/templater/templater.go b/internal/templater/templater.go index 58b7d71a..2e12cbb8 100644 --- a/internal/templater/templater.go +++ b/internal/templater/templater.go @@ -4,10 +4,10 @@ import ( "bytes" "maps" "strings" - "text/template" "github.com/go-task/task/v3/internal/deepcopy" "github.com/go-task/task/v3/taskfile/ast" + "github.com/go-task/template" ) // Cache is a help struct that allow us to call "replaceX" funcs multiple @@ -29,6 +29,25 @@ func (r *Cache) Err() error { return r.err } +func ResolveRef(ref string, cache *Cache) any { + // If there is already an error, do nothing + if cache.err != nil { + return nil + } + + // Initialize the cache map if it's not already initialized + if cache.cacheMap == nil { + cache.cacheMap = cache.Vars.ToCacheMap() + } + + val, err := template.ResolveRef(ref, cache.cacheMap) + if err != nil { + cache.err = err + return nil + } + return val +} + func Replace[T any](v T, cache *Cache) T { return ReplaceWithExtra(v, cache, nil) } @@ -91,6 +110,9 @@ func ReplaceVar(v ast.Var, cache *Cache) ast.Var { } func ReplaceVarWithExtra(v ast.Var, cache *Cache, extra map[string]any) ast.Var { + if v.Ref != "" { + return ast.Var{Value: ResolveRef(v.Ref, cache)} + } return ast.Var{ Value: ReplaceWithExtra(v.Value, cache, extra), Sh: ReplaceWithExtra(v.Sh, cache, extra), diff --git a/testdata/vars/any2/Taskfile.yml b/testdata/vars/any2/Taskfile.yml index 4a11ff86..f3e8b80e 100644 --- a/testdata/vars/any2/Taskfile.yml +++ b/testdata/vars/any2/Taskfile.yml @@ -8,6 +8,7 @@ tasks: - task: ref - task: ref-sh - task: ref-dep + - task: ref-resolver - task: json - task: yaml @@ -16,10 +17,10 @@ tasks: MAP: map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} cmds: - - task: print-var + - task: print-story vars: VAR: - ref: MAP + ref: .MAP nested-map: vars: @@ -44,12 +45,12 @@ tasks: MAP: map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} MAP_REF: - ref: MAP + ref: .MAP cmds: - - task: print-var + - task: print-story vars: VAR: - ref: MAP_REF + ref: .MAP_REF ref-sh: vars: @@ -58,22 +59,34 @@ tasks: JSON: json: "{{.JSON_STRING}}" MAP_REF: - ref: JSON + ref: .JSON cmds: - - task: print-var + - task: print-story vars: VAR: - ref: MAP_REF + ref: .MAP_REF ref-dep: vars: MAP: map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} deps: + - task: print-story + vars: + VAR: + ref: .MAP + + ref-resolver: + vars: + MAP: + map: {"name":"Alice","age":30,"children":[{"name":"Bob","age":5},{"name":"Charlie","age":3},{"name":"Diane","age":1}]} + MAP_REF: + ref: .MAP + cmds: - task: print-var vars: VAR: - ref: MAP + ref: (index .MAP_REF.children 0).name json: vars: @@ -82,10 +95,10 @@ tasks: JSON: json: "{{.JSON_STRING}}" cmds: - - task: print-var + - task: print-story vars: VAR: - ref: JSON + ref: .JSON yaml: vars: @@ -94,12 +107,16 @@ tasks: YAML: yaml: "{{.YAML_STRING}}" cmds: - - task: print-var + - task: print-story vars: VAR: - ref: YAML + ref: .YAML print-var: + cmds: + - echo "{{.VAR}}" + + print-story: cmds: - >- echo "{{.VAR.name}} has {{len .VAR.children}} children called diff --git a/variables.go b/variables.go index 33b31fcf..62342301 100644 --- a/variables.go +++ b/variables.go @@ -164,17 +164,6 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task, newCmd.Cmd = templater.Replace(cmd.Cmd, cache) newCmd.Task = templater.Replace(cmd.Task, cache) newCmd.Vars = templater.ReplaceVars(cmd.Vars, cache) - // Loop over the command's variables and resolve any references to other variables - err := cmd.Vars.Range(func(k string, v ast.Var) error { - if v.Ref != "" { - refVal := vars.Get(v.Ref) - newCmd.Vars.Set(k, refVal) - } - return nil - }) - if err != nil { - return nil, err - } new.Cmds = append(new.Cmds, newCmd) } } @@ -214,17 +203,6 @@ func (e *Executor) compiledTask(call *ast.Call, evaluateShVars bool) (*ast.Task, newDep := dep.DeepCopy() newDep.Task = templater.Replace(dep.Task, cache) newDep.Vars = templater.ReplaceVars(dep.Vars, cache) - // Loop over the dep's variables and resolve any references to other variables - err := dep.Vars.Range(func(k string, v ast.Var) error { - if v.Ref != "" { - refVal := vars.Get(v.Ref) - newDep.Vars.Set(k, refVal) - } - return nil - }) - if err != nil { - return nil, err - } new.Deps = append(new.Deps, newDep) } } diff --git a/website/docs/experiments/map_variables.mdx b/website/docs/experiments/map_variables.mdx index f607519b..5be75d52 100644 --- a/website/docs/experiments/map_variables.mdx +++ b/website/docs/experiments/map_variables.mdx @@ -222,7 +222,7 @@ tasks: - task: bar vars: FOO: - ref: FOO # <-- FOO gets passed by reference to bar and maintains its type + 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 @@ -242,17 +242,38 @@ tasks: vars: FOO: [A, B, C] # <-- FOO is defined as an array BAR: - ref: FOO # <-- BAR is defined as a reference to FOO + 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 + 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