1
0
mirror of https://github.com/go-task/task.git synced 2025-06-17 00:17:51 +02:00

feat: remove v2 support (#1447)

* feat: remove v2 support

* docs: update v2 schema docs
This commit is contained in:
Pete Davison
2023-12-29 20:26:02 +00:00
committed by GitHub
parent 212ff42304
commit 2b67d05b9d
31 changed files with 317 additions and 990 deletions

View File

@ -6,8 +6,8 @@ import (
"github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile"
) )
// ParseV3 parses command line argument: tasks and global variables // Parse parses command line argument: tasks and global variables
func ParseV3(args ...string) ([]taskfile.Call, *taskfile.Vars) { func Parse(args ...string) ([]taskfile.Call, *taskfile.Vars) {
calls := []taskfile.Call{} calls := []taskfile.Call{}
globals := &taskfile.Vars{} globals := &taskfile.Vars{}
@ -24,32 +24,6 @@ func ParseV3(args ...string) ([]taskfile.Call, *taskfile.Vars) {
return calls, globals return calls, globals
} }
// ParseV2 parses command line argument: tasks and vars of each task
func ParseV2(args ...string) ([]taskfile.Call, *taskfile.Vars) {
calls := []taskfile.Call{}
globals := &taskfile.Vars{}
for _, arg := range args {
if !strings.Contains(arg, "=") {
calls = append(calls, taskfile.Call{Task: arg, Direct: true})
continue
}
if len(calls) < 1 {
name, value := splitVar(arg)
globals.Set(name, taskfile.Var{Value: value})
} else {
if calls[len(calls)-1].Vars == nil {
calls[len(calls)-1].Vars = &taskfile.Vars{}
}
name, value := splitVar(arg)
calls[len(calls)-1].Vars.Set(name, taskfile.Var{Value: value})
}
}
return calls, globals
}
func splitVar(s string) (string, string) { func splitVar(s string) (string, string) {
pair := strings.SplitN(s, "=", 2) pair := strings.SplitN(s, "=", 2)
return pair[0], pair[1] return pair[0], pair[1]

View File

@ -11,7 +11,7 @@ import (
"github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile"
) )
func TestArgsV3(t *testing.T) { func TestArgs(t *testing.T) {
tests := []struct { tests := []struct {
Args []string Args []string
ExpectedCalls []taskfile.Call ExpectedCalls []taskfile.Call
@ -97,7 +97,7 @@ func TestArgsV3(t *testing.T) {
for i, test := range tests { for i, test := range tests {
t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) { t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) {
calls, globals := args.ParseV3(test.Args...) calls, globals := args.Parse(test.Args...)
assert.Equal(t, test.ExpectedCalls, calls) assert.Equal(t, test.ExpectedCalls, calls)
if test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 { if test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 {
assert.Equal(t, test.ExpectedGlobals.Keys(), globals.Keys()) assert.Equal(t, test.ExpectedGlobals.Keys(), globals.Keys())
@ -106,114 +106,3 @@ func TestArgsV3(t *testing.T) {
}) })
} }
} }
func TestArgsV2(t *testing.T) {
tests := []struct {
Args []string
ExpectedCalls []taskfile.Call
ExpectedGlobals *taskfile.Vars
}{
{
Args: []string{"task-a", "task-b", "task-c"},
ExpectedCalls: []taskfile.Call{
{Task: "task-a", Direct: true},
{Task: "task-b", Direct: true},
{Task: "task-c", Direct: true},
},
},
{
Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"},
ExpectedCalls: []taskfile.Call{
{
Task: "task-a",
Direct: true,
Vars: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Value: "bar"},
},
[]string{"FOO"},
),
},
},
{Task: "task-b", Direct: true},
{
Task: "task-c",
Direct: true,
Vars: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"BAR": {Value: "baz"},
"BAZ": {Value: "foo"},
},
[]string{"BAR", "BAZ"},
),
},
},
},
},
{
Args: []string{"task-a", "CONTENT=with some spaces"},
ExpectedCalls: []taskfile.Call{
{
Task: "task-a",
Direct: true,
Vars: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"CONTENT": {Value: "with some spaces"},
},
[]string{"CONTENT"},
),
},
},
},
},
{
Args: []string{"FOO=bar", "task-a", "task-b"},
ExpectedCalls: []taskfile.Call{
{Task: "task-a", Direct: true},
{Task: "task-b", Direct: true},
},
ExpectedGlobals: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Value: "bar"},
},
[]string{"FOO"},
),
},
},
{
Args: nil,
ExpectedCalls: []taskfile.Call{},
},
{
Args: []string{},
ExpectedCalls: []taskfile.Call{},
},
{
Args: []string{"FOO=bar", "BAR=baz"},
ExpectedCalls: []taskfile.Call{},
ExpectedGlobals: &taskfile.Vars{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]taskfile.Var{
"FOO": {Value: "bar"},
"BAR": {Value: "baz"},
},
[]string{"FOO", "BAR"},
),
},
},
}
for i, test := range tests {
t.Run(fmt.Sprintf("TestArgs%d", i+1), func(t *testing.T) {
calls, globals := args.ParseV2(test.Args...)
assert.Equal(t, test.ExpectedCalls, calls)
if test.ExpectedGlobals.Len() > 0 || globals.Len() > 0 {
assert.Equal(t, test.ExpectedGlobals, globals)
}
})
}
}

View File

@ -300,11 +300,7 @@ func run() error {
return err return err
} }
if e.Taskfile.Version.Compare(taskfile.V3) >= 0 { calls, globals = args.Parse(tasksAndVars...)
calls, globals = args.ParseV3(tasksAndVars...)
} else {
calls, globals = args.ParseV2(tasksAndVars...)
}
// If there are no calls, run the default task instead // If there are no calls, run the default task instead
if len(calls) == 0 { if len(calls) == 0 {

View File

@ -13,18 +13,21 @@ This deprecation breaks the following functionality:
::: :::
The Taskfile v2 schema was introduced in March 2018 and replaced by version 3 in The Taskfile version 2 schema was introduced in March 2018 and replaced by
August the following year. Users have had a long time to update and so we feel version 3 in August 2019. In May 2023 [we published a deprecation
that it is time to tidy up the codebase and focus on new functionality instead. notice][deprecation-notice] for the version 2 schema on the basis that the vast
majority of users had already upgraded to version 3 and removing support for
version 2 would allow us to tidy up the codebase and focus on new functionality
instead.
This notice does not mean that we are immediately removing support for version 2 In December 2023, the final version of Task that supports the version 2 schema
schemas. However, support will not be extended to future major releases and we ([v3.33.0][v3.33.0]) was published and all legacy code was removed from Task's
_strongly recommend_ that anybody still using a version 2 schema upgrades to main branch. To use a more recent version of Task, you will need to ensure that
version 3 as soon as possible. your Taskfile uses the version 3 schema instead. A list of changes between
version 2 and version 3 are available in the [Task v3 Release Notes][v3.0.0].
A list of changes between version 2 and version 3 are available in the [Task v3
Release Notes][version-3-release-notes].
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[version-3-release-notes]: https://github.com/go-task/task/releases/tag/v3.0.0 [v3.0.0]: https://github.com/go-task/task/releases/tag/v3.0.0
[v3.33.0]: https://github.com/go-task/task/releases/tag/v3.33.0
[deprecation-notice]: https://github.com/go-task/task/issues/1197
<!-- prettier-ignore-end --> <!-- prettier-ignore-end -->

View File

@ -82,8 +82,8 @@ tasks:
:::caution :::caution
v2 schema support is [deprecated][deprecate-version-2-schema] and will be v2 schemas are [no longer supported by the latest version of
removed in a future release. Task][deprecate-version-2-schema].
::: :::
@ -106,8 +106,8 @@ Please check the [documentation][includes]
:::caution :::caution
v2 schema support is [deprecated][deprecate-version-2-schema] and will be v2 schemas are [no longer supported by the latest version of
removed in a future release. Task][deprecate-version-2-schema].
::: :::
@ -125,8 +125,8 @@ includes:
:::caution :::caution
v2 schema support is [deprecated][deprecate-version-2-schema] and will be v2 schemas are [no longer supported by the latest version of
removed in a future release. Task][deprecate-version-2-schema].
::: :::
@ -170,8 +170,8 @@ tasks:
:::caution :::caution
v2 schema support is [deprecated][deprecate-version-2-schema] and will be v2 schemas are [no longer supported by the latest version of
removed in a future release. Task][deprecate-version-2-schema].
::: :::
@ -256,7 +256,7 @@ The variable priority order was also different:
4. `Taskvars.yml` variables 4. `Taskvars.yml` variables
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
[deprecate-version-2-schema]: https://github.com/go-task/task/issues/1197 [deprecate-version-2-schema]: /deprecations/version-2-schema/
[output]: /usage#output-syntax [output]: /usage#output-syntax
[ignore_errors]: /usage#ignore-errors [ignore_errors]: /usage#ignore-errors
[includes]: /usage#including-other-taskfiles [includes]: /usage#including-other-taskfiles

View File

@ -1,15 +1,207 @@
package compiler package compiler
import ( import (
"bytes"
"context"
"fmt"
"strings"
"sync"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile"
) )
// Compiler handles compilation of a task before its execution. type Compiler struct {
// E.g. variable merger, template processing, etc. Dir string
type Compiler interface { UserWorkingDir string
GetTaskfileVariables() (*taskfile.Vars, error)
GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) TaskfileEnv *taskfile.Vars
FastGetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) TaskfileVars *taskfile.Vars
HandleDynamicVar(v taskfile.Var, dir string) (string, error)
ResetCache() Logger *logger.Logger
dynamicCache map[string]string
muDynamicCache sync.Mutex
}
func (c *Compiler) GetTaskfileVariables() (*taskfile.Vars, error) {
return c.getVariables(nil, nil, true)
}
func (c *Compiler) GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) {
return c.getVariables(t, &call, true)
}
func (c *Compiler) FastGetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) {
return c.getVariables(t, &call, false)
}
func (c *Compiler) getVariables(t *taskfile.Task, call *taskfile.Call, evaluateShVars bool) (*taskfile.Vars, error) {
result := GetEnviron()
if t != nil {
specialVars, err := c.getSpecialVars(t)
if err != nil {
return nil, err
}
for k, v := range specialVars {
result.Set(k, taskfile.Var{Value: v})
}
}
getRangeFunc := func(dir string) func(k string, v taskfile.Var) error {
return func(k string, v taskfile.Var) error {
tr := templater.Templater{Vars: result}
// Replace values
newVar := taskfile.Var{}
switch value := v.Value.(type) {
case string:
newVar.Value = tr.Replace(value)
default:
newVar.Value = value
}
newVar.Sh = tr.Replace(v.Sh)
newVar.Dir = v.Dir
// 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 {
result.Set(k, taskfile.Var{Value: ""})
return nil
}
// If the variable should not be evaluated and it is set, we can set it and return
if !evaluateShVars {
result.Set(k, taskfile.Var{Value: newVar.Value})
return nil
}
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
if err := tr.Err(); 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, taskfile.Var{Value: newVar.Value})
return nil
}
// If the variable is dynamic, we need to resolve it first
static, err := c.HandleDynamicVar(newVar, dir)
if err != nil {
return err
}
result.Set(k, taskfile.Var{Value: static})
return nil
}
}
rangeFunc := getRangeFunc(c.Dir)
var taskRangeFunc func(k string, v taskfile.Var) error
if t != nil {
// NOTE(@andreynering): We're manually joining these paths here because
// this is the raw task, not the compiled one.
tr := templater.Templater{Vars: result}
dir := tr.Replace(t.Dir)
if err := tr.Err(); err != nil {
return nil, err
}
dir = filepathext.SmartJoin(c.Dir, dir)
taskRangeFunc = getRangeFunc(dir)
}
if err := c.TaskfileEnv.Range(rangeFunc); err != nil {
return nil, err
}
if err := c.TaskfileVars.Range(rangeFunc); err != nil {
return nil, err
}
if t != nil {
if err := t.IncludedTaskfileVars.Range(taskRangeFunc); err != nil {
return nil, err
}
if err := t.IncludeVars.Range(rangeFunc); err != nil {
return nil, err
}
}
if t == nil || call == nil {
return result, nil
}
if err := call.Vars.Range(rangeFunc); err != nil {
return nil, err
}
if err := t.Vars.Range(taskRangeFunc); err != nil {
return nil, err
}
return result, nil
}
func (c *Compiler) HandleDynamicVar(v taskfile.Var, dir string) (string, error) {
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
}
// NOTE(@andreynering): If a var have a specific dir, use this instead
if v.Dir != "" {
dir = v.Dir
}
var stdout bytes.Buffer
opts := &execext.RunCommandOptions{
Command: v.Sh,
Dir: dir,
Stdout: &stdout,
Stderr: c.Logger.Stderr,
}
if err := execext.RunCommand(context.Background(), opts); err != nil {
return "", fmt.Errorf(`task: Command "%s" 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(), "\r\n")
result = strings.TrimSuffix(result, "\n")
c.dynamicCache[v.Sh] = result
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", v.Sh, result)
return result, nil
}
// ResetCache clear the dymanic variables cache
func (c *Compiler) ResetCache() {
c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock()
c.dynamicCache = nil
}
func (c *Compiler) getSpecialVars(t *taskfile.Task) (map[string]string, error) {
taskfileDir, err := c.getTaskfileDir(t)
if err != nil {
return nil, err
}
return map[string]string{
"TASK": t.Task,
"ROOT_DIR": c.Dir,
"TASKFILE_DIR": taskfileDir,
"USER_WORKING_DIR": c.UserWorkingDir,
"TASK_VERSION": version.GetVersion(),
}, nil
}
func (c *Compiler) getTaskfileDir(t *taskfile.Task) (string, error) {
if t.IncludedTaskfile != nil {
return t.IncludedTaskfile.FullDirPath()
}
return c.Dir, nil
} }

View File

@ -1,138 +0,0 @@
package v2
import (
"bytes"
"context"
"fmt"
"strings"
"sync"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
)
var _ compiler.Compiler = &CompilerV2{}
type CompilerV2 struct {
Dir string
Taskvars *taskfile.Vars
TaskfileVars *taskfile.Vars
Expansions int
Logger *logger.Logger
dynamicCache map[string]string
muDynamicCache sync.Mutex
}
func (c *CompilerV2) GetTaskfileVariables() (*taskfile.Vars, error) {
return &taskfile.Vars{}, nil
}
// FastGetVariables is a no-op on v2
func (c *CompilerV2) FastGetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) {
return c.GetVariables(t, call)
}
// GetVariables returns fully resolved variables following the priority order:
// 1. Task variables
// 2. Call variables
// 3. Taskfile variables
// 4. Taskvars file variables
// 5. Environment variables
func (c *CompilerV2) GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) {
vr := varResolver{
c: c,
vars: compiler.GetEnviron(),
}
vr.vars.Set("TASK", taskfile.Var{Value: t.Task})
for _, vars := range []*taskfile.Vars{c.Taskvars, c.TaskfileVars, call.Vars, t.Vars} {
for i := 0; i < c.Expansions; i++ {
vr.merge(vars)
}
}
return vr.vars, vr.err
}
type varResolver struct {
c *CompilerV2
vars *taskfile.Vars
err error
}
func (vr *varResolver) merge(vars *taskfile.Vars) {
if vr.err != nil {
return
}
tr := templater.Templater{Vars: vr.vars}
_ = vars.Range(func(k string, v taskfile.Var) error {
// Replace values
newVar := taskfile.Var{}
switch value := v.Value.(type) {
case string:
newVar.Value = tr.Replace(value)
default:
newVar.Value = value
}
newVar.Sh = tr.Replace(v.Sh)
// If the variable is not dynamic, we can set it and return
if newVar.Value != nil || newVar.Sh == "" {
vr.vars.Set(k, taskfile.Var{Value: newVar.Value})
return nil
}
// If the variable is dynamic, we need to resolve it first
static, err := vr.c.HandleDynamicVar(newVar, "")
if err != nil {
vr.err = err
return err
}
vr.vars.Set(k, taskfile.Var{Value: static})
return nil
})
vr.err = tr.Err()
}
func (c *CompilerV2) HandleDynamicVar(v taskfile.Var, _ string) (string, error) {
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,
Stdout: &stdout,
Stderr: c.Logger.Stderr,
}
if err := execext.RunCommand(context.Background(), opts); err != nil {
return "", fmt.Errorf(`task: Command "%s" 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: %q result: %q\n", v.Sh, result)
return result, nil
}
// ResetCache clear the dymanic variables cache
func (c *CompilerV2) ResetCache() {
c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock()
c.dynamicCache = nil
}

View File

@ -1,210 +0,0 @@
package v3
import (
"bytes"
"context"
"fmt"
"strings"
"sync"
"github.com/go-task/task/v3/internal/compiler"
"github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/internal/version"
"github.com/go-task/task/v3/taskfile"
)
var _ compiler.Compiler = &CompilerV3{}
type CompilerV3 struct {
Dir string
UserWorkingDir string
TaskfileEnv *taskfile.Vars
TaskfileVars *taskfile.Vars
Logger *logger.Logger
dynamicCache map[string]string
muDynamicCache sync.Mutex
}
func (c *CompilerV3) GetTaskfileVariables() (*taskfile.Vars, error) {
return c.getVariables(nil, nil, true)
}
func (c *CompilerV3) GetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) {
return c.getVariables(t, &call, true)
}
func (c *CompilerV3) FastGetVariables(t *taskfile.Task, call taskfile.Call) (*taskfile.Vars, error) {
return c.getVariables(t, &call, false)
}
func (c *CompilerV3) getVariables(t *taskfile.Task, call *taskfile.Call, evaluateShVars bool) (*taskfile.Vars, error) {
result := compiler.GetEnviron()
if t != nil {
specialVars, err := c.getSpecialVars(t)
if err != nil {
return nil, err
}
for k, v := range specialVars {
result.Set(k, taskfile.Var{Value: v})
}
}
getRangeFunc := func(dir string) func(k string, v taskfile.Var) error {
return func(k string, v taskfile.Var) error {
tr := templater.Templater{Vars: result, RemoveNoValue: true}
// Replace values
newVar := taskfile.Var{}
switch value := v.Value.(type) {
case string:
newVar.Value = tr.Replace(value)
default:
newVar.Value = value
}
newVar.Sh = tr.Replace(v.Sh)
newVar.Dir = v.Dir
// 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 {
result.Set(k, taskfile.Var{Value: ""})
return nil
}
// If the variable should not be evaluated and it is set, we can set it and return
if !evaluateShVars {
result.Set(k, taskfile.Var{Value: newVar.Value})
return nil
}
// Now we can check for errors since we've handled all the cases when we don't want to evaluate
if err := tr.Err(); 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, taskfile.Var{Value: newVar.Value})
return nil
}
// If the variable is dynamic, we need to resolve it first
static, err := c.HandleDynamicVar(newVar, dir)
if err != nil {
return err
}
result.Set(k, taskfile.Var{Value: static})
return nil
}
}
rangeFunc := getRangeFunc(c.Dir)
var taskRangeFunc func(k string, v taskfile.Var) error
if t != nil {
// NOTE(@andreynering): We're manually joining these paths here because
// this is the raw task, not the compiled one.
tr := templater.Templater{Vars: result, RemoveNoValue: true}
dir := tr.Replace(t.Dir)
if err := tr.Err(); err != nil {
return nil, err
}
dir = filepathext.SmartJoin(c.Dir, dir)
taskRangeFunc = getRangeFunc(dir)
}
if err := c.TaskfileEnv.Range(rangeFunc); err != nil {
return nil, err
}
if err := c.TaskfileVars.Range(rangeFunc); err != nil {
return nil, err
}
if t != nil {
if err := t.IncludedTaskfileVars.Range(taskRangeFunc); err != nil {
return nil, err
}
if err := t.IncludeVars.Range(rangeFunc); err != nil {
return nil, err
}
}
if t == nil || call == nil {
return result, nil
}
if err := call.Vars.Range(rangeFunc); err != nil {
return nil, err
}
if err := t.Vars.Range(taskRangeFunc); err != nil {
return nil, err
}
return result, nil
}
func (c *CompilerV3) HandleDynamicVar(v taskfile.Var, dir string) (string, error) {
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
}
// NOTE(@andreynering): If a var have a specific dir, use this instead
if v.Dir != "" {
dir = v.Dir
}
var stdout bytes.Buffer
opts := &execext.RunCommandOptions{
Command: v.Sh,
Dir: dir,
Stdout: &stdout,
Stderr: c.Logger.Stderr,
}
if err := execext.RunCommand(context.Background(), opts); err != nil {
return "", fmt.Errorf(`task: Command "%s" 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(), "\r\n")
result = strings.TrimSuffix(result, "\n")
c.dynamicCache[v.Sh] = result
c.Logger.VerboseErrf(logger.Magenta, "task: dynamic variable: %q result: %q\n", v.Sh, result)
return result, nil
}
// ResetCache clear the dymanic variables cache
func (c *CompilerV3) ResetCache() {
c.muDynamicCache.Lock()
defer c.muDynamicCache.Unlock()
c.dynamicCache = nil
}
func (c *CompilerV3) getSpecialVars(t *taskfile.Task) (map[string]string, error) {
taskfileDir, err := c.getTaskfileDir(t)
if err != nil {
return nil, err
}
return map[string]string{
"TASK": t.Task,
"ROOT_DIR": c.Dir,
"TASKFILE_DIR": taskfileDir,
"USER_WORKING_DIR": c.UserWorkingDir,
"TASK_VERSION": version.GetVersion(),
}, nil
}
func (c *CompilerV3) getTaskfileDir(t *taskfile.Task) (string, error) {
if t.IncludedTaskfile != nil {
return t.IncludedTaskfile.FullDirPath()
}
return c.Dir, nil
}

View File

@ -15,8 +15,7 @@ import (
// happen will be assigned to r.err, and consecutive calls to funcs will just // happen will be assigned to r.err, and consecutive calls to funcs will just
// return the zero value. // return the zero value.
type Templater struct { type Templater struct {
Vars *taskfile.Vars Vars *taskfile.Vars
RemoveNoValue bool
cacheMap map[string]any cacheMap map[string]any
err error err error
@ -62,10 +61,7 @@ func (r *Templater) replace(str string, extra map[string]any) string {
r.err = err r.err = err
return "" return ""
} }
if r.RemoveNoValue { return strings.ReplaceAll(b.String(), "<no value>", "")
return strings.ReplaceAll(b.String(), "<no value>", "")
}
return b.String()
} }
func (r *Templater) ReplaceSlice(strs []string) []string { func (r *Templater) ReplaceSlice(strs []string) []string {

103
setup.go
View File

@ -12,8 +12,7 @@ import (
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/sajari/fuzzy" "github.com/sajari/fuzzy"
compilerv2 "github.com/go-task/task/v3/internal/compiler/v2" "github.com/go-task/task/v3/internal/compiler"
compilerv3 "github.com/go-task/task/v3/internal/compiler/v3"
"github.com/go-task/task/v3/internal/execext" "github.com/go-task/task/v3/internal/execext"
"github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/logger"
@ -179,38 +178,21 @@ func (e *Executor) setupOutput() error {
} }
func (e *Executor) setupCompiler() error { func (e *Executor) setupCompiler() error {
if e.Taskfile.Version.LessThan(taskfile.V3) { if e.UserWorkingDir == "" {
var err error var err error
e.taskvars, err = read.Taskvars(e.Dir) e.UserWorkingDir, err = os.Getwd()
if err != nil { if err != nil {
return err return err
} }
e.Compiler = &compilerv2.CompilerV2{
Dir: e.Dir,
Taskvars: e.taskvars,
TaskfileVars: e.Taskfile.Vars,
Expansions: e.Taskfile.Expansions,
Logger: e.Logger,
}
} else {
if e.UserWorkingDir == "" {
var err error
e.UserWorkingDir, err = os.Getwd()
if err != nil {
return err
}
}
e.Compiler = &compilerv3.CompilerV3{
Dir: e.Dir,
UserWorkingDir: e.UserWorkingDir,
TaskfileEnv: e.Taskfile.Env,
TaskfileVars: e.Taskfile.Vars,
Logger: e.Logger,
}
} }
e.Compiler = &compiler.Compiler{
Dir: e.Dir,
UserWorkingDir: e.UserWorkingDir,
TaskfileEnv: e.Taskfile.Env,
TaskfileVars: e.Taskfile.Vars,
Logger: e.Logger,
}
return nil return nil
} }
@ -234,19 +216,9 @@ func (e *Executor) readDotEnvFiles() error {
} }
func (e *Executor) setupDefaults() { func (e *Executor) setupDefaults() {
// Color available only on v3
if e.Taskfile.Version.LessThan(taskfile.V3) {
e.Logger.Color = false
}
if e.Taskfile.Method == "" { if e.Taskfile.Method == "" {
if e.Taskfile.Version.Compare(taskfile.V3) >= 0 { e.Taskfile.Method = "checksum"
e.Taskfile.Method = "checksum"
} else {
e.Taskfile.Method = "timestamp"
}
} }
if e.Taskfile.Run == "" { if e.Taskfile.Run == "" {
e.Taskfile.Run = "always" e.Taskfile.Run = "always"
} }
@ -272,18 +244,11 @@ func (e *Executor) doVersionChecks() error {
v := &semver.Version{} v := &semver.Version{}
*v = *e.Taskfile.Version *v = *e.Taskfile.Version
if v.LessThan(taskfile.V2) {
return fmt.Errorf(`task: version 1 schemas are no longer supported`)
}
if v.LessThan(taskfile.V3) { if v.LessThan(taskfile.V3) {
e.Logger.Errf(logger.Yellow, "task: version 2 schemas are deprecated and will be removed in a future release\nSee https://github.com/go-task/task/issues/1197 for more details\n") return fmt.Errorf(`task: Taskfile schemas prior to v3 are no longer supported`)
} }
// consider as equal to the greater version if round // consider as equal to the greater version if round
if v.Equal(taskfile.V2) {
v = semver.MustParse("2.6")
}
if v.Equal(taskfile.V3) { if v.Equal(taskfile.V3) {
v = semver.MustParse("3.8") v = semver.MustParse("3.8")
} }
@ -292,54 +257,10 @@ func (e *Executor) doVersionChecks() error {
return fmt.Errorf(`task: Taskfile versions greater than v3.8 not implemented in the version of Task`) return fmt.Errorf(`task: Taskfile versions greater than v3.8 not implemented in the version of Task`)
} }
if v.LessThan(semver.MustParse("2.1")) && !e.Taskfile.Output.IsSet() {
return fmt.Errorf(`task: Taskfile option "output" is only available starting on Taskfile version v2.1`)
}
if v.LessThan(semver.MustParse("2.2")) && e.Taskfile.Includes.Len() > 0 {
return fmt.Errorf(`task: Including Taskfiles is only available starting on Taskfile version v2.2`)
}
if v.Compare(taskfile.V3) >= 0 && e.Taskfile.Expansions > 2 {
return fmt.Errorf(`task: The "expansions" setting is not available anymore on v3.0`)
}
if v.LessThan(semver.MustParse("3.8")) && e.Taskfile.Output.Group.IsSet() { if v.LessThan(semver.MustParse("3.8")) && e.Taskfile.Output.Group.IsSet() {
return fmt.Errorf(`task: Taskfile option "output.group" is only available starting on Taskfile version v3.8`) return fmt.Errorf(`task: Taskfile option "output.group" is only available starting on Taskfile version v3.8`)
} }
if v.Compare(semver.MustParse("2.1")) <= 0 {
err := errors.New(`task: Taskfile option "ignore_error" is only available starting on Taskfile version v2.1`)
for _, task := range e.Taskfile.Tasks.Values() {
if task.IgnoreError {
return err
}
for _, cmd := range task.Cmds {
if cmd.IgnoreError {
return err
}
}
}
}
if v.LessThan(semver.MustParse("2.6")) {
for _, task := range e.Taskfile.Tasks.Values() {
if len(task.Preconditions) > 0 {
return errors.New(`task: Task option "preconditions" is only available starting on Taskfile version v2.6`)
}
}
}
if v.LessThan(taskfile.V3) {
err := e.Taskfile.Includes.Range(func(_ string, taskfile taskfile.IncludedTaskfile) error {
if taskfile.AdvancedImport {
return errors.New(`task: Import with additional parameters is only available starting on Taskfile version v3`)
}
return nil
})
if err != nil {
return err
}
}
if v.LessThan(semver.MustParse("3.7")) { if v.LessThan(semver.MustParse("3.7")) {
if e.Taskfile.Run != "" { if e.Taskfile.Run != "" {
return errors.New(`task: Setting the "run" type is only available starting on Taskfile version v3.7`) return errors.New(`task: Setting the "run" type is only available starting on Taskfile version v3.7`)

View File

@ -64,13 +64,12 @@ type Executor struct {
Stderr io.Writer Stderr io.Writer
Logger *logger.Logger Logger *logger.Logger
Compiler compiler.Compiler Compiler *compiler.Compiler
Output output.Output Output output.Output
OutputStyle taskfile.Output OutputStyle taskfile.Output
TaskSorter sort.TaskSorter TaskSorter sort.TaskSorter
UserWorkingDir string UserWorkingDir string
taskvars *taskfile.Vars
fuzzyModel *fuzzy.Model fuzzyModel *fuzzy.Model
concurrencySemaphore chan struct{} concurrencySemaphore chan struct{}
@ -349,7 +348,7 @@ func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfi
outputWrapper = output.Interleaved{} outputWrapper = output.Interleaved{}
} }
vars, err := e.Compiler.FastGetVariables(t, call) vars, err := e.Compiler.FastGetVariables(t, call)
outputTemplater := &templater.Templater{Vars: vars, RemoveNoValue: true} outputTemplater := &templater.Templater{Vars: vars}
if err != nil { if err != nil {
return fmt.Errorf("task: failed to get variables: %w", err) return fmt.Errorf("task: failed to get variables: %w", err)
} }

View File

@ -91,47 +91,9 @@ func TestEnv(t *testing.T) {
tt.Run(t) tt.Run(t)
} }
func TestVarsV2(t *testing.T) { func TestVars(t *testing.T) {
tt := fileContentTest{ tt := fileContentTest{
Dir: "testdata/vars/v2", Dir: "testdata/vars",
Target: "default",
TrimSpace: true,
Files: map[string]string{
"foo.txt": "foo",
"bar.txt": "bar",
"baz.txt": "baz",
"tmpl_foo.txt": "foo",
"tmpl_bar.txt": "bar",
"tmpl_foo2.txt": "foo2",
"tmpl_bar2.txt": "bar2",
"shtmpl_foo.txt": "foo",
"shtmpl_foo2.txt": "foo2",
"nestedtmpl_foo.txt": "<no value>",
"nestedtmpl_foo2.txt": "foo2",
"foo2.txt": "foo2",
"bar2.txt": "bar2",
"baz2.txt": "baz2",
"tmpl2_foo.txt": "<no value>",
"tmpl2_foo2.txt": "foo2",
"tmpl2_bar.txt": "<no value>",
"tmpl2_bar2.txt": "bar2",
"shtmpl2_foo.txt": "<no value>",
"shtmpl2_foo2.txt": "foo2",
"nestedtmpl2_foo2.txt": "<no value>",
"override.txt": "bar",
"nested.txt": "Taskvars-TaskfileVars-TaskVars",
"task_name.txt": "hello",
},
}
tt.Run(t)
// Ensure identical results when running hello task directly.
tt.Target = "hello"
tt.Run(t)
}
func TestVarsV3(t *testing.T) {
tt := fileContentTest{
Dir: "testdata/vars/v3",
Target: "default", Target: "default",
Files: map[string]string{ Files: map[string]string{
"missing-var.txt": "\n", "missing-var.txt": "\n",
@ -144,29 +106,6 @@ func TestVarsV3(t *testing.T) {
tt.Run(t) tt.Run(t)
} }
func TestMultilineVars(t *testing.T) {
for _, dir := range []string{"testdata/vars/v2/multiline"} {
tt := fileContentTest{
Dir: dir,
Target: "default",
TrimSpace: false,
Files: map[string]string{
// Note:
// - task does not strip a trailing newline from var entries
// - task strips one trailing newline from shell output
// - the cat command adds a trailing newline
"echo_foobar.txt": "foo\nbar\n",
"echo_n_foobar.txt": "foo\nbar\n",
"echo_n_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n",
"var_multiline.txt": "\n\nfoo\n bar\nfoobar\n\nbaz\n\n\n",
"var_catlines.txt": " foo bar foobar baz \n",
"var_enumfile.txt": "0:\n1:\n2:foo\n3: bar\n4:foobar\n5:\n6:baz\n7:\n8:\n",
},
}
tt.Run(t)
}
}
func TestSpecialVars(t *testing.T) { func TestSpecialVars(t *testing.T) {
const dir = "testdata/special_vars" const dir = "testdata/special_vars"
const target = "default" const target = "default"
@ -202,22 +141,6 @@ func TestSpecialVars(t *testing.T) {
assert.Contains(t, output, "included/TASK_VERSION=unknown") assert.Contains(t, output, "included/TASK_VERSION=unknown")
} }
func TestVarsInvalidTmpl(t *testing.T) {
const (
dir = "testdata/vars/v2"
target = "invalid-var-tmpl"
expectError = "template: :1: unexpected EOF"
)
e := &task.Executor{
Dir: dir,
Stdout: io.Discard,
Stderr: io.Discard,
}
require.NoError(t, e.Setup(), "e.Setup()")
assert.EqualError(t, e.Run(context.Background(), taskfile.Call{Task: target}), expectError, "e.Run(target)")
}
func TestConcurrency(t *testing.T) { func TestConcurrency(t *testing.T) {
const ( const (
dir = "testdata/concurrency" dir = "testdata/concurrency"
@ -910,8 +833,11 @@ func TestTaskVersion(t *testing.T) {
tests := []struct { tests := []struct {
Dir string Dir string
Version *semver.Version Version *semver.Version
wantErr bool
}{ }{
{"testdata/version/v2", semver.MustParse("2")}, {"testdata/version/v1", semver.MustParse("1"), true},
{"testdata/version/v2", semver.MustParse("2"), true},
{"testdata/version/v3", semver.MustParse("3"), false},
} }
for _, test := range tests { for _, test := range tests {
@ -921,7 +847,12 @@ func TestTaskVersion(t *testing.T) {
Stdout: io.Discard, Stdout: io.Discard,
Stderr: io.Discard, Stderr: io.Discard,
} }
require.NoError(t, e.Setup()) err := e.Setup()
if test.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Equal(t, test.Version, e.Taskfile.Version) assert.Equal(t, test.Version, e.Taskfile.Version)
assert.Equal(t, 2, e.Taskfile.Tasks.Len()) assert.Equal(t, 2, e.Taskfile.Tasks.Len())
}) })
@ -1061,21 +992,6 @@ func TestIncludeCycle(t *testing.T) {
assert.Contains(t, err.Error(), "task: include cycle detected between") assert.Contains(t, err.Error(), "task: include cycle detected between")
} }
func TestIncorrectVersionIncludes(t *testing.T) {
const dir = "testdata/incorrect_includes"
expectedError := "task: Import with additional parameters is only available starting on Taskfile version v3"
var buff bytes.Buffer
e := task.Executor{
Dir: dir,
Stdout: &buff,
Stderr: &buff,
Silent: true,
}
assert.EqualError(t, e.Setup(), expectedError)
}
func TestIncludesIncorrect(t *testing.T) { func TestIncludesIncorrect(t *testing.T) {
const dir = "testdata/includes_incorrect" const dir = "testdata/includes_incorrect"
@ -1500,10 +1416,10 @@ func TestDisplaysErrorOnVersion1Schema(t *testing.T) {
} }
err := e.Setup() err := e.Setup()
require.Error(t, err) require.Error(t, err)
assert.Equal(t, "task: version 1 schemas are no longer supported", err.Error()) assert.Equal(t, "task: Taskfile schemas prior to v3 are no longer supported", err.Error())
} }
func TestDisplaysWarningOnVersion2Schema(t *testing.T) { func TestDisplaysErrorOnVersion2Schema(t *testing.T) {
var buff bytes.Buffer var buff bytes.Buffer
e := task.Executor{ e := task.Executor{
Dir: "testdata/version/v2", Dir: "testdata/version/v2",
@ -1511,8 +1427,8 @@ func TestDisplaysWarningOnVersion2Schema(t *testing.T) {
Stderr: &buff, Stderr: &buff,
} }
err := e.Setup() err := e.Setup()
require.NoError(t, err) require.Error(t, err)
assert.Equal(t, "task: version 2 schemas are deprecated and will be removed in a future release\nSee https://github.com/go-task/task/issues/1197 for more details\n", buff.String()) assert.Equal(t, "task: Taskfile schemas prior to v3 are no longer supported", err.Error())
} }
func TestShortTaskNotation(t *testing.T) { func TestShortTaskNotation(t *testing.T) {

View File

@ -13,10 +13,6 @@ func Merge(t1, t2 *Taskfile, includedTaskfile *IncludedTaskfile, namespaces ...s
if !t1.Version.Equal(t2.Version) { if !t1.Version.Equal(t2.Version) {
return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version) return fmt.Errorf(`task: Taskfiles versions should match. First is "%s" but second is "%s"`, t1.Version, t2.Version)
} }
if t2.Expansions != 0 && t2.Expansions != 2 {
t1.Expansions = t2.Expansions
}
if t2.Output.IsSet() { if t2.Output.IsSet() {
t1.Output = t2.Output t1.Output = t2.Output
} }

View File

@ -11,7 +11,7 @@ import (
"github.com/go-task/task/v3/taskfile" "github.com/go-task/task/v3/taskfile"
) )
func Dotenv(c compiler.Compiler, tf *taskfile.Taskfile, dir string) (*taskfile.Vars, error) { func Dotenv(c *compiler.Compiler, tf *taskfile.Taskfile, dir string) (*taskfile.Vars, error) {
if len(tf.Dotenv) == 0 { if len(tf.Dotenv) == 0 {
return nil, nil return nil, nil
} }
@ -23,7 +23,7 @@ func Dotenv(c compiler.Compiler, tf *taskfile.Taskfile, dir string) (*taskfile.V
env := &taskfile.Vars{} env := &taskfile.Vars{}
tr := templater.Templater{Vars: vars, RemoveNoValue: true} tr := templater.Templater{Vars: vars}
for _, dotEnvPath := range tf.Dotenv { for _, dotEnvPath := range tf.Dotenv {
dotEnvPath = tr.Replace(dotEnvPath) dotEnvPath = tr.Replace(dotEnvPath)

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"time" "time"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -174,21 +173,19 @@ func Taskfile(
} }
err = t.Includes.Range(func(namespace string, includedTask taskfile.IncludedTaskfile) error { err = t.Includes.Range(func(namespace string, includedTask taskfile.IncludedTaskfile) error {
if t.Version.Compare(taskfile.V3) >= 0 { tr := templater.Templater{Vars: t.Vars}
tr := templater.Templater{Vars: t.Vars, RemoveNoValue: true} includedTask = taskfile.IncludedTaskfile{
includedTask = taskfile.IncludedTaskfile{ Taskfile: tr.Replace(includedTask.Taskfile),
Taskfile: tr.Replace(includedTask.Taskfile), Dir: tr.Replace(includedTask.Dir),
Dir: tr.Replace(includedTask.Dir), Optional: includedTask.Optional,
Optional: includedTask.Optional, Internal: includedTask.Internal,
Internal: includedTask.Internal, Aliases: includedTask.Aliases,
Aliases: includedTask.Aliases, AdvancedImport: includedTask.AdvancedImport,
AdvancedImport: includedTask.AdvancedImport, Vars: includedTask.Vars,
Vars: includedTask.Vars, BaseDir: includedTask.BaseDir,
BaseDir: includedTask.BaseDir, }
} if err := tr.Err(); err != nil {
if err := tr.Err(); err != nil { return err
return err
}
} }
uri, err := includedTask.FullTaskfilePath() uri, err := includedTask.FullTaskfilePath()
@ -219,7 +216,7 @@ func Taskfile(
return err return err
} }
if t.Version.Compare(taskfile.V3) >= 0 && len(includedTaskfile.Dotenv) > 0 { if len(includedTaskfile.Dotenv) > 0 {
return ErrIncludedTaskfilesCantHaveDotenvs return ErrIncludedTaskfilesCantHaveDotenvs
} }
@ -273,31 +270,6 @@ func Taskfile(
return nil, err return nil, err
} }
if t.Version.Compare(taskfile.V3) < 0 {
if node, isFileNode := node.(*FileNode); isFileNode {
path := filepathext.SmartJoin(node.Dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS))
if _, err = os.Stat(path); err == nil {
osNode := &FileNode{
BaseNode: NewBaseNode(WithParent(node)),
Entrypoint: path,
Dir: node.Dir,
}
b, err := osNode.Read(context.Background())
if err != nil {
return nil, err
}
var osTaskfile *taskfile.Taskfile
if err := yaml.Unmarshal(b, &osTaskfile); err != nil {
return nil, &errors.TaskfileInvalidError{URI: filepathext.TryAbsToRel(node.Location()), Err: err}
}
t.Location = node.Location()
if err = taskfile.Merge(t, osTaskfile, nil); err != nil {
return nil, err
}
}
}
}
for _, task := range t.Tasks.Values() { for _, task := range t.Tasks.Values() {
// If the task is not defined, create a new one // If the task is not defined, create a new one
if task == nil { if task == nil {

View File

@ -1,45 +0,0 @@
package read
import (
"fmt"
"os"
"runtime"
"gopkg.in/yaml.v3"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/taskfile"
)
// Taskvars reads a Taskvars for a given directory
func Taskvars(dir string) (*taskfile.Vars, error) {
vars := &taskfile.Vars{}
path := filepathext.SmartJoin(dir, "Taskvars.yml")
if _, err := os.Stat(path); err == nil {
vars, err = readTaskvars(path)
if err != nil {
return nil, err
}
}
path = filepathext.SmartJoin(dir, fmt.Sprintf("Taskvars_%s.yml", runtime.GOOS))
if _, err := os.Stat(path); err == nil {
osVars, err := readTaskvars(path)
if err != nil {
return nil, err
}
vars.Merge(osVars)
}
return vars, nil
}
func readTaskvars(file string) (*taskfile.Vars, error) {
f, err := os.Open(file)
if err != nil {
return nil, err
}
var vars taskfile.Vars
return &vars, yaml.NewDecoder(f).Decode(&vars)
}

View File

@ -10,54 +10,48 @@ import (
"github.com/go-task/task/v3/errors" "github.com/go-task/task/v3/errors"
) )
var ( var V3 = semver.MustParse("3")
V3 = semver.MustParse("3")
V2 = semver.MustParse("2")
)
// Taskfile represents a Taskfile.yml // Taskfile represents a Taskfile.yml
type Taskfile struct { type Taskfile struct {
Location string Location string
Version *semver.Version Version *semver.Version
Expansions int Output Output
Output Output Method string
Method string Includes *IncludedTaskfiles
Includes *IncludedTaskfiles Set []string
Set []string Shopt []string
Shopt []string Vars *Vars
Vars *Vars Env *Vars
Env *Vars Tasks Tasks
Tasks Tasks Silent bool
Silent bool Dotenv []string
Dotenv []string Run string
Run string Interval time.Duration
Interval time.Duration
} }
func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error { func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
switch node.Kind { switch node.Kind {
case yaml.MappingNode: case yaml.MappingNode:
var taskfile struct { var taskfile struct {
Version *semver.Version Version *semver.Version
Expansions int Output Output
Output Output Method string
Method string Includes *IncludedTaskfiles
Includes *IncludedTaskfiles Set []string
Set []string Shopt []string
Shopt []string Vars *Vars
Vars *Vars Env *Vars
Env *Vars Tasks Tasks
Tasks Tasks Silent bool
Silent bool Dotenv []string
Dotenv []string Run string
Run string Interval time.Duration
Interval time.Duration
} }
if err := node.Decode(&taskfile); err != nil { if err := node.Decode(&taskfile); err != nil {
return err return err
} }
tf.Version = taskfile.Version tf.Version = taskfile.Version
tf.Expansions = taskfile.Expansions
tf.Output = taskfile.Output tf.Output = taskfile.Output
tf.Method = taskfile.Method tf.Method = taskfile.Method
tf.Includes = taskfile.Includes tf.Includes = taskfile.Includes
@ -70,9 +64,6 @@ func (tf *Taskfile) UnmarshalYAML(node *yaml.Node) error {
tf.Dotenv = taskfile.Dotenv tf.Dotenv = taskfile.Dotenv
tf.Run = taskfile.Run tf.Run = taskfile.Run
tf.Interval = taskfile.Interval tf.Interval = taskfile.Interval
if tf.Expansions <= 0 {
tf.Expansions = 2
}
if tf.Version == nil { if tf.Version == nil {
return errors.New("task: 'version' is required") return errors.New("task: 'version' is required")
} }

View File

@ -1,4 +1,4 @@
version: '2' version: "3"
tasks: tasks:
default: default:

View File

@ -1,10 +0,0 @@
version: '2.6'
includes:
included:
taskfile: ./included
tasks:
default:
cmds:
- task: gen

View File

@ -1,6 +0,0 @@
version: '2.6'
tasks:
gen:
cmds:
- echo incorrect includes test

View File

@ -1,58 +0,0 @@
version: '2'
vars:
NESTED2: "{{.NESTED1}}-TaskfileVars"
tasks:
default:
deps: [hello]
hello:
cmds:
- echo {{.FOO}} > foo.txt
- echo {{.BAR}} > bar.txt
- echo {{.BAZ}} > baz.txt
- echo '{{.TMPL_FOO}}' > tmpl_foo.txt
- echo '{{.TMPL_BAR}}' > tmpl_bar.txt
- echo '{{.TMPL_FOO2}}' > tmpl_foo2.txt
- echo '{{.TMPL_BAR2}}' > tmpl_bar2.txt
- echo '{{.SHTMPL_FOO}}' > shtmpl_foo.txt
- echo '{{.SHTMPL_FOO2}}' > shtmpl_foo2.txt
- echo '{{.NESTEDTMPL_FOO}}' > nestedtmpl_foo.txt
- echo '{{.NESTEDTMPL_FOO2}}' > nestedtmpl_foo2.txt
- echo {{.FOO2}} > foo2.txt
- echo {{.BAR2}} > bar2.txt
- echo {{.BAZ2}} > baz2.txt
- echo '{{.TMPL2_FOO}}' > tmpl2_foo.txt
- echo '{{.TMPL2_BAR}}' > tmpl2_bar.txt
- echo '{{.TMPL2_FOO2}}' > tmpl2_foo2.txt
- echo '{{.TMPL2_BAR2}}' > tmpl2_bar2.txt
- echo '{{.SHTMPL2_FOO}}' > shtmpl2_foo.txt
- echo '{{.SHTMPL2_FOO2}}' > shtmpl2_foo2.txt
- echo '{{.NESTEDTMPL2_FOO2}}' > nestedtmpl2_foo2.txt
- echo {{.OVERRIDE}} > override.txt
- echo '{{.NESTED3}}' > nested.txt
- echo '{{.TASK}}' > task_name.txt
vars:
FOO: foo
BAR:
sh: echo bar
BAZ:
sh: echo baz
TMPL_FOO: "{{.FOO}}"
TMPL_BAR: "{{.BAR}}"
TMPL_FOO2: "{{.FOO2}}"
TMPL_BAR2: "{{.BAR2}}"
SHTMPL_FOO:
sh: "echo '{{.FOO}}'"
SHTMPL_FOO2:
sh: "echo '{{.FOO2}}'"
NESTEDTMPL_FOO: "{{.TMPL_FOO}}"
NESTEDTMPL_FOO2: "{{.TMPL2_FOO2}}"
OVERRIDE: "bar"
NESTED3: "{{.NESTED2}}-TaskVars"
invalid-var-tmpl:
vars:
CHARS: "abcd"
INVALID: "{{range .CHARS}}no end"

View File

@ -1,14 +0,0 @@
FOO2: foo2
BAR2:
sh: echo bar2
BAZ2:
sh: echo baz2
TMPL2_FOO: "{{.FOO}}"
TMPL2_BAR: "{{.BAR}}"
TMPL2_FOO2: "{{.FOO2}}"
TMPL2_BAR2: "{{.BAR2}}"
SHTMPL2_FOO2:
sh: "echo '{{.FOO2}}'"
NESTEDTMPL2_FOO2: "{{.TMPL2_FOO2}}"
OVERRIDE: "foo"
NESTED1: "Taskvars"

View File

@ -1,45 +0,0 @@
version: '2'
tasks:
default:
vars:
MULTILINE: "\n\nfoo\n bar\nfoobar\n\nbaz\n\n"
cmds:
- task: file
vars:
CONTENT:
sh: "echo 'foo\nbar'"
FILE: "echo_foobar.txt"
- task: file
vars:
CONTENT:
sh: "echo -n 'foo\nbar'"
FILE: "echo_n_foobar.txt"
- task: file
vars:
CONTENT:
sh: echo -n "{{.MULTILINE}}"
FILE: "echo_n_multiline.txt"
- task: file
vars:
CONTENT: "{{.MULTILINE}}"
FILE: "var_multiline.txt"
- task: file
vars:
CONTENT: "{{.MULTILINE | catLines}}"
FILE: "var_catlines.txt"
- task: enumfile
vars:
LINES: "{{.MULTILINE}}"
FILE: "var_enumfile.txt"
file:
cmds:
- |
cat << EOF > '{{.FILE}}'
{{.CONTENT}}
EOF
enumfile:
cmds:
- |
cat << EOF > '{{.FILE}}'
{{range $i, $line := .LINES| splitLines}}{{$i}}:{{$line}}
{{end}}EOF

View File

@ -1 +0,0 @@
*.txt

View File

@ -1,4 +1,4 @@
version: 1 version: "1"
tasks: tasks:
foo: foo:
cmds: cmds:

View File

@ -1,4 +1,4 @@
version: '2' version: "2"
tasks: tasks:
foo: foo:

9
testdata/version/v3/Taskfile.yml vendored Normal file
View File

@ -0,0 +1,9 @@
version: "3"
tasks:
foo:
cmds:
- echo "Foo"
bar:
cmds:
- echo "Bar"

View File

@ -42,7 +42,7 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf
return nil, err return nil, err
} }
r := templater.Templater{Vars: vars, RemoveNoValue: e.Taskfile.Version.Compare(taskfile.V3) >= 0} r := templater.Templater{Vars: vars}
new := taskfile.Task{ new := taskfile.Task{
Task: origTask.Task, Task: origTask.Task,