diff --git a/.gitignore b/.gitignore index 40625d50..15c634ca 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ ./task dist/ + +.DS_Store diff --git a/Taskvars.yml b/Taskvars.yml index 14911472..7470fbd0 100644 --- a/Taskvars.yml +++ b/Taskvars.yml @@ -5,5 +5,11 @@ GO_PACKAGES: . ./cmd/task ./internal/args + ./internal/compiler + ./internal/compiler/v1 + ./internal/compiler/v2 ./internal/execext + ./internal/logger ./internal/status + ./internal/taskfile + ./internal/templater diff --git a/errors.go b/errors.go index 0ef9a7b5..e0bcab73 100644 --- a/errors.go +++ b/errors.go @@ -51,15 +51,6 @@ func (err *cantWatchNoSourcesError) Error() string { return fmt.Sprintf(`task: Can't watch task "%s" because it has no specified sources`, err.taskName) } -type dynamicVarError struct { - cause error - cmd string -} - -func (err *dynamicVarError) Error() string { - return fmt.Sprintf(`task: Command "%s" in taskvars file failed: %s`, err.cmd, err.cause) -} - // MaximumTaskCallExceededError is returned when a task is called too // many times. In this case you probably have a cyclic dependendy or // infinite loop diff --git a/help.go b/help.go index 381897ee..50b31fae 100644 --- a/help.go +++ b/help.go @@ -4,16 +4,18 @@ import ( "fmt" "sort" "text/tabwriter" + + "github.com/go-task/task/internal/taskfile" ) // PrintTasksHelp prints help os tasks that have a description func (e *Executor) PrintTasksHelp() { tasks := e.tasksWithDesc() if len(tasks) == 0 { - e.outf("task: No tasks with description available") + e.Logger.Outf("task: No tasks with description available") return } - e.outf("task: Available tasks for this project:") + e.Logger.Outf("task: Available tasks for this project:") // Format in tab-separated columns with a tab stop of 8. w := tabwriter.NewWriter(e.Stdout, 0, 8, 0, '\t', 0) @@ -23,8 +25,8 @@ func (e *Executor) PrintTasksHelp() { w.Flush() } -func (e *Executor) tasksWithDesc() (tasks []*Task) { - tasks = make([]*Task, 0, len(e.Taskfile.Tasks)) +func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) { + tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks)) for _, task := range e.Taskfile.Tasks { if task.Desc != "" { tasks = append(tasks, task) diff --git a/internal/args/args.go b/internal/args/args.go index 3aba9159..2e1a8cd7 100644 --- a/internal/args/args.go +++ b/internal/args/args.go @@ -4,7 +4,7 @@ import ( "errors" "strings" - "github.com/go-task/task" + "github.com/go-task/task/internal/taskfile" ) var ( @@ -13,12 +13,12 @@ var ( ) // Parse parses command line argument: tasks and vars of each task -func Parse(args ...string) ([]task.Call, error) { - var calls []task.Call +func Parse(args ...string) ([]taskfile.Call, error) { + var calls []taskfile.Call for _, arg := range args { if !strings.Contains(arg, "=") { - calls = append(calls, task.Call{Task: arg}) + calls = append(calls, taskfile.Call{Task: arg}) continue } if len(calls) < 1 { @@ -26,11 +26,11 @@ func Parse(args ...string) ([]task.Call, error) { } if calls[len(calls)-1].Vars == nil { - calls[len(calls)-1].Vars = make(task.Vars) + calls[len(calls)-1].Vars = make(taskfile.Vars) } pair := strings.SplitN(arg, "=", 2) - calls[len(calls)-1].Vars[pair[0]] = task.Var{Static: pair[1]} + calls[len(calls)-1].Vars[pair[0]] = taskfile.Var{Static: pair[1]} } return calls, nil } diff --git a/internal/args/args_test.go b/internal/args/args_test.go index 99b0f04a..f6d42d97 100644 --- a/internal/args/args_test.go +++ b/internal/args/args_test.go @@ -4,8 +4,8 @@ import ( "fmt" "testing" - "github.com/go-task/task" "github.com/go-task/task/internal/args" + "github.com/go-task/task/internal/taskfile" "github.com/stretchr/testify/assert" ) @@ -13,12 +13,12 @@ import ( func TestArgs(t *testing.T) { tests := []struct { Args []string - Expected []task.Call + Expected []taskfile.Call Err error }{ { Args: []string{"task-a", "task-b", "task-c"}, - Expected: []task.Call{ + Expected: []taskfile.Call{ {Task: "task-a"}, {Task: "task-b"}, {Task: "task-c"}, @@ -26,30 +26,30 @@ func TestArgs(t *testing.T) { }, { Args: []string{"task-a", "FOO=bar", "task-b", "task-c", "BAR=baz", "BAZ=foo"}, - Expected: []task.Call{ + Expected: []taskfile.Call{ { Task: "task-a", - Vars: task.Vars{ - "FOO": task.Var{Static: "bar"}, + Vars: taskfile.Vars{ + "FOO": taskfile.Var{Static: "bar"}, }, }, {Task: "task-b"}, { Task: "task-c", - Vars: task.Vars{ - "BAR": task.Var{Static: "baz"}, - "BAZ": task.Var{Static: "foo"}, + Vars: taskfile.Vars{ + "BAR": taskfile.Var{Static: "baz"}, + "BAZ": taskfile.Var{Static: "foo"}, }, }, }, }, { Args: []string{"task-a", "CONTENT=with some spaces"}, - Expected: []task.Call{ + Expected: []taskfile.Call{ { Task: "task-a", - Vars: task.Vars{ - "CONTENT": task.Var{Static: "with some spaces"}, + Vars: taskfile.Vars{ + "CONTENT": taskfile.Var{Static: "with some spaces"}, }, }, }, diff --git a/internal/compiler/compiler.go b/internal/compiler/compiler.go new file mode 100644 index 00000000..efe121cb --- /dev/null +++ b/internal/compiler/compiler.go @@ -0,0 +1,12 @@ +package compiler + +import ( + "github.com/go-task/task/internal/taskfile" +) + +// Compiler handles compilation of a task before its execution. +// E.g. variable merger, template processing, etc. +type Compiler interface { + GetVariables(t *taskfile.Task, call taskfile.Call) (taskfile.Vars, error) + HandleDynamicVar(v taskfile.Var) (string, error) +} diff --git a/internal/compiler/env.go b/internal/compiler/env.go new file mode 100644 index 00000000..aa39e72b --- /dev/null +++ b/internal/compiler/env.go @@ -0,0 +1,24 @@ +package compiler + +import ( + "os" + "strings" + + "github.com/go-task/task/internal/taskfile" +) + +// GetEnviron the all return all environment variables encapsulated on a +// taskfile.Vars +func GetEnviron() taskfile.Vars { + var ( + env = os.Environ() + m = make(taskfile.Vars, len(env)) + ) + + for _, e := range env { + keyVal := strings.SplitN(e, "=", 2) + key, val := keyVal[0], keyVal[1] + m[key] = taskfile.Var{Static: val} + } + return m +} diff --git a/internal/compiler/v1/compiler_v1.go b/internal/compiler/v1/compiler_v1.go new file mode 100644 index 00000000..7c56c57e --- /dev/null +++ b/internal/compiler/v1/compiler_v1.go @@ -0,0 +1,136 @@ +package v1 + +import ( + "bytes" + "fmt" + "strings" + "sync" + + "github.com/go-task/task/internal/compiler" + "github.com/go-task/task/internal/execext" + "github.com/go-task/task/internal/logger" + "github.com/go-task/task/internal/taskfile" + "github.com/go-task/task/internal/templater" +) + +var _ compiler.Compiler = &CompilerV1{} + +type CompilerV1 struct { + Dir string + Vars taskfile.Vars + + Logger *logger.Logger + + dynamicCache map[string]string + muDynamicCache sync.Mutex +} + +// GetVariables returns fully resolved variables following the priority order: +// 1. Call variables (should already have been resolved) +// 2. Environment (should not need to be resolved) +// 3. Task variables, resolved with access to: +// - call, taskvars and environment variables +// 4. Taskvars variables, resolved with access to: +// - environment variables +func (c *CompilerV1) GetVariables(t *taskfile.Task, call taskfile.Call) (taskfile.Vars, error) { + merge := func(dest taskfile.Vars, srcs ...taskfile.Vars) { + for _, src := range srcs { + for k, v := range src { + dest[k] = v + } + } + } + varsKeys := func(srcs ...taskfile.Vars) []string { + m := make(map[string]struct{}) + for _, src := range srcs { + for k := range src { + m[k] = struct{}{} + } + } + lst := make([]string, 0, len(m)) + for k := range m { + lst = append(lst, k) + } + return lst + } + replaceVars := func(dest taskfile.Vars, keys []string) error { + r := templater.Templater{Vars: dest} + for _, k := range keys { + v := dest[k] + dest[k] = taskfile.Var{ + Static: r.Replace(v.Static), + Sh: r.Replace(v.Sh), + } + } + return r.Err() + } + resolveShell := func(dest taskfile.Vars, keys []string) error { + for _, k := range keys { + v := dest[k] + static, err := c.HandleDynamicVar(v) + if err != nil { + return err + } + dest[k] = taskfile.Var{Static: static} + } + return nil + } + update := func(dest taskfile.Vars, srcs ...taskfile.Vars) error { + merge(dest, srcs...) + // updatedKeys ensures template evaluation is run only once. + updatedKeys := varsKeys(srcs...) + if err := replaceVars(dest, updatedKeys); err != nil { + return err + } + return resolveShell(dest, updatedKeys) + } + + // Resolve taskvars variables to "result" with environment override variables. + override := compiler.GetEnviron() + result := make(taskfile.Vars, len(c.Vars)+len(t.Vars)+len(override)) + if err := update(result, c.Vars, override); err != nil { + return nil, err + } + // Resolve task variables to "result" with environment and call override variables. + merge(override, call.Vars) + if err := update(result, t.Vars, override); err != nil { + return nil, err + } + return result, nil +} + +func (c *CompilerV1) 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(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(`task: dynamic variable: '%s' result: '%s'`, v.Sh, result) + + return result, nil +} diff --git a/internal/compiler/v2/compiler_v2.go b/internal/compiler/v2/compiler_v2.go new file mode 100644 index 00000000..0b67f7a6 --- /dev/null +++ b/internal/compiler/v2/compiler_v2.go @@ -0,0 +1,104 @@ +package v2 + +import ( + "bytes" + "fmt" + "strings" + "sync" + + "github.com/go-task/task/internal/compiler" + "github.com/go-task/task/internal/execext" + "github.com/go-task/task/internal/logger" + "github.com/go-task/task/internal/taskfile" + "github.com/go-task/task/internal/templater" +) + +var _ compiler.Compiler = &CompilerV2{} + +type CompilerV2 struct { + Dir string + Vars taskfile.Vars + + Logger *logger.Logger + + dynamicCache map[string]string + muDynamicCache sync.Mutex +} + +// GetVariables returns fully resolved variables following the priority order: +// 1. Task variables +// 2. Call variables +// 3. Taskvars file variables +// 4. Environment variables +func (c *CompilerV2) GetVariables(t *taskfile.Task, call taskfile.Call) (taskfile.Vars, error) { + vr := varResolver{c: c, vars: compiler.GetEnviron()} + vr.merge(c.Vars) + vr.merge(c.Vars) + vr.merge(call.Vars) + vr.merge(call.Vars) + vr.merge(t.Vars) + vr.merge(t.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} + for k, v := range vars { + v = taskfile.Var{ + Static: tr.Replace(v.Static), + Sh: tr.Replace(v.Sh), + } + static, err := vr.c.HandleDynamicVar(v) + if err != nil { + vr.err = err + return + } + vr.vars[k] = taskfile.Var{Static: static} + } + vr.err = tr.Err() +} + +func (c *CompilerV2) 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(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(`task: dynamic variable: '%s' result: '%s'`, v.Sh, result) + + return result, nil +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 00000000..85c971d4 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,38 @@ +package logger + +import ( + "fmt" + "io" +) + +type Logger struct { + Stdout io.Writer + Stderr io.Writer + Verbose bool +} + +func (l *Logger) Outf(s string, args ...interface{}) { + if len(args) == 0 { + s, args = "%s", []interface{}{s} + } + fmt.Fprintf(l.Stdout, s+"\n", args...) +} + +func (l *Logger) VerboseOutf(s string, args ...interface{}) { + if l.Verbose { + l.Outf(s, args...) + } +} + +func (l *Logger) Errf(s string, args ...interface{}) { + if len(args) == 0 { + s, args = "%s", []interface{}{s} + } + fmt.Fprintf(l.Stderr, s+"\n", args...) +} + +func (l *Logger) VerboseErrf(s string, args ...interface{}) { + if l.Verbose { + l.Errf(s, args...) + } +} diff --git a/internal/taskfile/call.go b/internal/taskfile/call.go new file mode 100644 index 00000000..eec41031 --- /dev/null +++ b/internal/taskfile/call.go @@ -0,0 +1,7 @@ +package taskfile + +// Call is the parameters to a task call +type Call struct { + Task string + Vars Vars +} diff --git a/command.go b/internal/taskfile/cmd.go similarity index 94% rename from command.go rename to internal/taskfile/cmd.go index c14c94ba..f2bae2fc 100644 --- a/command.go +++ b/internal/taskfile/cmd.go @@ -1,4 +1,4 @@ -package task +package taskfile import ( "errors" @@ -76,9 +76,3 @@ func (d *Dep) UnmarshalYAML(unmarshal func(interface{}) error) error { } return ErrCantUnmarshalDep } - -// Call is the parameters to a task call -type Call struct { - Task string - Vars Vars -} diff --git a/internal/taskfile/task.go b/internal/taskfile/task.go new file mode 100644 index 00000000..6a2b4708 --- /dev/null +++ b/internal/taskfile/task.go @@ -0,0 +1,20 @@ +package taskfile + +// Tasks representas a group of tasks +type Tasks map[string]*Task + +// Task represents a task +type Task struct { + Task string + Cmds []*Cmd + Deps []*Dep + Desc string + Sources []string + Generates []string + Status []string + Dir string + Vars Vars + Env Vars + Silent bool + Method string +} diff --git a/internal/taskfile/taskfile.go b/internal/taskfile/taskfile.go new file mode 100644 index 00000000..dd5317bc --- /dev/null +++ b/internal/taskfile/taskfile.go @@ -0,0 +1,26 @@ +package taskfile + +// Taskfile represents a Taskfile.yml +type Taskfile struct { + // TODO: version is still not used + Version int + Tasks Tasks +} + +// UnmarshalYAML implements yaml.Unmarshaler interface +func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { + if err := unmarshal(&tf.Tasks); err == nil { + return nil + } + + var taskfile struct { + Version int + Tasks Tasks + } + if err := unmarshal(&taskfile); err != nil { + return err + } + tf.Version = taskfile.Version + tf.Tasks = taskfile.Tasks + return nil +} diff --git a/command_test.go b/internal/taskfile/taskfile_test.go similarity index 54% rename from command_test.go rename to internal/taskfile/taskfile_test.go index ad0b610a..9c5e0150 100644 --- a/command_test.go +++ b/internal/taskfile/taskfile_test.go @@ -1,9 +1,9 @@ -package task_test +package taskfile_test import ( "testing" - "github.com/go-task/task" + "github.com/go-task/task/internal/taskfile" "github.com/stretchr/testify/assert" "gopkg.in/yaml.v2" @@ -27,28 +27,28 @@ vars: }{ { yamlCmd, - &task.Cmd{}, - &task.Cmd{Cmd: `echo "a string command"`}, + &taskfile.Cmd{}, + &taskfile.Cmd{Cmd: `echo "a string command"`}, }, { yamlTaskCall, - &task.Cmd{}, - &task.Cmd{Task: "another-task", Vars: task.Vars{ - "PARAM1": task.Var{Static: "VALUE1"}, - "PARAM2": task.Var{Static: "VALUE2"}, + &taskfile.Cmd{}, + &taskfile.Cmd{Task: "another-task", Vars: taskfile.Vars{ + "PARAM1": taskfile.Var{Static: "VALUE1"}, + "PARAM2": taskfile.Var{Static: "VALUE2"}, }}, }, { yamlDep, - &task.Dep{}, - &task.Dep{Task: "task-name"}, + &taskfile.Dep{}, + &taskfile.Dep{Task: "task-name"}, }, { yamlTaskCall, - &task.Dep{}, - &task.Dep{Task: "another-task", Vars: task.Vars{ - "PARAM1": task.Var{Static: "VALUE1"}, - "PARAM2": task.Var{Static: "VALUE2"}, + &taskfile.Dep{}, + &taskfile.Dep{Task: "another-task", Vars: taskfile.Vars{ + "PARAM1": taskfile.Var{Static: "VALUE1"}, + "PARAM2": taskfile.Var{Static: "VALUE2"}, }}, }, } diff --git a/internal/taskfile/var.go b/internal/taskfile/var.go new file mode 100644 index 00000000..f52e0f8b --- /dev/null +++ b/internal/taskfile/var.go @@ -0,0 +1,58 @@ +package taskfile + +import ( + "errors" + "strings" +) + +var ( + // ErrCantUnmarshalVar is returned for invalid var YAML. + ErrCantUnmarshalVar = errors.New("task: can't unmarshal var value") +) + +// Vars is a string[string] variables map. +type Vars map[string]Var + +// ToStringMap converts Vars to a string map containing only the static +// variables +func (vs Vars) ToStringMap() (m map[string]string) { + m = make(map[string]string, len(vs)) + for k, v := range vs { + if v.Sh != "" { + // Dynamic variable is not yet resolved; trigger + // to be used in templates. + continue + } + m[k] = v.Static + } + return +} + +// Var represents either a static or dynamic variable. +type Var struct { + Static string + Sh string +} + +// UnmarshalYAML implements yaml.Unmarshaler interface. +func (v *Var) UnmarshalYAML(unmarshal func(interface{}) error) error { + var str string + if err := unmarshal(&str); err == nil { + if strings.HasPrefix(str, "$") { + v.Sh = strings.TrimPrefix(str, "$") + } else { + v.Static = str + } + return nil + } + + var sh struct { + Sh string + } + if err := unmarshal(&sh); err == nil { + v.Sh = sh.Sh + return nil + } + + return ErrCantUnmarshalVar +} diff --git a/internal/templater/funcs.go b/internal/templater/funcs.go new file mode 100644 index 00000000..96f6080d --- /dev/null +++ b/internal/templater/funcs.go @@ -0,0 +1,52 @@ +package templater + +import ( + "path/filepath" + "runtime" + "strings" + "text/template" + + "github.com/Masterminds/sprig" +) + +var ( + templateFuncs template.FuncMap +) + +func init() { + taskFuncs := template.FuncMap{ + "OS": func() string { return runtime.GOOS }, + "ARCH": func() string { return runtime.GOARCH }, + "catLines": func(s string) string { + s = strings.Replace(s, "\r\n", " ", -1) + return strings.Replace(s, "\n", " ", -1) + }, + "splitLines": func(s string) []string { + s = strings.Replace(s, "\r\n", "\n", -1) + return strings.Split(s, "\n") + }, + "fromSlash": func(path string) string { + return filepath.FromSlash(path) + }, + "toSlash": func(path string) string { + return filepath.ToSlash(path) + }, + "exeExt": func() string { + if runtime.GOOS == "windows" { + return ".exe" + } + return "" + }, + // IsSH is deprecated. + "IsSH": func() bool { return true }, + } + // Deprecated aliases for renamed functions. + taskFuncs["FromSlash"] = taskFuncs["fromSlash"] + taskFuncs["ToSlash"] = taskFuncs["toSlash"] + taskFuncs["ExeExt"] = taskFuncs["exeExt"] + + templateFuncs = sprig.TxtFuncMap() + for k, v := range taskFuncs { + templateFuncs[k] = v + } +} diff --git a/internal/templater/templater.go b/internal/templater/templater.go new file mode 100644 index 00000000..dfd6572b --- /dev/null +++ b/internal/templater/templater.go @@ -0,0 +1,73 @@ +package templater + +import ( + "bytes" + "text/template" + + "github.com/go-task/task/internal/taskfile" +) + +// Templater is a help struct that allow us to call "replaceX" funcs multiple +// times, without having to check for error each time. The first error that +// happen will be assigned to r.err, and consecutive calls to funcs will just +// return the zero value. +type Templater struct { + Vars taskfile.Vars + + strMap map[string]string + err error +} + +func (r *Templater) Replace(str string) string { + if r.err != nil || str == "" { + return "" + } + + templ, err := template.New("").Funcs(templateFuncs).Parse(str) + if err != nil { + r.err = err + return "" + } + + if r.strMap == nil { + r.strMap = r.Vars.ToStringMap() + } + + var b bytes.Buffer + if err = templ.Execute(&b, r.strMap); err != nil { + r.err = err + return "" + } + return b.String() +} + +func (r *Templater) ReplaceSlice(strs []string) []string { + if r.err != nil || len(strs) == 0 { + return nil + } + + new := make([]string, len(strs)) + for i, str := range strs { + new[i] = r.Replace(str) + } + return new +} + +func (r *Templater) ReplaceVars(vars taskfile.Vars) taskfile.Vars { + if r.err != nil || len(vars) == 0 { + return nil + } + + new := make(taskfile.Vars, len(vars)) + for k, v := range vars { + new[k] = taskfile.Var{ + Static: r.Replace(v.Static), + Sh: r.Replace(v.Sh), + } + } + return new +} + +func (r *Templater) Err() error { + return r.err +} diff --git a/log.go b/log.go deleted file mode 100644 index fc168130..00000000 --- a/log.go +++ /dev/null @@ -1,31 +0,0 @@ -package task - -import ( - "fmt" -) - -func (e *Executor) outf(s string, args ...interface{}) { - if len(args) == 0 { - s, args = "%s", []interface{}{s} - } - fmt.Fprintf(e.Stdout, s+"\n", args...) -} - -func (e *Executor) verboseOutf(s string, args ...interface{}) { - if e.Verbose { - e.outf(s, args...) - } -} - -func (e *Executor) errf(s string, args ...interface{}) { - if len(args) == 0 { - s, args = "%s", []interface{}{s} - } - fmt.Fprintf(e.Stderr, s+"\n", args...) -} - -func (e *Executor) verboseErrf(s string, args ...interface{}) { - if e.Verbose { - e.errf(s, args...) - } -} diff --git a/status.go b/status.go index d231fb3f..d2dabd37 100644 --- a/status.go +++ b/status.go @@ -6,16 +6,17 @@ import ( "github.com/go-task/task/internal/execext" "github.com/go-task/task/internal/status" + "github.com/go-task/task/internal/taskfile" ) // Status returns an error if any the of given tasks is not up-to-date -func (e *Executor) Status(calls ...Call) error { +func (e *Executor) Status(calls ...taskfile.Call) error { for _, call := range calls { t, ok := e.Taskfile.Tasks[call.Task] if !ok { return &taskNotFoundError{taskName: call.Task} } - isUpToDate, err := t.isUpToDate(e.Context) + isUpToDate, err := isTaskUpToDate(e.Context, t) if err != nil { return err } @@ -26,12 +27,12 @@ func (e *Executor) Status(calls ...Call) error { return nil } -func (t *Task) isUpToDate(ctx context.Context) (bool, error) { +func isTaskUpToDate(ctx context.Context, t *taskfile.Task) (bool, error) { if len(t.Status) > 0 { - return t.isUpToDateStatus(ctx) + return isTaskUpToDateStatus(ctx, t) } - checker, err := t.getStatusChecker() + checker, err := getStatusChecker(t) if err != nil { return false, err } @@ -39,15 +40,15 @@ func (t *Task) isUpToDate(ctx context.Context) (bool, error) { return checker.IsUpToDate() } -func (t *Task) statusOnError() error { - checker, err := t.getStatusChecker() +func statusOnError(t *taskfile.Task) error { + checker, err := getStatusChecker(t) if err != nil { return err } return checker.OnError() } -func (t *Task) getStatusChecker() (status.Checker, error) { +func getStatusChecker(t *taskfile.Task) (status.Checker, error) { switch t.Method { case "", "timestamp": return &status.Timestamp{ @@ -68,13 +69,13 @@ func (t *Task) getStatusChecker() (status.Checker, error) { } } -func (t *Task) isUpToDateStatus(ctx context.Context) (bool, error) { +func isTaskUpToDateStatus(ctx context.Context, t *taskfile.Task) (bool, error) { for _, s := range t.Status { err := execext.RunCommand(&execext.RunCommandOptions{ Context: ctx, Command: s, Dir: t.Dir, - Env: t.getEnviron(), + Env: getEnviron(t), }) if err != nil { return false, nil diff --git a/task.go b/task.go index c4abe999..7e3aca0c 100644 --- a/task.go +++ b/task.go @@ -5,10 +5,14 @@ import ( "fmt" "io" "os" - "sync" "sync/atomic" + "github.com/go-task/task/internal/compiler" + compilerv1 "github.com/go-task/task/internal/compiler/v1" + compilerv2 "github.com/go-task/task/internal/compiler/v2" "github.com/go-task/task/internal/execext" + "github.com/go-task/task/internal/logger" + "github.com/go-task/task/internal/taskfile" "golang.org/x/sync/errgroup" ) @@ -23,7 +27,7 @@ const ( // Executor executes a Taskfile type Executor struct { - Taskfile *Taskfile + Taskfile *taskfile.Taskfile Dir string Force bool Watch bool @@ -36,55 +40,18 @@ type Executor struct { Stdout io.Writer Stderr io.Writer - taskvars Vars + Logger *logger.Logger + Compiler compiler.Compiler + + taskvars taskfile.Vars taskCallCount map[string]*int32 - - dynamicCache map[string]string - muDynamicCache sync.Mutex -} - -// Tasks representas a group of tasks -type Tasks map[string]*Task - -// Task represents a task -type Task struct { - Task string - Cmds []*Cmd - Deps []*Dep - Desc string - Sources []string - Generates []string - Status []string - Dir string - Vars Vars - Env Vars - Silent bool - Method string } // Run runs Task -func (e *Executor) Run(calls ...Call) error { - if e.Context == nil { - e.Context = context.Background() - } - if e.Stdin == nil { - e.Stdin = os.Stdin - } - if e.Stdout == nil { - e.Stdout = os.Stdout - } - if e.Stderr == nil { - e.Stderr = os.Stderr - } - - e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks)) - for k := range e.Taskfile.Tasks { - e.taskCallCount[k] = new(int32) - } - - if e.dynamicCache == nil { - e.dynamicCache = make(map[string]string, 10) +func (e *Executor) Run(calls ...taskfile.Call) error { + if err := e.setup(); err != nil { + return err } // check if given tasks exist @@ -108,8 +75,57 @@ func (e *Executor) Run(calls ...Call) error { return nil } +func (e *Executor) setup() error { + if e.Taskfile.Version == 0 { + e.Taskfile.Version = 1 + } + if e.Context == nil { + e.Context = context.Background() + } + if e.Stdin == nil { + e.Stdin = os.Stdin + } + if e.Stdout == nil { + e.Stdout = os.Stdout + } + if e.Stderr == nil { + e.Stderr = os.Stderr + } + e.Logger = &logger.Logger{ + Stdout: e.Stdout, + Stderr: e.Stderr, + Verbose: e.Verbose, + } + switch e.Taskfile.Version { + case 1: + e.Compiler = &compilerv1.CompilerV1{ + Dir: e.Dir, + Vars: e.taskvars, + Logger: e.Logger, + } + case 2: + e.Compiler = &compilerv2.CompilerV2{ + Dir: e.Dir, + Vars: e.taskvars, + Logger: e.Logger, + } + + if !e.Silent { + e.Logger.Errf(`task: warning: Taskfile "version: 2" is experimental and implementation can change before v2.0.0 release`) + } + default: + return fmt.Errorf(`task: Unrecognized Taskfile version "%d"`, e.Taskfile.Version) + } + + e.taskCallCount = make(map[string]*int32, len(e.Taskfile.Tasks)) + for k := range e.Taskfile.Tasks { + e.taskCallCount[k] = new(int32) + } + return nil +} + // RunTask runs a task by its name -func (e *Executor) RunTask(ctx context.Context, call Call) error { +func (e *Executor) RunTask(ctx context.Context, call taskfile.Call) error { t, err := e.CompiledTask(call) if err != nil { return err @@ -123,13 +139,13 @@ func (e *Executor) RunTask(ctx context.Context, call Call) error { } if !e.Force { - upToDate, err := t.isUpToDate(ctx) + upToDate, err := isTaskUpToDate(ctx, t) if err != nil { return err } if upToDate { if !e.Silent { - e.errf(`task: Task "%s" is up to date`, t.Task) + e.Logger.Errf(`task: Task "%s" is up to date`, t.Task) } return nil } @@ -137,8 +153,8 @@ func (e *Executor) RunTask(ctx context.Context, call Call) error { for i := range t.Cmds { if err := e.runCommand(ctx, t, call, i); err != nil { - if err2 := t.statusOnError(); err2 != nil { - e.verboseErrf("task: error cleaning status on error: %v", err2) + if err2 := statusOnError(t); err2 != nil { + e.Logger.VerboseErrf("task: error cleaning status on error: %v", err2) } return &taskRunError{t.Task, err} } @@ -146,49 +162,49 @@ func (e *Executor) RunTask(ctx context.Context, call Call) error { return nil } -func (e *Executor) runDeps(ctx context.Context, t *Task) error { +func (e *Executor) runDeps(ctx context.Context, t *taskfile.Task) error { g, ctx := errgroup.WithContext(ctx) for _, d := range t.Deps { d := d g.Go(func() error { - return e.RunTask(ctx, Call{Task: d.Task, Vars: d.Vars}) + return e.RunTask(ctx, taskfile.Call{Task: d.Task, Vars: d.Vars}) }) } return g.Wait() } -func (e *Executor) runCommand(ctx context.Context, t *Task, call Call, i int) error { +func (e *Executor) runCommand(ctx context.Context, t *taskfile.Task, call taskfile.Call, i int) error { cmd := t.Cmds[i] if cmd.Cmd == "" { - return e.RunTask(ctx, Call{Task: cmd.Task, Vars: cmd.Vars}) + return e.RunTask(ctx, taskfile.Call{Task: cmd.Task, Vars: cmd.Vars}) } if e.Verbose || (!cmd.Silent && !t.Silent && !e.Silent) { - e.errf(cmd.Cmd) + e.Logger.Errf(cmd.Cmd) } return execext.RunCommand(&execext.RunCommandOptions{ Context: ctx, Command: cmd.Cmd, Dir: t.Dir, - Env: t.getEnviron(), + Env: getEnviron(t), Stdin: e.Stdin, Stdout: e.Stdout, Stderr: e.Stderr, }) } -func (t *Task) getEnviron() []string { +func getEnviron(t *taskfile.Task) []string { if t.Env == nil { return nil } envs := os.Environ() - for k, v := range t.Env.toStringMap() { + for k, v := range t.Env.ToStringMap() { envs = append(envs, fmt.Sprintf("%s=%s", k, v)) } return envs diff --git a/task_test.go b/task_test.go index b93de4db..78bfb1c1 100644 --- a/task_test.go +++ b/task_test.go @@ -10,6 +10,7 @@ import ( "testing" "github.com/go-task/task" + "github.com/go-task/task/internal/taskfile" "github.com/stretchr/testify/assert" ) @@ -38,7 +39,7 @@ func (fct fileContentTest) Run(t *testing.T) { Stderr: ioutil.Discard, } assert.NoError(t, e.ReadTaskfile(), "e.ReadTaskfile()") - assert.NoError(t, e.Run(task.Call{Task: fct.Target}), "e.Run(target)") + assert.NoError(t, e.Run(taskfile.Call{Task: fct.Target}), "e.Run(target)") for name, expectContent := range fct.Files { t.Run(fct.name(name), func(t *testing.T) { @@ -66,9 +67,9 @@ func TestEnv(t *testing.T) { tt.Run(t) } -func TestVars(t *testing.T) { +func TestVarsV1(t *testing.T) { tt := fileContentTest{ - Dir: "testdata/vars", + Dir: "testdata/vars/v1", Target: "default", TrimSpace: true, Files: map[string]string{ @@ -102,30 +103,69 @@ func TestVars(t *testing.T) { tt.Target = "hello" tt.Run(t) } -func TestMultilineVars(t *testing.T) { + +func TestVarsV2(t *testing.T) { tt := fileContentTest{ - Dir: "testdata/vars/multiline", + Dir: "testdata/vars/v2", Target: "default", - TrimSpace: false, + TrimSpace: true, 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", + "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": "", + "nestedtmpl_foo2.txt": "foo2", + "foo2.txt": "foo2", + "bar2.txt": "bar2", + "baz2.txt": "baz2", + "tmpl2_foo.txt": "", + "tmpl2_foo2.txt": "foo2", + "tmpl2_bar.txt": "", + "tmpl2_bar2.txt": "bar2", + "shtmpl2_foo.txt": "", + "shtmpl2_foo2.txt": "foo2", + "nestedtmpl2_foo2.txt": "", + "override.txt": "bar", }, } tt.Run(t) + // Ensure identical results when running hello task directly. + tt.Target = "hello" + tt.Run(t) +} + +func TestMultilineVars(t *testing.T) { + for _, dir := range []string{"testdata/vars/v1/multiline", "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 TestVarsInvalidTmpl(t *testing.T) { const ( - dir = "testdata/vars" + dir = "testdata/vars/v1" target = "invalid-var-tmpl" expectError = "template: :1: unexpected EOF" ) @@ -136,7 +176,7 @@ func TestVarsInvalidTmpl(t *testing.T) { Stderr: ioutil.Discard, } assert.NoError(t, e.ReadTaskfile(), "e.ReadTaskfile()") - assert.EqualError(t, e.Run(task.Call{Task: target}), expectError, "e.Run(target)") + assert.EqualError(t, e.Run(taskfile.Call{Task: target}), expectError, "e.Run(target)") } func TestParams(t *testing.T) { @@ -188,7 +228,7 @@ func TestDeps(t *testing.T) { Stderr: ioutil.Discard, } assert.NoError(t, e.ReadTaskfile()) - assert.NoError(t, e.Run(task.Call{Task: "default"})) + assert.NoError(t, e.Run(taskfile.Call{Task: "default"})) for _, f := range files { f = filepath.Join(dir, f) @@ -208,21 +248,22 @@ func TestStatus(t *testing.T) { t.Errorf("File should not exists: %v", err) } + var buff bytes.Buffer e := &task.Executor{ Dir: dir, - Stdout: ioutil.Discard, - Stderr: ioutil.Discard, + Stdout: &buff, + Stderr: &buff, + Silent: true, } assert.NoError(t, e.ReadTaskfile()) - assert.NoError(t, e.Run(task.Call{Task: "gen-foo"})) + assert.NoError(t, e.Run(taskfile.Call{Task: "gen-foo"})) if _, err := os.Stat(file); err != nil { t.Errorf("File should exists: %v", err) } - buff := bytes.NewBuffer(nil) - e.Stdout, e.Stderr = buff, buff - assert.NoError(t, e.Run(task.Call{Task: "gen-foo"})) + e.Silent = false + assert.NoError(t, e.Run(taskfile.Call{Task: "gen-foo"})) if buff.String() != `task: Task "gen-foo" is up to date`+"\n" { t.Errorf("Wrong output message: %s", buff.String()) @@ -261,7 +302,7 @@ func TestGenerates(t *testing.T) { fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask) // Run task for the first time. - assert.NoError(t, e.Run(task.Call{Task: theTask})) + assert.NoError(t, e.Run(taskfile.Call{Task: theTask})) if _, err := os.Stat(srcFile); err != nil { t.Errorf("File should exists: %v", err) @@ -276,7 +317,7 @@ func TestGenerates(t *testing.T) { buff.Reset() // Re-run task to ensure it's now found to be up-to-date. - assert.NoError(t, e.Run(task.Call{Task: theTask})) + assert.NoError(t, e.Run(taskfile.Call{Task: theTask})) if buff.String() != upToDate { t.Errorf("Wrong output message: %s", buff.String()) } @@ -307,14 +348,14 @@ func TestStatusChecksum(t *testing.T) { } assert.NoError(t, e.ReadTaskfile()) - assert.NoError(t, e.Run(task.Call{Task: "build"})) + assert.NoError(t, e.Run(taskfile.Call{Task: "build"})) for _, f := range files { _, err := os.Stat(filepath.Join(dir, f)) assert.NoError(t, err) } buff.Reset() - assert.NoError(t, e.Run(task.Call{Task: "build"})) + assert.NoError(t, e.Run(taskfile.Call{Task: "build"})) assert.Equal(t, `task: Task "build" is up to date`+"\n", buff.String()) } @@ -345,7 +386,7 @@ func TestCyclicDep(t *testing.T) { Stderr: ioutil.Discard, } assert.NoError(t, e.ReadTaskfile()) - assert.IsType(t, &task.MaximumTaskCallExceededError{}, e.Run(task.Call{Task: "task-1"})) + assert.IsType(t, &task.MaximumTaskCallExceededError{}, e.Run(taskfile.Call{Task: "task-1"})) } func TestTaskVersion(t *testing.T) { diff --git a/taskfile.go b/taskfile.go index 6b8fc3d1..d6ebf9d8 100644 --- a/taskfile.go +++ b/taskfile.go @@ -6,35 +6,12 @@ import ( "path/filepath" "runtime" + "github.com/go-task/task/internal/taskfile" + "github.com/imdario/mergo" "gopkg.in/yaml.v2" ) -// Taskfile represents a Taskfile.yml -type Taskfile struct { - // TODO: version is still not used - Version int - Tasks Tasks -} - -// UnmarshalYAML implements yaml.Unmarshaler interface -func (tf *Taskfile) UnmarshalYAML(unmarshal func(interface{}) error) error { - if err := unmarshal(&tf.Tasks); err == nil { - return nil - } - - var taskfile struct { - Version int - Tasks Tasks - } - if err := unmarshal(&taskfile); err != nil { - return err - } - tf.Version = taskfile.Version - tf.Tasks = taskfile.Tasks - return nil -} - // ReadTaskfile parses Taskfile from the disk func (e *Executor) ReadTaskfile() error { path := filepath.Join(e.Dir, TaskFilePath) @@ -64,9 +41,9 @@ func (e *Executor) ReadTaskfile() error { return e.readTaskvars() } -func (e *Executor) readTaskfileData(path string) (*Taskfile, error) { +func (e *Executor) readTaskfileData(path string) (*taskfile.Taskfile, error) { if b, err := ioutil.ReadFile(path + ".yml"); err == nil { - var taskfile Taskfile + var taskfile taskfile.Taskfile return &taskfile, yaml.UnmarshalStrict(b, &taskfile) } return nil, taskFileNotFound{path} @@ -85,7 +62,7 @@ func (e *Executor) readTaskvars() error { } if b, err := ioutil.ReadFile(osSpecificFile + ".yml"); err == nil { - osTaskvars := make(Vars, 10) + osTaskvars := make(taskfile.Vars, 10) if err := yaml.UnmarshalStrict(b, &osTaskvars); err != nil { return err } diff --git a/testdata/vars/.gitignore b/testdata/vars/v1/.gitignore similarity index 100% rename from testdata/vars/.gitignore rename to testdata/vars/v1/.gitignore diff --git a/testdata/vars/Taskfile.yml b/testdata/vars/v1/Taskfile.yml similarity index 100% rename from testdata/vars/Taskfile.yml rename to testdata/vars/v1/Taskfile.yml diff --git a/testdata/vars/Taskvars.yml b/testdata/vars/v1/Taskvars.yml similarity index 100% rename from testdata/vars/Taskvars.yml rename to testdata/vars/v1/Taskvars.yml diff --git a/testdata/vars/multiline/Taskfile.yml b/testdata/vars/v1/multiline/Taskfile.yml similarity index 100% rename from testdata/vars/multiline/Taskfile.yml rename to testdata/vars/v1/multiline/Taskfile.yml diff --git a/testdata/vars/v2/.gitignore b/testdata/vars/v2/.gitignore new file mode 100644 index 00000000..2211df63 --- /dev/null +++ b/testdata/vars/v2/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/testdata/vars/v2/Taskfile.yml b/testdata/vars/v2/Taskfile.yml new file mode 100644 index 00000000..8f2324eb --- /dev/null +++ b/testdata/vars/v2/Taskfile.yml @@ -0,0 +1,50 @@ +version: 2 +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 + vars: + FOO: foo + BAR: $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" + + invalid-var-tmpl: + vars: + CHARS: "abcd" + INVALID: "{{range .CHARS}}no end" diff --git a/testdata/vars/v2/Taskvars.yml b/testdata/vars/v2/Taskvars.yml new file mode 100644 index 00000000..7b5bfb33 --- /dev/null +++ b/testdata/vars/v2/Taskvars.yml @@ -0,0 +1,12 @@ +FOO2: foo2 +BAR2: $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" diff --git a/testdata/vars/v2/multiline/Taskfile.yml b/testdata/vars/v2/multiline/Taskfile.yml new file mode 100644 index 00000000..8587a367 --- /dev/null +++ b/testdata/vars/v2/multiline/Taskfile.yml @@ -0,0 +1,45 @@ +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 diff --git a/variables.go b/variables.go index 526a2ed6..ca0722bb 100644 --- a/variables.go +++ b/variables.go @@ -1,17 +1,11 @@ package task import ( - "bytes" - "errors" - "os" "path/filepath" - "runtime" - "strings" - "text/template" - "github.com/go-task/task/internal/execext" + "github.com/go-task/task/internal/taskfile" + "github.com/go-task/task/internal/templater" - "github.com/Masterminds/sprig" "github.com/mitchellh/go-homedir" ) @@ -20,206 +14,31 @@ var ( TaskvarsFilePath = "Taskvars" ) -var ( - // ErrCantUnmarshalVar is returned for invalid var YAML. - ErrCantUnmarshalVar = errors.New("task: can't unmarshal var value") -) - -// Vars is a string[string] variables map. -type Vars map[string]Var - -func getEnvironmentVariables() Vars { - var ( - env = os.Environ() - m = make(Vars, len(env)) - ) - - for _, e := range env { - keyVal := strings.SplitN(e, "=", 2) - key, val := keyVal[0], keyVal[1] - m[key] = Var{Static: val} - } - return m -} - -func (vs Vars) toStringMap() (m map[string]string) { - m = make(map[string]string, len(vs)) - for k, v := range vs { - if v.Sh != "" { - // Dynamic variable is not yet resolved; trigger - // to be used in templates. - continue - } - m[k] = v.Static - } - return -} - -// Var represents either a static or dynamic variable. -type Var struct { - Static string - Sh string -} - -// UnmarshalYAML implements yaml.Unmarshaler interface. -func (v *Var) UnmarshalYAML(unmarshal func(interface{}) error) error { - var str string - if err := unmarshal(&str); err == nil { - if strings.HasPrefix(str, "$") { - v.Sh = strings.TrimPrefix(str, "$") - } else { - v.Static = str - } - return nil - } - - var sh struct { - Sh string - } - if err := unmarshal(&sh); err == nil { - v.Sh = sh.Sh - return nil - } - - return ErrCantUnmarshalVar -} - -// getVariables returns fully resolved variables following the priority order: -// 1. Call variables (should already have been resolved) -// 2. Environment (should not need to be resolved) -// 3. Task variables, resolved with access to: -// - call, taskvars and environment variables -// 4. Taskvars variables, resolved with access to: -// - environment variables -func (e *Executor) getVariables(call Call) (Vars, error) { - t, ok := e.Taskfile.Tasks[call.Task] - if !ok { - return nil, &taskNotFoundError{call.Task} - } - - merge := func(dest Vars, srcs ...Vars) { - for _, src := range srcs { - for k, v := range src { - dest[k] = v - } - } - } - varsKeys := func(srcs ...Vars) []string { - m := make(map[string]struct{}) - for _, src := range srcs { - for k := range src { - m[k] = struct{}{} - } - } - lst := make([]string, 0, len(m)) - for k := range m { - lst = append(lst, k) - } - return lst - } - replaceVars := func(dest Vars, keys []string) error { - r := varReplacer{vars: dest} - for _, k := range keys { - v := dest[k] - dest[k] = Var{ - Static: r.replace(v.Static), - Sh: r.replace(v.Sh), - } - } - return r.err - } - resolveShell := func(dest Vars, keys []string) error { - for _, k := range keys { - v := dest[k] - static, err := e.handleShVar(v) - if err != nil { - return err - } - dest[k] = Var{Static: static} - } - return nil - } - update := func(dest Vars, srcs ...Vars) error { - merge(dest, srcs...) - // updatedKeys ensures template evaluation is run only once. - updatedKeys := varsKeys(srcs...) - if err := replaceVars(dest, updatedKeys); err != nil { - return err - } - return resolveShell(dest, updatedKeys) - } - - // Resolve taskvars variables to "result" with environment override variables. - override := getEnvironmentVariables() - result := make(Vars, len(e.taskvars)+len(t.Vars)+len(override)) - if err := update(result, e.taskvars, override); err != nil { - return nil, err - } - // Resolve task variables to "result" with environment and call override variables. - merge(override, call.Vars) - if err := update(result, t.Vars, override); err != nil { - return nil, err - } - return result, nil -} - -func (e *Executor) handleShVar(v Var) (string, error) { - if v.Static != "" || v.Sh == "" { - return v.Static, nil - } - e.muDynamicCache.Lock() - defer e.muDynamicCache.Unlock() - - if result, ok := e.dynamicCache[v.Sh]; ok { - return result, nil - } - - var stdout bytes.Buffer - opts := &execext.RunCommandOptions{ - Command: v.Sh, - Dir: e.Dir, - Stdout: &stdout, - Stderr: e.Stderr, - } - if err := execext.RunCommand(opts); err != nil { - return "", &dynamicVarError{cause: err, cmd: opts.Command} - } - - // 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") - - e.dynamicCache[v.Sh] = result - e.verboseErrf(`task: dynamic variable: '%s' result: '%s'`, v.Sh, result) - - return result, nil -} - // CompiledTask returns a copy of a task, but replacing variables in almost all // properties using the Go template package. -func (e *Executor) CompiledTask(call Call) (*Task, error) { +func (e *Executor) CompiledTask(call taskfile.Call) (*taskfile.Task, error) { origTask, ok := e.Taskfile.Tasks[call.Task] if !ok { return nil, &taskNotFoundError{call.Task} } - vars, err := e.getVariables(call) + vars, err := e.Compiler.GetVariables(origTask, call) if err != nil { return nil, err } - r := varReplacer{vars: vars} + r := templater.Templater{Vars: vars} - new := Task{ + new := taskfile.Task{ Task: origTask.Task, - Desc: r.replace(origTask.Desc), - Sources: r.replaceSlice(origTask.Sources), - Generates: r.replaceSlice(origTask.Generates), - Status: r.replaceSlice(origTask.Status), - Dir: r.replace(origTask.Dir), + Desc: r.Replace(origTask.Desc), + Sources: r.ReplaceSlice(origTask.Sources), + Generates: r.ReplaceSlice(origTask.Generates), + Status: r.ReplaceSlice(origTask.Status), + Dir: r.Replace(origTask.Dir), Vars: nil, - Env: r.replaceVars(origTask.Env), + Env: r.ReplaceVars(origTask.Env), Silent: origTask.Silent, - Method: r.replace(origTask.Method), + Method: r.Replace(origTask.Method), } new.Dir, err = homedir.Expand(new.Dir) if err != nil { @@ -229,136 +48,34 @@ func (e *Executor) CompiledTask(call Call) (*Task, error) { new.Dir = filepath.Join(e.Dir, new.Dir) } for k, v := range new.Env { - static, err := e.handleShVar(v) + static, err := e.Compiler.HandleDynamicVar(v) if err != nil { return nil, err } - new.Env[k] = Var{Static: static} + new.Env[k] = taskfile.Var{Static: static} } if len(origTask.Cmds) > 0 { - new.Cmds = make([]*Cmd, len(origTask.Cmds)) + new.Cmds = make([]*taskfile.Cmd, len(origTask.Cmds)) for i, cmd := range origTask.Cmds { - new.Cmds[i] = &Cmd{ - Task: r.replace(cmd.Task), + new.Cmds[i] = &taskfile.Cmd{ + Task: r.Replace(cmd.Task), Silent: cmd.Silent, - Cmd: r.replace(cmd.Cmd), - Vars: r.replaceVars(cmd.Vars), + Cmd: r.Replace(cmd.Cmd), + Vars: r.ReplaceVars(cmd.Vars), } } } if len(origTask.Deps) > 0 { - new.Deps = make([]*Dep, len(origTask.Deps)) + new.Deps = make([]*taskfile.Dep, len(origTask.Deps)) for i, dep := range origTask.Deps { - new.Deps[i] = &Dep{ - Task: r.replace(dep.Task), - Vars: r.replaceVars(dep.Vars), + new.Deps[i] = &taskfile.Dep{ + Task: r.Replace(dep.Task), + Vars: r.ReplaceVars(dep.Vars), } } } - return &new, r.err -} - -// varReplacer is a help struct that allow us to call "replaceX" funcs multiple -// times, without having to check for error each time. The first error that -// happen will be assigned to r.err, and consecutive calls to funcs will just -// return the zero value. -type varReplacer struct { - vars Vars - strMap map[string]string - err error -} - -func (r *varReplacer) replace(str string) string { - if r.err != nil || str == "" { - return "" - } - - templ, err := template.New("").Funcs(templateFuncs).Parse(str) - if err != nil { - r.err = err - return "" - } - - if r.strMap == nil { - r.strMap = r.vars.toStringMap() - } - - var b bytes.Buffer - if err = templ.Execute(&b, r.strMap); err != nil { - r.err = err - return "" - } - return b.String() -} - -func (r *varReplacer) replaceSlice(strs []string) []string { - if r.err != nil || len(strs) == 0 { - return nil - } - - new := make([]string, len(strs)) - for i, str := range strs { - new[i] = r.replace(str) - } - return new -} - -func (r *varReplacer) replaceVars(vars Vars) Vars { - if r.err != nil || len(vars) == 0 { - return nil - } - - new := make(Vars, len(vars)) - for k, v := range vars { - new[k] = Var{ - Static: r.replace(v.Static), - Sh: r.replace(v.Sh), - } - } - return new -} - -var ( - templateFuncs template.FuncMap -) - -func init() { - taskFuncs := template.FuncMap{ - "OS": func() string { return runtime.GOOS }, - "ARCH": func() string { return runtime.GOARCH }, - "catLines": func(s string) string { - s = strings.Replace(s, "\r\n", " ", -1) - return strings.Replace(s, "\n", " ", -1) - }, - "splitLines": func(s string) []string { - s = strings.Replace(s, "\r\n", "\n", -1) - return strings.Split(s, "\n") - }, - "fromSlash": func(path string) string { - return filepath.FromSlash(path) - }, - "toSlash": func(path string) string { - return filepath.ToSlash(path) - }, - "exeExt": func() string { - if runtime.GOOS == "windows" { - return ".exe" - } - return "" - }, - // IsSH is deprecated. - "IsSH": func() bool { return true }, - } - // Deprecated aliases for renamed functions. - taskFuncs["FromSlash"] = taskFuncs["fromSlash"] - taskFuncs["ToSlash"] = taskFuncs["toSlash"] - taskFuncs["ExeExt"] = taskFuncs["exeExt"] - - templateFuncs = sprig.TxtFuncMap() - for k, v := range taskFuncs { - templateFuncs[k] = v - } + return &new, r.Err() } diff --git a/watch.go b/watch.go index 33bacc84..43d346b2 100644 --- a/watch.go +++ b/watch.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/go-task/task/internal/taskfile" "github.com/mattn/go-zglob" "github.com/radovskyb/watcher" ) @@ -15,19 +16,19 @@ var watchIgnoredDirs = []string{ } // watchTasks start watching the given tasks -func (e *Executor) watchTasks(calls ...Call) error { +func (e *Executor) watchTasks(calls ...taskfile.Call) error { tasks := make([]string, len(calls)) for i, c := range calls { tasks[i] = c.Task } - e.errf("task: Started watching for tasks: %s", strings.Join(tasks, ", ")) + e.Logger.Errf("task: Started watching for tasks: %s", strings.Join(tasks, ", ")) ctx, cancel := context.WithCancel(context.Background()) for _, c := range calls { c := c go func() { if err := e.RunTask(ctx, c); err != nil && !isContextError(err) { - e.errf("%v", err) + e.Logger.Errf("%v", err) } }() } @@ -43,7 +44,7 @@ func (e *Executor) watchTasks(calls ...Call) error { for { select { case event := <-w.Event: - e.verboseErrf("task: received watch event: %v", event) + e.Logger.VerboseErrf("task: received watch event: %v", event) cancel() ctx, cancel = context.WithCancel(context.Background()) @@ -51,7 +52,7 @@ func (e *Executor) watchTasks(calls ...Call) error { c := c go func() { if err := e.RunTask(ctx, c); err != nil && !isContextError(err) { - e.errf("%v", err) + e.Logger.Errf("%v", err) } }() } @@ -62,7 +63,7 @@ func (e *Executor) watchTasks(calls ...Call) error { w.TriggerEvent(watcher.Remove, nil) }() default: - e.errf("%v", err) + e.Logger.Errf("%v", err) } case <-w.Closed: return @@ -74,7 +75,7 @@ func (e *Executor) watchTasks(calls ...Call) error { // re-register each second because we can have new files for { if err := e.registerWatchedFiles(w, calls...); err != nil { - e.errf("%v", err) + e.Logger.Errf("%v", err) } time.Sleep(time.Second) } @@ -83,7 +84,7 @@ func (e *Executor) watchTasks(calls ...Call) error { return w.Start(time.Second) } -func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...Call) error { +func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...taskfile.Call) error { oldWatchedFiles := make(map[string]struct{}) for f := range w.WatchedFiles() { oldWatchedFiles[f] = struct{}{} @@ -95,21 +96,21 @@ func (e *Executor) registerWatchedFiles(w *watcher.Watcher, calls ...Call) error } } - var registerTaskFiles func(Call) error - registerTaskFiles = func(c Call) error { + var registerTaskFiles func(taskfile.Call) error + registerTaskFiles = func(c taskfile.Call) error { task, err := e.CompiledTask(c) if err != nil { return err } for _, d := range task.Deps { - if err := registerTaskFiles(Call{Task: d.Task, Vars: d.Vars}); err != nil { + if err := registerTaskFiles(taskfile.Call{Task: d.Task, Vars: d.Vars}); err != nil { return err } } for _, c := range task.Cmds { if c.Task != "" { - if err := registerTaskFiles(Call{Task: c.Task, Vars: c.Vars}); err != nil { + if err := registerTaskFiles(taskfile.Call{Task: c.Task, Vars: c.Vars}); err != nil { return err } }