diff --git a/internal/compiler/v3/compiler_v3.go b/internal/compiler/v3/compiler_v3.go new file mode 100644 index 00000000..85e68d90 --- /dev/null +++ b/internal/compiler/v3/compiler_v3.go @@ -0,0 +1,98 @@ +package v3 + +import ( + "bytes" + "context" + "fmt" + "strings" + "sync" + + "github.com/go-task/task/v2/internal/compiler" + "github.com/go-task/task/v2/internal/execext" + "github.com/go-task/task/v2/internal/logger" + "github.com/go-task/task/v2/internal/taskfile" + "github.com/go-task/task/v2/internal/templater" +) + +var _ compiler.Compiler = &CompilerV3{} + +type CompilerV3 struct { + Dir string + + TaskfileVars *taskfile.Vars + + Logger *logger.Logger + + dynamicCache map[string]string + muDynamicCache sync.Mutex +} + +func (c *CompilerV3) GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) { + result := compiler.GetEnviron() + result.Set("TASK", taskfile.Var{Static: t.Task}) + + rangeFunc := func(k string, v taskfile.Var) error { + tr := templater.Templater{Vars: result, RemoveNoValue: true} + v = taskfile.Var{ + Static: tr.Replace(v.Static), + Sh: tr.Replace(v.Sh), + } + if err := tr.Err(); err != nil { + return err + } + static, err := c.HandleDynamicVar(v) + if err != nil { + return err + } + result.Set(k, taskfile.Var{Static: static}) + return nil + } + + if err := c.TaskfileVars.Range(rangeFunc); err != nil { + return nil, err + } + if err := call.Vars.Range(rangeFunc); err != nil { + return nil, err + } + if err := t.Vars.Range(rangeFunc); err != nil { + return nil, err + } + + return result, nil +} + +func (c *CompilerV3) HandleDynamicVar(v taskfile.Var) (string, error) { + if v.Static != "" || v.Sh == "" { + return v.Static, nil + } + + c.muDynamicCache.Lock() + defer c.muDynamicCache.Unlock() + + if c.dynamicCache == nil { + c.dynamicCache = make(map[string]string, 30) + } + if result, ok := c.dynamicCache[v.Sh]; ok { + return result, nil + } + + var stdout bytes.Buffer + opts := &execext.RunCommandOptions{ + Command: v.Sh, + Dir: c.Dir, + Stdout: &stdout, + Stderr: c.Logger.Stderr, + } + if err := execext.RunCommand(context.Background(), opts); err != nil { + return "", fmt.Errorf(`task: Command "%s" in taskvars file failed: %s`, opts.Command, err) + } + + // Trim a single trailing newline from the result to make most command + // output easier to use in shell commands. + result := strings.TrimSuffix(stdout.String(), "\n") + + c.dynamicCache[v.Sh] = result + c.Logger.VerboseErrf(logger.Magenta, `task: dynamic variable: '%s' result: '%s'`, v.Sh, result) + + return result, nil +} diff --git a/internal/templater/templater.go b/internal/templater/templater.go index b905b1ba..85de72fb 100644 --- a/internal/templater/templater.go +++ b/internal/templater/templater.go @@ -2,6 +2,7 @@ package templater import ( "bytes" + "strings" "text/template" "github.com/go-task/task/v2/internal/taskfile" @@ -12,7 +13,8 @@ import ( // happen will be assigned to r.err, and consecutive calls to funcs will just // return the zero value. type Templater struct { - Vars *taskfile.Vars + Vars *taskfile.Vars + RemoveNoValue bool cacheMap map[string]interface{} err error @@ -42,6 +44,9 @@ func (r *Templater) Replace(str string) string { r.err = err return "" } + if r.RemoveNoValue { + return strings.ReplaceAll(b.String(), "", "") + } return b.String() } diff --git a/task.go b/task.go index 0e3eb327..836fd027 100644 --- a/task.go +++ b/task.go @@ -12,6 +12,7 @@ import ( "github.com/go-task/task/v2/internal/compiler" compilerv2 "github.com/go-task/task/v2/internal/compiler/v2" + compilerv3 "github.com/go-task/task/v2/internal/compiler/v3" "github.com/go-task/task/v2/internal/execext" "github.com/go-task/task/v2/internal/logger" "github.com/go-task/task/v2/internal/output" @@ -131,9 +132,9 @@ func (e *Executor) Setup() error { Color: e.Color, } - v, err := strconv.ParseFloat(e.Taskfile.Version, 64) + v, err := e.parsedVersion() if err != nil { - return fmt.Errorf(`task: Could not parse taskfile version "%s": %v`, e.Taskfile.Version, err) + } if v < 2 { @@ -154,12 +155,20 @@ func (e *Executor) Setup() error { e.Logger.Color = false } - e.Compiler = &compilerv2.CompilerV2{ - Dir: e.Dir, - Taskvars: e.taskvars, - TaskfileVars: e.Taskfile.Vars, - Expansions: e.Taskfile.Expansions, - Logger: e.Logger, + if v < 3 { + e.Compiler = &compilerv2.CompilerV2{ + Dir: e.Dir, + Taskvars: e.taskvars, + TaskfileVars: e.Taskfile.Vars, + Expansions: e.Taskfile.Expansions, + Logger: e.Logger, + } + } else { + e.Compiler = &compilerv3.CompilerV3{ + Dir: e.Dir, + TaskfileVars: e.Taskfile.Vars, + Logger: e.Logger, + } } if v < 2.1 && e.Taskfile.Output != "" { @@ -231,6 +240,14 @@ func (e *Executor) Setup() error { return nil } +func (e *Executor) parsedVersion() (float64, error) { + v, err := strconv.ParseFloat(e.Taskfile.Version, 64) + if err != nil { + return 0, fmt.Errorf(`task: Could not parse taskfile version "%s": %v`, e.Taskfile.Version, err) + } + return v, nil +} + // RunTask runs a task by its name func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { t, err := e.CompiledTask(call) diff --git a/task_test.go b/task_test.go index c752e95b..ef985361 100644 --- a/task_test.go +++ b/task_test.go @@ -107,6 +107,20 @@ func TestVarsV2(t *testing.T) { tt.Run(t) } +func TestVarsV3(t *testing.T) { + tt := fileContentTest{ + Dir: "testdata/vars/v3", + Target: "default", + Files: map[string]string{ + "missing-var.txt": "\n", + "var-order.txt": "ABCDEF\n", + "dependent-sh.txt": "123456\n", + "with-call.txt": "Hi, ABC123!\n", + }, + } + tt.Run(t) +} + func TestMultilineVars(t *testing.T) { for _, dir := range []string{"testdata/vars/v2/multiline"} { tt := fileContentTest{ diff --git a/testdata/checksum/Taskfile.yml b/testdata/checksum/Taskfile.yml index 1c620a71..d2de1086 100644 --- a/testdata/checksum/Taskfile.yml +++ b/testdata/checksum/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: build: diff --git a/testdata/cyclic/Taskfile.yml b/testdata/cyclic/Taskfile.yml index 0da55536..83cf39db 100644 --- a/testdata/cyclic/Taskfile.yml +++ b/testdata/cyclic/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: task-1: diff --git a/testdata/deps/Taskfile.yml b/testdata/deps/Taskfile.yml index 411eeb91..bc2aa71c 100644 --- a/testdata/deps/Taskfile.yml +++ b/testdata/deps/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: default: diff --git a/testdata/dir/Taskfile.yml b/testdata/dir/Taskfile.yml index f17dd9fe..8e740dbe 100644 --- a/testdata/dir/Taskfile.yml +++ b/testdata/dir/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: whereami: diff --git a/testdata/dir/explicit_doesnt_exist/Taskfile.yml b/testdata/dir/explicit_doesnt_exist/Taskfile.yml index 1b6fb7d1..0f0fb52c 100644 --- a/testdata/dir/explicit_doesnt_exist/Taskfile.yml +++ b/testdata/dir/explicit_doesnt_exist/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: whereami: diff --git a/testdata/dir/explicit_exists/Taskfile.yml b/testdata/dir/explicit_exists/Taskfile.yml index 0ab53b26..a1797562 100644 --- a/testdata/dir/explicit_exists/Taskfile.yml +++ b/testdata/dir/explicit_exists/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: whereami: diff --git a/testdata/dry/Taskfile.yml b/testdata/dry/Taskfile.yml index b4e932ed..81c7e0d1 100644 --- a/testdata/dry/Taskfile.yml +++ b/testdata/dry/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: build: diff --git a/testdata/dry_checksum/Taskfile.yml b/testdata/dry_checksum/Taskfile.yml index 057816b3..f58cf0ef 100644 --- a/testdata/dry_checksum/Taskfile.yml +++ b/testdata/dry_checksum/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: default: diff --git a/testdata/env/Taskfile.yml b/testdata/env/Taskfile.yml index d5a7419d..cb88f95e 100644 --- a/testdata/env/Taskfile.yml +++ b/testdata/env/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' vars: BAZ: diff --git a/testdata/expand/Taskfile.yml b/testdata/expand/Taskfile.yml index 7398407c..967490ac 100644 --- a/testdata/expand/Taskfile.yml +++ b/testdata/expand/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: pwd: diff --git a/testdata/generates/Taskfile.yml b/testdata/generates/Taskfile.yml index 9f05cfd0..4dc6e9ec 100644 --- a/testdata/generates/Taskfile.yml +++ b/testdata/generates/Taskfile.yml @@ -1,4 +1,7 @@ -version: '2' +version: '3' + +vars: + BUILD_DIR: $pwd tasks: abs.txt: diff --git a/testdata/generates/Taskvars.yml b/testdata/generates/Taskvars.yml deleted file mode 100644 index 0907174f..00000000 --- a/testdata/generates/Taskvars.yml +++ /dev/null @@ -1 +0,0 @@ -BUILD_DIR: $pwd diff --git a/testdata/ignore_errors/Taskfile.yml b/testdata/ignore_errors/Taskfile.yml index 1d7dc8b5..31b82a70 100644 --- a/testdata/ignore_errors/Taskfile.yml +++ b/testdata/ignore_errors/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: task-should-pass: diff --git a/testdata/includes_call_root_task/Taskfile.yml b/testdata/includes_call_root_task/Taskfile.yml index 41e60ad0..26373378 100644 --- a/testdata/includes_call_root_task/Taskfile.yml +++ b/testdata/includes_call_root_task/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' includes: included: Taskfile2.yml diff --git a/testdata/includes_call_root_task/Taskfile2.yml b/testdata/includes_call_root_task/Taskfile2.yml index 99d5cb3a..b520bc4b 100644 --- a/testdata/includes_call_root_task/Taskfile2.yml +++ b/testdata/includes_call_root_task/Taskfile2.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: call-root: diff --git a/testdata/includes_deps/Taskfile.yml b/testdata/includes_deps/Taskfile.yml index e6d4d186..bed642bb 100644 --- a/testdata/includes_deps/Taskfile.yml +++ b/testdata/includes_deps/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' includes: included: Taskfile2.yml diff --git a/testdata/includes_deps/Taskfile2.yml b/testdata/includes_deps/Taskfile2.yml index 27eb2b97..c5e704a7 100644 --- a/testdata/includes_deps/Taskfile2.yml +++ b/testdata/includes_deps/Taskfile2.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: default: diff --git a/testdata/includes_empty/Taskfile.yml b/testdata/includes_empty/Taskfile.yml index 2d24e533..a58ab424 100644 --- a/testdata/includes_empty/Taskfile.yml +++ b/testdata/includes_empty/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' includes: included: Taskfile2.yml diff --git a/testdata/includes_empty/Taskfile2.yml b/testdata/includes_empty/Taskfile2.yml index 825285aa..7e984ef7 100644 --- a/testdata/includes_empty/Taskfile2.yml +++ b/testdata/includes_empty/Taskfile2.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' vars: FILE: file.txt diff --git a/testdata/params/Taskfile.yml b/testdata/params/Taskfile.yml index f8d9a91a..06cbe488 100644 --- a/testdata/params/Taskfile.yml +++ b/testdata/params/Taskfile.yml @@ -1,4 +1,8 @@ -version: '2' +version: '3' + +vars: + PORTUGUESE_HELLO_WORLD: Olá, mundo! + GERMAN: Hello tasks: default: diff --git a/testdata/params/Taskvars.yml b/testdata/params/Taskvars.yml deleted file mode 100644 index af0a0efb..00000000 --- a/testdata/params/Taskvars.yml +++ /dev/null @@ -1,2 +0,0 @@ -PORTUGUESE_HELLO_WORLD: Olá, mundo! -GERMAN: "Hello" diff --git a/testdata/precondition/Taskfile.yml b/testdata/precondition/Taskfile.yml index eedc588f..669eb9d8 100644 --- a/testdata/precondition/Taskfile.yml +++ b/testdata/precondition/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: foo: diff --git a/testdata/status/Taskfile.yml b/testdata/status/Taskfile.yml index 57900efc..7923c426 100644 --- a/testdata/status/Taskfile.yml +++ b/testdata/status/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: gen-foo: diff --git a/testdata/summary/Taskfile.yml b/testdata/summary/Taskfile.yml index 3e4d4640..7b857f76 100644 --- a/testdata/summary/Taskfile.yml +++ b/testdata/summary/Taskfile.yml @@ -1,4 +1,4 @@ -version: '2' +version: '3' tasks: task-with-summary: diff --git a/testdata/vars/v3/.gitignore b/testdata/vars/v3/.gitignore new file mode 100644 index 00000000..2211df63 --- /dev/null +++ b/testdata/vars/v3/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/testdata/vars/v3/Taskfile.yml b/testdata/vars/v3/Taskfile.yml new file mode 100644 index 00000000..4c581115 --- /dev/null +++ b/testdata/vars/v3/Taskfile.yml @@ -0,0 +1,46 @@ +version: '3' + +vars: + VAR_A: A + VAR_B: '{{.VAR_A}}B' + VAR_C: '{{.VAR_B}}C' + + VAR_1: {sh: echo 1} + VAR_2: {sh: 'echo "{{.VAR_1}}2"'} + VAR_3: {sh: 'echo "{{.VAR_2}}3"'} + +tasks: + default: + - task: missing-var + - task: var-order + - task: dependent-sh + - task: with-call + + missing-var: echo '{{.NON_EXISTING_VAR}}' > missing-var.txt + + var-order: + vars: + VAR_D: '{{.VAR_C}}D' + VAR_E: '{{.VAR_D}}E' + VAR_F: '{{.VAR_E}}F' + cmds: + - echo '{{.VAR_F}}' > var-order.txt + + dependent-sh: + vars: + VAR_4: {sh: 'echo "{{.VAR_3}}4"'} + VAR_5: {sh: 'echo "{{.VAR_4}}5"'} + VAR_6: {sh: 'echo "{{.VAR_5}}6"'} + cmds: + - echo '{{.VAR_6}}' > dependent-sh.txt + + with-call: + - task: called-task + vars: + ABC123: '{{.VAR_C}}{{.VAR_3}}' + + called-task: + vars: + MESSAGE: Hi, {{.ABC123}}! + cmds: + - echo "{{.MESSAGE}}" > with-call.txt diff --git a/variables.go b/variables.go index a2ef97d8..569f56ed 100644 --- a/variables.go +++ b/variables.go @@ -23,7 +23,12 @@ func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { return nil, err } - r := templater.Templater{Vars: vars} + v, err := e.parsedVersion() + if err != nil { + return nil, err + } + + r := templater.Templater{Vars: vars, RemoveNoValue: v >= 3.0} new := taskfile.Task{ Task: origTask.Task,