diff --git a/cmd/task/task.go b/cmd/task/task.go index 70fe9f01..cb0f58f6 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "log" "github.com/go-task/task" @@ -9,6 +10,8 @@ import ( ) func main() { + log.SetFlags(0) + pflag.Usage = func() { fmt.Println(`task [target1 target2 ...]: Runs commands under targets like make. @@ -25,9 +28,40 @@ hello: `) pflag.PrintDefaults() } - pflag.BoolVarP(&task.Init, "init", "i", false, "creates a new Taskfile.yml in the current folder") - pflag.BoolVarP(&task.Force, "force", "f", false, "forces execution even when the task is up-to-date") - pflag.BoolVarP(&task.Watch, "watch", "w", false, "enables watch of the given task") + + var ( + init bool + force bool + watch bool + ) + + pflag.BoolVarP(&init, "init", "i", false, "creates a new Taskfile.yml in the current folder") + pflag.BoolVarP(&force, "force", "f", false, "forces execution even when the task is up-to-date") + pflag.BoolVarP(&watch, "watch", "w", false, "enables watch of the given task") pflag.Parse() - task.Run() + + if init { + if err := task.InitTaskfile(); err != nil { + log.Fatal(err) + } + return + } + + e := task.Executor{ + Force: force, + Watch: watch, + } + if err := e.ReadTaskfile(); err != nil { + log.Fatal(err) + } + + args := pflag.Args() + if len(args) == 0 { + log.Println("task: No argument given, trying default task") + args = []string{"default"} + } + + if err := e.Run(args...); err != nil { + log.Fatal(err) + } } diff --git a/cyclic.go b/cyclic.go index cbc30b50..236ef4f6 100644 --- a/cyclic.go +++ b/cyclic.go @@ -1,8 +1,8 @@ package task // HasCyclicDep checks if a task tree has any cyclic dependency -func HasCyclicDep(m map[string]*Task) bool { - visits := make(map[string]struct{}, len(m)) +func (e *Executor) HasCyclicDep() bool { + visits := make(map[string]struct{}, len(e.Tasks)) var checkCyclicDep func(string, *Task) bool checkCyclicDep = func(name string, t *Task) bool { @@ -13,14 +13,14 @@ func HasCyclicDep(m map[string]*Task) bool { defer delete(visits, name) for _, d := range t.Deps { - if !checkCyclicDep(d, m[d]) { + if !checkCyclicDep(d, e.Tasks[d]) { return false } } return true } - for k, v := range m { + for k, v := range e.Tasks { if !checkCyclicDep(k, v) { return true } diff --git a/cyclic_test.go b/cyclic_test.go index 08f8e087..8406afa2 100644 --- a/cyclic_test.go +++ b/cyclic_test.go @@ -7,30 +7,34 @@ import ( ) func TestCyclicDepCheck(t *testing.T) { - isCyclic := map[string]*task.Task{ - "task-a": &task.Task{ - Deps: []string{"task-b"}, - }, - "task-b": &task.Task{ - Deps: []string{"task-a"}, + isCyclic := &task.Executor{ + Tasks: map[string]*task.Task{ + "task-a": &task.Task{ + Deps: []string{"task-b"}, + }, + "task-b": &task.Task{ + Deps: []string{"task-a"}, + }, }, } - if !task.HasCyclicDep(isCyclic) { + if !isCyclic.HasCyclicDep() { t.Error("Task should be cyclic") } - isNotCyclic := map[string]*task.Task{ - "task-a": &task.Task{ - Deps: []string{"task-c"}, + isNotCyclic := &task.Executor{ + Tasks: map[string]*task.Task{ + "task-a": &task.Task{ + Deps: []string{"task-c"}, + }, + "task-b": &task.Task{ + Deps: []string{"task-c"}, + }, + "task-c": &task.Task{}, }, - "task-b": &task.Task{ - Deps: []string{"task-c"}, - }, - "task-c": &task.Task{}, } - if task.HasCyclicDep(isNotCyclic) { + if isNotCyclic.HasCyclicDep() { t.Error("Task should not be cyclic") } } diff --git a/errors.go b/errors.go index 11945dc4..82c701b0 100644 --- a/errors.go +++ b/errors.go @@ -1,9 +1,17 @@ package task import ( + "errors" "fmt" ) +var ( + // ErrCyclicDependencyDetected is returned when a cyclic dependency was found in the Taskfile + ErrCyclicDependencyDetected = errors.New("task: cyclic dependency detected") + // ErrTaskfileAlreadyExists is returned on creating a Taskfile if one already exists + ErrTaskfileAlreadyExists = errors.New("task: A Taskfile already exists") +) + type taskFileNotFound struct { taskFile string } diff --git a/help.go b/help.go index 3f085dbc..2673c294 100644 --- a/help.go +++ b/help.go @@ -7,8 +7,8 @@ import ( "text/tabwriter" ) -func printExistingTasksHelp() { - tasks := tasksWithDesc() +func (e *Executor) printExistingTasksHelp() { + tasks := e.tasksWithDesc() if len(tasks) == 0 { return } @@ -17,13 +17,13 @@ func printExistingTasksHelp() { // Format in tab-separated columns with a tab stop of 8. w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0) for _, task := range tasks { - fmt.Fprintln(w, fmt.Sprintf("- %s:\t%s", task, Tasks[task].Desc)) + fmt.Fprintln(w, fmt.Sprintf("- %s:\t%s", task, e.Tasks[task].Desc)) } w.Flush() } -func tasksWithDesc() (tasks []string) { - for name, task := range Tasks { +func (e *Executor) tasksWithDesc() (tasks []string) { + for name, task := range e.Tasks { if task.Desc != "" { tasks = append(tasks, name) } diff --git a/init.go b/init.go index 2d015e4e..21675b99 100644 --- a/init.go +++ b/init.go @@ -13,17 +13,17 @@ default: - echo "Hello, World!" ` -func initTaskfile() { +// InitTaskfile Taskfile creates a new Taskfile +func InitTaskfile() error { for _, f := range []string{"Taskfile.yml", "Taskfile.toml", "Taskfile.json"} { if _, err := os.Stat(f); err == nil { - log.Printf("A Taskfile already exists") - os.Exit(1) - return + return ErrTaskfileAlreadyExists } } if err := ioutil.WriteFile("Taskfile.yml", []byte(defaultTaskfile), 0666); err != nil { - log.Fatalf("%v", err) + return err } log.Printf("Taskfile.yml created in the current directory") + return nil } diff --git a/read_taskfile.go b/read_taskfile.go index 5d063b08..f3328ac4 100644 --- a/read_taskfile.go +++ b/read_taskfile.go @@ -11,27 +11,30 @@ import ( "gopkg.in/yaml.v2" ) -func readTaskfile() (map[string]*Task, error) { - initialTasks, err := readTaskfileData(TaskFilePath) +// ReadTaskfile parses Taskfile from the disk +func (e *Executor) ReadTaskfile() error { + var err error + e.Tasks, err = e.readTaskfileData(TaskFilePath) if err != nil { - return nil, err + return err } - mergeTasks, err := readTaskfileData(fmt.Sprintf("%s_%s", TaskFilePath, runtime.GOOS)) + + osTasks, err := e.readTaskfileData(fmt.Sprintf("%s_%s", TaskFilePath, runtime.GOOS)) if err != nil { switch err.(type) { - default: - return nil, err case taskFileNotFound: - return initialTasks, nil + return nil + default: + return err } } - if err := mergo.MapWithOverwrite(&initialTasks, mergeTasks); err != nil { - return nil, err + if err := mergo.MapWithOverwrite(&e.Tasks, osTasks); err != nil { + return err } - return initialTasks, nil + return nil } -func readTaskfileData(path string) (tasks map[string]*Task, err error) { +func (e *Executor) readTaskfileData(path string) (tasks map[string]*Task, err error) { if b, err := ioutil.ReadFile(path + ".yml"); err == nil { return tasks, yaml.Unmarshal(b, &tasks) } diff --git a/task.go b/task.go index 12cbadec..4e171517 100644 --- a/task.go +++ b/task.go @@ -10,24 +10,22 @@ import ( "github.com/go-task/task/execext" - "github.com/spf13/pflag" "golang.org/x/sync/errgroup" ) -var ( +const ( // TaskFilePath is the default Taskfile TaskFilePath = "Taskfile" +) - // Init (--init or -f flag) creates a Taskfile.yml in the current folder if not exists - Init bool - // Force (--force or -f flag) forces a task to run even when it's up-to-date +// Executor executes a Taskfile +type Executor struct { + Tasks map[string]*Task Force bool - // Watch (--watch or -w flag) enables watch of a task Watch bool - // Tasks constains the tasks parsed from Taskfile - Tasks = make(map[string]*Task) -) + watchingFiles map[string]struct{} +} // Task represents a task type Task struct { @@ -44,67 +42,47 @@ type Task struct { } // Run runs Task -func Run() { - log.SetFlags(0) - - args := pflag.Args() - - if Init { - initTaskfile() - return - } - - if len(args) == 0 { - log.Println("task: No argument given, trying default task") - args = []string{"default"} - } - - var err error - Tasks, err = readTaskfile() - if err != nil { - log.Fatal(err) - } - - if HasCyclicDep(Tasks) { - log.Fatal("Cyclic dependency detected") +func (e *Executor) Run(args ...string) error { + if e.HasCyclicDep() { + return ErrCyclicDependencyDetected } // check if given tasks exist for _, a := range args { - if _, ok := Tasks[a]; !ok { - var err error = &taskNotFoundError{taskName: a} - fmt.Println(err) - printExistingTasksHelp() - return + if _, ok := e.Tasks[a]; !ok { + // FIXME: move to the main package + e.printExistingTasksHelp() + return &taskNotFoundError{taskName: a} } } - if Watch { - if err := WatchTasks(args); err != nil { - log.Fatal(err) + if e.Watch { + if err := e.watchTasks(args...); err != nil { + return err } - return + return nil } for _, a := range args { - if err = RunTask(context.Background(), a); err != nil { - log.Fatal(err) + if err := e.RunTask(context.Background(), a); err != nil { + return err } } + return nil } // RunTask runs a task by its name -func RunTask(ctx context.Context, name string) error { - t, ok := Tasks[name] +func (e *Executor) RunTask(ctx context.Context, name string) error { + t, ok := e.Tasks[name] if !ok { return &taskNotFoundError{name} } - if err := t.runDeps(ctx); err != nil { + if err := e.runDeps(ctx, name); err != nil { return err } - if !Force { + if !e.Force { upToDate, err := t.isUpToDate(ctx) if err != nil { return err @@ -116,15 +94,16 @@ func RunTask(ctx context.Context, name string) error { } for i := range t.Cmds { - if err := t.runCommand(ctx, i); err != nil { + if err := e.runCommand(ctx, name, i); err != nil { return &taskRunError{name, err} } } return nil } -func (t *Task) runDeps(ctx context.Context) error { +func (e *Executor) runDeps(ctx context.Context, task string) error { g, ctx := errgroup.WithContext(ctx) + t := e.Tasks[task] for _, d := range t.Deps { dep := d @@ -135,7 +114,7 @@ func (t *Task) runDeps(ctx context.Context) error { return err } - if err = RunTask(ctx, dep); err != nil { + if err = e.RunTask(ctx, dep); err != nil { return err } return nil @@ -195,7 +174,9 @@ func (t *Task) isUpToDate(ctx context.Context) (bool, error) { return generatesMinTime.After(sourcesMaxTime), nil } -func (t *Task) runCommand(ctx context.Context, i int) error { +func (e *Executor) runCommand(ctx context.Context, task string, i int) error { + t := e.Tasks[task] + c, err := t.ReplaceVariables(t.Cmds[i]) if err != nil { return err @@ -203,7 +184,7 @@ func (t *Task) runCommand(ctx context.Context, i int) error { if strings.HasPrefix(c, "^") { c = strings.TrimPrefix(c, "^") - if err = RunTask(ctx, c); err != nil { + if err = e.RunTask(ctx, c); err != nil { return err } return nil diff --git a/watch.go b/watch.go index 0e9d5181..49f2127a 100644 --- a/watch.go +++ b/watch.go @@ -11,13 +11,13 @@ import ( "github.com/mattn/go-zglob" ) -// WatchTasks start watching the given tasks -func WatchTasks(args []string) error { +// watchTasks start watching the given tasks +func (e *Executor) watchTasks(args ...string) error { log.Printf("task: Started watching for tasks: %s", strings.Join(args, ", ")) // run tasks on init for _, a := range args { - if err := RunTask(context.Background(), a); err != nil { + if err := e.RunTask(context.Background(), a); err != nil { fmt.Println(err) break } @@ -31,7 +31,7 @@ func WatchTasks(args []string) error { go func() { for { - if err := registerWatchedFiles(watcher, args); err != nil { + if err := e.registerWatchedFiles(watcher, args); err != nil { log.Printf("Error watching files: %v", err) } time.Sleep(time.Second * 2) @@ -43,7 +43,7 @@ loop: select { case <-watcher.Events: for _, a := range args { - if err := RunTask(context.Background(), a); err != nil { + if err := e.RunTask(context.Background(), a); err != nil { fmt.Println(err) continue loop } @@ -55,11 +55,9 @@ loop: } } -var watchingFiles map[string]struct{} - -func registerWatchedFiles(w *fsnotify.Watcher, args []string) error { - oldWatchingFiles := watchingFiles - watchingFiles = make(map[string]struct{}, len(oldWatchingFiles)) +func (e *Executor) registerWatchedFiles(w *fsnotify.Watcher, args []string) error { + oldWatchingFiles := e.watchingFiles + e.watchingFiles = make(map[string]struct{}, len(oldWatchingFiles)) for k := range oldWatchingFiles { if err := w.Remove(k); err != nil { @@ -68,11 +66,11 @@ func registerWatchedFiles(w *fsnotify.Watcher, args []string) error { } for _, a := range args { - task, ok := Tasks[a] + task, ok := e.Tasks[a] if !ok { return &taskNotFoundError{a} } - if err := registerWatchedFiles(w, task.Deps); err != nil { + if err := e.registerWatchedFiles(w, task.Deps); err != nil { return err } for _, s := range task.Sources { @@ -84,7 +82,7 @@ func registerWatchedFiles(w *fsnotify.Watcher, args []string) error { if err := w.Add(f); err != nil { return err } - watchingFiles[f] = struct{}{} + e.watchingFiles[f] = struct{}{} // run if is new file if oldWatchingFiles != nil {