diff --git a/command.go b/command.go new file mode 100644 index 00000000..cbcddb3b --- /dev/null +++ b/command.go @@ -0,0 +1,64 @@ +package task + +import ( + "errors" + "strings" +) + +type Params map[string]string + +type Cmd struct { + Cmd string + Task string + Params Params +} + +type Dep struct { + Task string + Params Params +} + +var ( + ErrCantUnmarshalCmd = errors.New("task: can't unmarshal cmd value") + ErrCantUnmarshalDep = errors.New("task: can't unmarshal dep value") +) + +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 + Params Params + } + if err := unmarshal(&taskCall); err == nil { + c.Task = taskCall.Task + c.Params = taskCall.Params + return nil + } + return ErrCantUnmarshalCmd +} + +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 + Params Params + } + if err := unmarshal(&taskCall); err == nil { + d.Task = taskCall.Task + d.Params = taskCall.Params + return nil + } + return ErrCantUnmarshalDep +} diff --git a/command_test.go b/command_test.go new file mode 100644 index 00000000..74b4e31e --- /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 +params: + 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", Params: task.Params{"PARAM1": "VALUE1", "PARAM2": "VALUE2"}}, + }, + { + yamlDep, + &task.Dep{}, + &task.Dep{Task: "task-name"}, + }, + { + yamlTaskCall, + &task.Dep{}, + &task.Dep{Task: "another-task", Params: task.Params{"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/task.go b/task.go index 07378958..acae9e22 100644 --- a/task.go +++ b/task.go @@ -38,8 +38,8 @@ 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 @@ -83,7 +83,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 +91,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, params Params) 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, params); err != nil { return err } if !e.Force { - upToDate, err := e.isTaskUpToDate(ctx, name) + upToDate, err := e.isTaskUpToDate(ctx, name, params) if err != nil { return err } @@ -113,27 +113,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, params); 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, params Params) 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, params) if err != nil { return err } - if err = e.RunTask(ctx, dep); err != nil { + if err = e.RunTask(ctx, dep, d.Params); err != nil { return err } return nil @@ -146,28 +146,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, params Params) (bool, error) { t := e.Tasks[task] if len(t.Status) > 0 { - return e.isUpToDateStatus(ctx, task) + return e.isUpToDateStatus(ctx, task, params) } - return e.isUpToDateTimestamp(ctx, task) + return e.isUpToDateTimestamp(ctx, task, params) } -func (e *Executor) isUpToDateStatus(ctx context.Context, task string) (bool, error) { +func (e *Executor) isUpToDateStatus(ctx context.Context, task string, params Params) (bool, error) { t := e.Tasks[task] - environ, err := e.getEnviron(task) + environ, err := e.getEnviron(task, params) if err != nil { return false, err } - dir, err := e.getTaskDir(task) + dir, err := e.getTaskDir(task, params) + if err != nil { + return false, err + } + status, err := e.ReplaceSliceVariables(t.Status, task, params) 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 +185,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, params Params) (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, params) if err != nil { return false, err } - sources, err := e.ReplaceSliceVariables(task, t.Sources) + sources, err := e.ReplaceSliceVariables(t.Sources, task, params) if err != nil { return false, err } - generates, err := e.ReplaceSliceVariables(task, t.Generates) + generates, err := e.ReplaceSliceVariables(t.Generates, task, params) if err != nil { return false, err } @@ -215,28 +219,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, params Params) 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.Params) + } + + c, err := e.ReplaceVariables(cmd.Cmd, task, params) 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, params) if err != nil { return err } - envs, err := e.getEnviron(task) + envs, err := e.getEnviron(task, params) if err != nil { return err } @@ -266,14 +267,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, params Params) (string, error) { + t := e.Tasks[task] - exeDir, err := e.ReplaceVariables(name, e.Dir) + exeDir, err := e.ReplaceVariables(e.Dir, task, params) if err != nil { return "", err } - taskDir, err := e.ReplaceVariables(name, t.Dir) + taskDir, err := e.ReplaceVariables(t.Dir, task, params) if err != nil { return "", err } @@ -281,7 +282,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, params Params) ([]string, error) { t := e.Tasks[task] if t.Env == nil { @@ -291,7 +292,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, params) 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..acc60bc5 --- /dev/null +++ b/testdata/params/Taskfile.yml @@ -0,0 +1,27 @@ +default: + deps: + - task: write-file + params: + CONTENT: Dependence1 + FILE: dep1.txt + - task: write-file + params: + CONTENT: Dependence2 + FILE: dep2.txt + cmds: + - task: write-file + params: + CONTENT: Hello + FILE: hello.txt + - task: write-file + params: + CONTENT: World + FILE: world.txt + - task: write-file + params: + CONTENT: "!" + FILE: exclamation.txt + +write-file: + cmds: + - echo {{.CONTENT}} > {{.FILE}} diff --git a/variable_handling.go b/variable_handling.go index 08cd7685..f3c42c26 100644 --- a/variable_handling.go +++ b/variable_handling.go @@ -52,7 +52,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, params Params) (map[string]string, error) { t := e.Tasks[task] localVariables := make(map[string]string) @@ -77,6 +77,15 @@ func (e *Executor) getVariables(task string) (map[string]string, error) { for key, value := range getEnvironmentVariables() { localVariables[key] = value } + if params != nil { + for k, v := range params { + val, err := e.handleDynamicVariableContent(v) + if err != nil { + return nil, err + } + localVariables[k] = val + } + } return localVariables, nil } @@ -109,11 +118,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, params Params) ([]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, params) if err != nil { return nil, err } @@ -122,8 +131,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, params Params) (string, error) { + vars, err := e.getVariables(task, params) if err != nil { return "", err } 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 {