1
0
mirror of https://github.com/go-task/task.git synced 2025-01-12 04:34:11 +02:00

Merge pull request #32 from go-task/parameters

Add task parameters
This commit is contained in:
Andrey Nering 2017-07-05 20:34:42 -03:00 committed by GitHub
commit cb72c404f5
14 changed files with 302 additions and 126 deletions

View File

@ -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:

68
command.go Normal file
View File

@ -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
}

54
command_test.go Normal file
View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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{},
},

View File

@ -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"
]
}
}

View File

@ -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"]

View File

@ -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
}

89
task.go
View File

@ -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
}

View File

@ -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))
}
}

1
testdata/params/.gitignore vendored Normal file
View File

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

17
testdata/params/Taskfile.yml vendored Normal file
View File

@ -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}}

View File

@ -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
}

View File

@ -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 {