diff --git a/README.md b/README.md index de109bad..9f18691f 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,7 @@ your `PATH`. DEB and RPM packages are also available. ## Usage Create a file called `Taskfile.yml` in the root of the project. -(`Taskfile.toml` and `Taskfile.json` are also supported, but YAML is used in -the documentation). The `cmds` attribute should contains the commands of a -task: +The `cmds` attribute should contains the commands of a task: ```yml build: @@ -167,22 +165,53 @@ The above will fail with the message: "cyclic dependency detected". When a task has many dependencies, they are executed concurrently. This will often result in a faster build pipeline. But in some situations you may need -to call other tasks serially. For this just prefix a command with `^`: +to call other tasks serially. In this case, just use the following syntax: + +```yml +main-task: + cmds: + - task: task-to-be-called + - task: another-task + - echo "Both done" + +task-to-be-called: + cmds: + - echo "Task to be called" + +another-task: + cmds: + - echo "Another task" +``` + +Overriding variables in the called task is as simple as informing `vars` +attribute: + +```yml +main-task: + cmds: + - task: write-file + vars: {FILE: "hello.txt", CONTENT: "Hello!"} + - task: write-file + vars: {FILE: "world.txt", CONTENT: "World!"} + +write-file: + cmds: + - echo "{{.CONTENT}}" > {{.FILE}} +``` + +The above syntax is also supported in `deps`. + +> NOTE: It's also possible to call a task without any param prefixing it +with `^`, but this syntax is deprecaded: ```yml a-task: cmds: - ^another-task - - ^even-another-task - - echo "Both done" another-task: cmds: - - ... - -even-another-task: - cmds: - - ... + - echo "Another task" ``` ### Prevent unnecessary work @@ -256,7 +285,7 @@ setvar: The above sample saves the path into a new variable which is then again echoed. You can use environment variables, task level variables and a file called -`Taskvars.yml` (or `Taskvars.toml` or `Taskvars.json`) as source of variables. +`Taskvars.yml` as source of variables. They are evaluated in the following order: diff --git a/command.go b/command.go new file mode 100644 index 00000000..4c6b19bd --- /dev/null +++ b/command.go @@ -0,0 +1,68 @@ +package task + +import ( + "errors" + "strings" +) + +// Cmd is a task command +type Cmd struct { + Cmd string + Task string + Vars Vars +} + +// Dep is a task dependency +type Dep struct { + Task string + Vars Vars +} + +var ( + // ErrCantUnmarshalCmd is returned for invalid command YAML + ErrCantUnmarshalCmd = errors.New("task: can't unmarshal cmd value") + // ErrCantUnmarshalDep is returned for invalid dependency YAML + ErrCantUnmarshalDep = errors.New("task: can't unmarshal dep value") +) + +// UnmarshalYAML implements yaml.Unmarshaler interface +func (c *Cmd) UnmarshalYAML(unmarshal func(interface{}) error) error { + var cmd string + if err := unmarshal(&cmd); err == nil { + if strings.HasPrefix(cmd, "^") { + c.Task = strings.TrimPrefix(cmd, "^") + } else { + c.Cmd = cmd + } + return nil + } + var taskCall struct { + Task string + Vars Vars + } + if err := unmarshal(&taskCall); err == nil { + c.Task = taskCall.Task + c.Vars = taskCall.Vars + return nil + } + return ErrCantUnmarshalCmd +} + +// UnmarshalYAML implements yaml.Unmarshaler interface +func (d *Dep) UnmarshalYAML(unmarshal func(interface{}) error) error { + var task string + if err := unmarshal(&task); err == nil { + d.Task = task + return nil + } + var taskCall struct { + Task string + Vars Vars + } + if err := unmarshal(&taskCall); err == nil { + d.Task = taskCall.Task + d.Vars = taskCall.Vars + return nil + } + return ErrCantUnmarshalDep +} diff --git a/command_test.go b/command_test.go new file mode 100644 index 00000000..77c2def3 --- /dev/null +++ b/command_test.go @@ -0,0 +1,54 @@ +package task_test + +import ( + "testing" + + "github.com/go-task/task" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" +) + +func TestCmdParse(t *testing.T) { + const ( + yamlCmd = `echo "a string command"` + yamlDep = `"task-name"` + yamlTaskCall = ` +task: another-task +vars: + PARAM1: VALUE1 + PARAM2: VALUE2 +` + ) + tests := []struct { + content string + v interface{} + expected interface{} + }{ + { + yamlCmd, + &task.Cmd{}, + &task.Cmd{Cmd: `echo "a string command"`}, + }, + { + yamlTaskCall, + &task.Cmd{}, + &task.Cmd{Task: "another-task", Vars: task.Vars{"PARAM1": "VALUE1", "PARAM2": "VALUE2"}}, + }, + { + yamlDep, + &task.Dep{}, + &task.Dep{Task: "task-name"}, + }, + { + yamlTaskCall, + &task.Dep{}, + &task.Dep{Task: "another-task", Vars: task.Vars{"PARAM1": "VALUE1", "PARAM2": "VALUE2"}}, + }, + } + for _, test := range tests { + err := yaml.Unmarshal([]byte(test.content), test.v) + assert.NoError(t, err) + assert.Equal(t, test.expected, test.v) + } +} diff --git a/cyclic.go b/cyclic.go index 236ef4f6..f7c9387d 100644 --- a/cyclic.go +++ b/cyclic.go @@ -13,7 +13,7 @@ func (e *Executor) HasCyclicDep() bool { defer delete(visits, name) for _, d := range t.Deps { - if !checkCyclicDep(d, e.Tasks[d]) { + if !checkCyclicDep(d.Task, e.Tasks[d.Task]) { return false } } diff --git a/cyclic_test.go b/cyclic_test.go index ab3c3c45..c0c00c61 100644 --- a/cyclic_test.go +++ b/cyclic_test.go @@ -10,10 +10,10 @@ func TestCyclicDepCheck(t *testing.T) { isCyclic := &task.Executor{ Tasks: task.Tasks{ "task-a": &task.Task{ - Deps: []string{"task-b"}, + Deps: []*task.Dep{&task.Dep{Task: "task-b"}}, }, "task-b": &task.Task{ - Deps: []string{"task-a"}, + Deps: []*task.Dep{&task.Dep{Task: "task-a"}}, }, }, } @@ -25,10 +25,10 @@ func TestCyclicDepCheck(t *testing.T) { isNotCyclic := &task.Executor{ Tasks: task.Tasks{ "task-a": &task.Task{ - Deps: []string{"task-c"}, + Deps: []*task.Dep{&task.Dep{Task: "task-c"}}, }, "task-b": &task.Task{ - Deps: []string{"task-c"}, + Deps: []*task.Dep{&task.Dep{Task: "task-c"}}, }, "task-c": &task.Task{}, }, diff --git a/example/Taskfile.json b/example/Taskfile.json deleted file mode 100644 index 032cf0e1..00000000 --- a/example/Taskfile.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "hello": { - "cmds": [ - "echo \"I am going to write a file named 'output.txt' now.\"", - "echo \"hello\" > output.txt" - ], - "generates": [ - "output.txt" - ] - } -} diff --git a/example/Taskfile.toml b/example/Taskfile.toml deleted file mode 100644 index c9e53661..00000000 --- a/example/Taskfile.toml +++ /dev/null @@ -1,6 +0,0 @@ -[hello] -cmds = [ - "echo \"I am going to write a file named 'output.txt' now.\"", - "echo \"hello\" > output.txt" -] -generates = ["output.txt"] diff --git a/read_taskfile.go b/read_taskfile.go index cc5da31d..ec48742c 100644 --- a/read_taskfile.go +++ b/read_taskfile.go @@ -1,13 +1,11 @@ package task import ( - "encoding/json" "fmt" "io/ioutil" "path/filepath" "runtime" - "github.com/BurntSushi/toml" "github.com/imdario/mergo" "gopkg.in/yaml.v2" ) @@ -26,7 +24,6 @@ func (e *Executor) ReadTaskfile() error { if err != nil { switch err.(type) { case taskFileNotFound: - return nil default: return err } @@ -34,6 +31,9 @@ func (e *Executor) ReadTaskfile() error { if err := mergo.MapWithOverwrite(&e.Tasks, osTasks); err != nil { return err } + if err := e.readTaskvarsFile(); err != nil { + return err + } return nil } @@ -41,11 +41,16 @@ func (e *Executor) readTaskfileData(path string) (tasks map[string]*Task, err er if b, err := ioutil.ReadFile(path + ".yml"); err == nil { return tasks, yaml.Unmarshal(b, &tasks) } - if b, err := ioutil.ReadFile(path + ".json"); err == nil { - return tasks, json.Unmarshal(b, &tasks) - } - if b, err := ioutil.ReadFile(path + ".toml"); err == nil { - return tasks, toml.Unmarshal(b, &tasks) - } return nil, taskFileNotFound{path} } + +func (e *Executor) readTaskvarsFile() error { + file := filepath.Join(e.Dir, TaskvarsFilePath) + + if b, err := ioutil.ReadFile(file + ".yml"); err == nil { + if err := yaml.Unmarshal(b, &e.taskvars); err != nil { + return err + } + } + return nil +} diff --git a/task.go b/task.go index 07378958..ab4e5960 100644 --- a/task.go +++ b/task.go @@ -30,24 +30,28 @@ type Executor struct { Stdout io.Writer Stderr io.Writer + taskvars Vars watchingFiles map[string]struct{} } +// Vars is a string[string] variables map +type Vars map[string]string + // Tasks representas a group of tasks type Tasks map[string]*Task // Task represents a task type Task struct { - Cmds []string - Deps []string + Cmds []*Cmd + Deps []*Dep Desc string Sources []string Generates []string Status []string Dir string - Vars map[string]string + Vars Vars Set string - Env map[string]string + Env Vars } // Run runs Task @@ -83,7 +87,7 @@ func (e *Executor) Run(args ...string) error { } for _, a := range args { - if err := e.RunTask(context.Background(), a); err != nil { + if err := e.RunTask(context.Background(), a, nil); err != nil { return err } } @@ -91,18 +95,18 @@ func (e *Executor) Run(args ...string) error { } // RunTask runs a task by its name -func (e *Executor) RunTask(ctx context.Context, name string) error { +func (e *Executor) RunTask(ctx context.Context, name string, vars Vars) error { t, ok := e.Tasks[name] if !ok { return &taskNotFoundError{name} } - if err := e.runDeps(ctx, name); err != nil { + if err := e.runDeps(ctx, name, vars); err != nil { return err } if !e.Force { - upToDate, err := e.isTaskUpToDate(ctx, name) + upToDate, err := e.isTaskUpToDate(ctx, name, vars) if err != nil { return err } @@ -113,27 +117,27 @@ func (e *Executor) RunTask(ctx context.Context, name string) error { } for i := range t.Cmds { - if err := e.runCommand(ctx, name, i); err != nil { + if err := e.runCommand(ctx, name, i, vars); err != nil { return &taskRunError{name, err} } } return nil } -func (e *Executor) runDeps(ctx context.Context, task string) error { +func (e *Executor) runDeps(ctx context.Context, task string, vars Vars) error { g, ctx := errgroup.WithContext(ctx) t := e.Tasks[task] for _, d := range t.Deps { - dep := d + d := d g.Go(func() error { - dep, err := e.ReplaceVariables(task, dep) + dep, err := e.ReplaceVariables(d.Task, task, vars) if err != nil { return err } - if err = e.RunTask(ctx, dep); err != nil { + if err = e.RunTask(ctx, dep, d.Vars); err != nil { return err } return nil @@ -146,28 +150,32 @@ func (e *Executor) runDeps(ctx context.Context, task string) error { return nil } -func (e *Executor) isTaskUpToDate(ctx context.Context, task string) (bool, error) { +func (e *Executor) isTaskUpToDate(ctx context.Context, task string, vars Vars) (bool, error) { t := e.Tasks[task] if len(t.Status) > 0 { - return e.isUpToDateStatus(ctx, task) + return e.isUpToDateStatus(ctx, task, vars) } - return e.isUpToDateTimestamp(ctx, task) + return e.isUpToDateTimestamp(ctx, task, vars) } -func (e *Executor) isUpToDateStatus(ctx context.Context, task string) (bool, error) { +func (e *Executor) isUpToDateStatus(ctx context.Context, task string, vars Vars) (bool, error) { t := e.Tasks[task] - environ, err := e.getEnviron(task) + environ, err := e.getEnviron(task, vars) if err != nil { return false, err } - dir, err := e.getTaskDir(task) + dir, err := e.getTaskDir(task, vars) + if err != nil { + return false, err + } + status, err := e.ReplaceSliceVariables(t.Status, task, vars) if err != nil { return false, err } - for _, s := range t.Status { + for _, s := range status { err = execext.RunCommand(&execext.RunCommandOptions{ Context: ctx, Command: s, @@ -181,23 +189,23 @@ func (e *Executor) isUpToDateStatus(ctx context.Context, task string) (bool, err return true, nil } -func (e *Executor) isUpToDateTimestamp(ctx context.Context, task string) (bool, error) { +func (e *Executor) isUpToDateTimestamp(ctx context.Context, task string, vars Vars) (bool, error) { t := e.Tasks[task] if len(t.Sources) == 0 || len(t.Generates) == 0 { return false, nil } - dir, err := e.getTaskDir(task) + dir, err := e.getTaskDir(task, vars) if err != nil { return false, err } - sources, err := e.ReplaceSliceVariables(task, t.Sources) + sources, err := e.ReplaceSliceVariables(t.Sources, task, vars) if err != nil { return false, err } - generates, err := e.ReplaceSliceVariables(task, t.Generates) + generates, err := e.ReplaceSliceVariables(t.Generates, task, vars) if err != nil { return false, err } @@ -215,28 +223,25 @@ func (e *Executor) isUpToDateTimestamp(ctx context.Context, task string) (bool, return generatesMinTime.After(sourcesMaxTime), nil } -func (e *Executor) runCommand(ctx context.Context, task string, i int) error { +func (e *Executor) runCommand(ctx context.Context, task string, i int, vars Vars) error { t := e.Tasks[task] + cmd := t.Cmds[i] - c, err := e.ReplaceVariables(task, t.Cmds[i]) + if cmd.Cmd == "" { + return e.RunTask(ctx, cmd.Task, cmd.Vars) + } + + c, err := e.ReplaceVariables(cmd.Cmd, task, vars) if err != nil { return err } - if strings.HasPrefix(c, "^") { - c = strings.TrimPrefix(c, "^") - if err = e.RunTask(ctx, c); err != nil { - return err - } - return nil - } - - dir, err := e.getTaskDir(task) + dir, err := e.getTaskDir(task, vars) if err != nil { return err } - envs, err := e.getEnviron(task) + envs, err := e.getEnviron(task, vars) if err != nil { return err } @@ -266,14 +271,14 @@ func (e *Executor) runCommand(ctx context.Context, task string, i int) error { return nil } -func (e *Executor) getTaskDir(name string) (string, error) { - t := e.Tasks[name] +func (e *Executor) getTaskDir(task string, vars Vars) (string, error) { + t := e.Tasks[task] - exeDir, err := e.ReplaceVariables(name, e.Dir) + exeDir, err := e.ReplaceVariables(e.Dir, task, vars) if err != nil { return "", err } - taskDir, err := e.ReplaceVariables(name, t.Dir) + taskDir, err := e.ReplaceVariables(t.Dir, task, vars) if err != nil { return "", err } @@ -281,7 +286,7 @@ func (e *Executor) getTaskDir(name string) (string, error) { return filepath.Join(exeDir, taskDir), nil } -func (e *Executor) getEnviron(task string) ([]string, error) { +func (e *Executor) getEnviron(task string, vars Vars) ([]string, error) { t := e.Tasks[task] if t.Env == nil { @@ -291,7 +296,7 @@ func (e *Executor) getEnviron(task string) ([]string, error) { envs := os.Environ() for k, v := range t.Env { - env, err := e.ReplaceVariables(task, fmt.Sprintf("%s=%s", k, v)) + env, err := e.ReplaceVariables(fmt.Sprintf("%s=%s", k, v), task, vars) if err != nil { return nil, err } diff --git a/task_test.go b/task_test.go index d38e7446..98738d3b 100644 --- a/task_test.go +++ b/task_test.go @@ -166,3 +166,35 @@ func TestInit(t *testing.T) { t.Errorf("Taskfile.yml should exists") } } + +func TestParams(t *testing.T) { + const dir = "testdata/params" + var files = []struct { + file string + content string + }{ + {"hello.txt", "Hello\n"}, + {"world.txt", "World\n"}, + {"exclamation.txt", "!\n"}, + {"dep1.txt", "Dependence1\n"}, + {"dep2.txt", "Dependence2\n"}, + } + + for _, f := range files { + _ = os.Remove(filepath.Join(dir, f.file)) + } + + e := task.Executor{ + Dir: dir, + Stdout: ioutil.Discard, + Stderr: ioutil.Discard, + } + assert.NoError(t, e.ReadTaskfile()) + assert.NoError(t, e.Run("default")) + + for _, f := range files { + content, err := ioutil.ReadFile(filepath.Join(dir, f.file)) + assert.NoError(t, err) + assert.Equal(t, f.content, string(content)) + } +} diff --git a/testdata/params/.gitignore b/testdata/params/.gitignore new file mode 100644 index 00000000..2211df63 --- /dev/null +++ b/testdata/params/.gitignore @@ -0,0 +1 @@ +*.txt diff --git a/testdata/params/Taskfile.yml b/testdata/params/Taskfile.yml new file mode 100644 index 00000000..d04620a7 --- /dev/null +++ b/testdata/params/Taskfile.yml @@ -0,0 +1,17 @@ +default: + deps: + - task: write-file + vars: {CONTENT: Dependence1, FILE: dep1.txt} + - task: write-file + vars: {CONTENT: Dependence2, FILE: dep2.txt} + cmds: + - task: write-file + vars: {CONTENT: Hello, FILE: hello.txt} + - task: write-file + vars: {CONTENT: "$echo 'World'", FILE: world.txt} + - task: write-file + vars: {CONTENT: "!", FILE: exclamation.txt} + +write-file: + cmds: + - echo {{.CONTENT}} > {{.FILE}} diff --git a/variable_handling.go b/variable_handling.go index 08cd7685..5a6ab0a7 100644 --- a/variable_handling.go +++ b/variable_handling.go @@ -2,9 +2,7 @@ package task import ( "bytes" - "encoding/json" "errors" - "io/ioutil" "os" "path/filepath" "runtime" @@ -13,9 +11,7 @@ import ( "github.com/go-task/task/execext" - "github.com/BurntSushi/toml" "github.com/Masterminds/sprig" - "gopkg.in/yaml.v2" ) var ( @@ -52,7 +48,7 @@ func (e *Executor) handleDynamicVariableContent(value string) (string, error) { return result, nil } -func (e *Executor) getVariables(task string) (map[string]string, error) { +func (e *Executor) getVariables(task string, vars Vars) (map[string]string, error) { t := e.Tasks[task] localVariables := make(map[string]string) @@ -63,20 +59,27 @@ func (e *Executor) getVariables(task string) (map[string]string, error) { } localVariables[key] = val } - if fileVariables, err := e.readTaskvarsFile(); err == nil { - for key, value := range fileVariables { + if e.taskvars != nil { + for key, value := range e.taskvars { val, err := e.handleDynamicVariableContent(value) if err != nil { return nil, err } localVariables[key] = val } - } else { - return nil, err } for key, value := range getEnvironmentVariables() { localVariables[key] = value } + if vars != nil { + for k, v := range vars { + val, err := e.handleDynamicVariableContent(v) + if err != nil { + return nil, err + } + localVariables[k] = val + } + } return localVariables, nil } @@ -109,11 +112,11 @@ func init() { } // ReplaceSliceVariables writes vars into initial string slice -func (e *Executor) ReplaceSliceVariables(task string, initials []string) ([]string, error) { +func (e *Executor) ReplaceSliceVariables(initials []string, task string, vars Vars) ([]string, error) { result := make([]string, len(initials)) for i, s := range initials { var err error - result[i], err = e.ReplaceVariables(task, s) + result[i], err = e.ReplaceVariables(s, task, vars) if err != nil { return nil, err } @@ -122,8 +125,8 @@ func (e *Executor) ReplaceSliceVariables(task string, initials []string) ([]stri } // ReplaceVariables writes vars into initial string -func (e *Executor) ReplaceVariables(task, initial string) (string, error) { - vars, err := e.getVariables(task) +func (e *Executor) ReplaceVariables(initial, task string, vars Vars) (string, error) { + vars, err := e.getVariables(task, vars) if err != nil { return "", err } @@ -154,28 +157,3 @@ func getEnvironmentVariables() map[string]string { } return m } - -func (e *Executor) readTaskvarsFile() (map[string]string, error) { - file := filepath.Join(e.Dir, TaskvarsFilePath) - - var variables map[string]string - if b, err := ioutil.ReadFile(file + ".yml"); err == nil { - if err := yaml.Unmarshal(b, &variables); err != nil { - return nil, err - } - return variables, nil - } - if b, err := ioutil.ReadFile(file + ".json"); err == nil { - if err := json.Unmarshal(b, &variables); err != nil { - return nil, err - } - return variables, nil - } - if b, err := ioutil.ReadFile(file + ".toml"); err == nil { - if err := toml.Unmarshal(b, &variables); err != nil { - return nil, err - } - return variables, nil - } - return variables, nil -} diff --git a/watch.go b/watch.go index d203fe34..b58f8003 100644 --- a/watch.go +++ b/watch.go @@ -15,7 +15,7 @@ func (e *Executor) watchTasks(args ...string) error { // run tasks on init for _, a := range args { - if err := e.RunTask(context.Background(), a); err != nil { + if err := e.RunTask(context.Background(), a, nil); err != nil { e.println(err) break } @@ -41,7 +41,7 @@ loop: select { case <-watcher.Events: for _, a := range args { - if err := e.RunTask(context.Background(), a); err != nil { + if err := e.RunTask(context.Background(), a, nil); err != nil { e.println(err) continue loop } @@ -68,7 +68,11 @@ func (e *Executor) registerWatchedFiles(w *fsnotify.Watcher, args []string) erro if !ok { return &taskNotFoundError{a} } - if err := e.registerWatchedFiles(w, task.Deps); err != nil { + deps := make([]string, len(task.Deps)) + for i, d := range task.Deps { + deps[i] = d.Task + } + if err := e.registerWatchedFiles(w, deps); err != nil { return err } for _, s := range task.Sources {