From 5b319c0cdcb5088f4bfc3c9b7346da38b167a491 Mon Sep 17 00:00:00 2001 From: David Alpert Date: Thu, 17 Nov 2022 07:46:56 -0600 Subject: [PATCH 1/3] feat: add --json flag to be used by editor extensions a new --json flag works only with the --list and --list-all flags to format those task lists as JSON output. --- cmd/task/task.go | 24 +++++++-------- help.go | 61 ++++++++++++++++++++++++++++++++++++-- internal/editors/output.go | 38 ++++++++++++++++++++++++ task.go | 8 ++--- task_test.go | 6 ++-- 5 files changed, 113 insertions(+), 24 deletions(-) create mode 100644 internal/editors/output.go diff --git a/cmd/task/task.go b/cmd/task/task.go index d69b6b1c2f..fe26430f91 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -59,6 +59,7 @@ func main() { init bool list bool listAll bool + listJson bool status bool force bool watch bool @@ -81,6 +82,7 @@ func main() { pflag.BoolVarP(&init, "init", "i", false, "creates a new Taskfile.yaml in the current folder") pflag.BoolVarP(&list, "list", "l", false, "lists tasks with description of current Taskfile") pflag.BoolVarP(&listAll, "list-all", "a", false, "lists tasks with or without a description") + pflag.BoolVarP(&listJson, "json", "j", false, "formats task list as json") pflag.BoolVar(&status, "status", false, "exits with non-zero exit code if any of the given tasks is not up-to-date") 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") @@ -162,7 +164,12 @@ func main() { OutputStyle: output, } - if (list || listAll) && silent { + var listOptions = task.NewListOptions(list, listAll, listJson) + if err := listOptions.Validate(); err != nil { + log.Fatal(err) + } + + if (listOptions.ShouldListTasks()) && silent { e.ListTaskNames(listAll) return } @@ -176,18 +183,9 @@ func main() { return } - if list { - if ok := e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc()); !ok { - e.Logger.Outf(logger.Yellow, "task: No tasks with description available. Try --list-all to list all tasks") - } - return - } - - if listAll { - if ok := e.ListTasks(task.FilterOutInternal()); !ok { - e.Logger.Outf(logger.Yellow, "task: No tasks available") - } - return + if listOptions.ShouldListTasks() { + _ = e.ListTasks(listOptions) + return // if we are listing tasks, we don't expect a task to run } var ( diff --git a/help.go b/help.go index 57cbc80fb0..9c93fac865 100644 --- a/help.go +++ b/help.go @@ -1,6 +1,7 @@ package task import ( + "encoding/json" "fmt" "io" "log" @@ -9,15 +10,69 @@ import ( "strings" "text/tabwriter" + "github.com/go-task/task/v3/internal/editors" "github.com/go-task/task/v3/internal/logger" ) +// ListOptions collects list-related options +type ListOptions struct { + ListOnlyTasksWithDescriptions bool + ListAllTasks bool + FormatTaskListAsJSON bool +} + +// NewListOptions creates a new ListOptions instance +func NewListOptions(list, listAll, listAsJson bool) ListOptions { + return ListOptions{ + ListOnlyTasksWithDescriptions: list, + ListAllTasks: listAll, + FormatTaskListAsJSON: listAsJson, + } +} + +// ShouldListTasks returns true if one of the options to list tasks has been set to true +func (o ListOptions) ShouldListTasks() bool { + return o.ListOnlyTasksWithDescriptions || o.ListAllTasks +} + +// Validate validates that the collection of list-related options are in a valid configuration +func (o ListOptions) Validate() interface{} { + if o.ListOnlyTasksWithDescriptions && o.ListAllTasks { + return fmt.Errorf("task: cannot use --list and --list-all at the same time") + } + if o.FormatTaskListAsJSON && !(o.ShouldListTasks()) { + return fmt.Errorf("task: --json only applies to --list or --list-all") + } + return nil +} + +// Filters returns the slice of FilterFunc which filters a list +// of taskfile.Task according to the given ListOptions +func (o ListOptions) Filters() []FilterFunc { + filters := []FilterFunc{FilterOutInternal()} + + if o.ListOnlyTasksWithDescriptions { + filters = append(filters, FilterOutNoDesc()) + } + + return filters +} + // ListTasks prints a list of tasks. // Tasks that match the given filters will be excluded from the list. -// The function returns a boolean indicating whether or not tasks were found. -func (e *Executor) ListTasks(filters ...FilterFunc) bool { - tasks := e.GetTaskList(filters...) +// The function returns a boolean indicating whether tasks were found. +func (e *Executor) ListTasks(o ListOptions) bool { + tasks := e.GetTaskList(o.Filters()...) + if o.FormatTaskListAsJSON { + _ = json.NewEncoder(os.Stdout).Encode(editors.ToOutput(tasks)) + return len(tasks) > 0 + } if len(tasks) == 0 { + if o.ListOnlyTasksWithDescriptions { + e.Logger.Outf(logger.Yellow, "task: No tasks with description available. Try --list-all to list all tasks") + } else if o.ListAllTasks { + e.Logger.Outf(logger.Yellow, "task: No tasks available") + } return false } e.Logger.Outf(logger.Default, "task: Available tasks for this project:") diff --git a/internal/editors/output.go b/internal/editors/output.go new file mode 100644 index 0000000000..59003799d2 --- /dev/null +++ b/internal/editors/output.go @@ -0,0 +1,38 @@ +package editors + +import "github.com/go-task/task/v3/taskfile" + +// Output wraps task list output for use in editor integrations (e.g. VSCode, etc) +type Output struct { + Tasks []Task `json:"tasks"` +} + +// Task describes a single task +type Task struct { + Name string `json:"name"` + Desc string `json:"desc"` + Summary string `json:"summary"` + + // "up-to-date" vs. "out-of-date"? Alternatively could be a "UpToDate bool" + //Status string `json:"status"` + UpToDate bool `json:"up_to_date"` + + //// These could be added on the future. Don't need to be on the MVP + //Env map[string]string `json:"env"` + //Vars map[string]string `json:"vars"` +} + +func ToOutput(tasks []*taskfile.Task) *Output { + o := &Output{ + Tasks: make([]Task, len(tasks)), + } + for i, t := range tasks { + o.Tasks[i] = Task{ + Name: t.Name(), + Desc: t.Desc, + Summary: t.Summary, + UpToDate: false, // TODO + } + } + return o +} diff --git a/task.go b/task.go index 65524181ee..e74c4d7525 100644 --- a/task.go +++ b/task.go @@ -72,13 +72,11 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error { for _, call := range calls { task, err := e.GetTask(call) if err != nil { - e.ListTasks(FilterOutInternal(), FilterOutNoDesc()) - return err + return fmt.Errorf("task: executor.Run(): executor.GetTask(): %v", err) } if task.Internal { - e.ListTasks(FilterOutInternal(), FilterOutNoDesc()) - return &taskInternalError{taskName: call.Task} + return fmt.Errorf("task: executor.Run(): %v", &taskInternalError{taskName: call.Task}) } } @@ -396,7 +394,7 @@ func (e *Executor) GetTaskList(filters ...FilterFunc) []*taskfile.Task { tasks = filter(tasks) } - // Sort the tasks + // Sort the tasks. // Tasks that are not namespaced should be listed before tasks that are. // We detect this by searching for a ':' in the task name. sort.Slice(tasks, func(i, j int) bool { diff --git a/task_test.go b/task_test.go index e391a48ee1..bcad937fca 100644 --- a/task_test.go +++ b/task_test.go @@ -606,7 +606,7 @@ func TestNoLabelInList(t *testing.T) { Stderr: &buff, } assert.NoError(t, e.Setup()) - e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc()) + e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}) assert.Contains(t, buff.String(), "foo") } @@ -624,7 +624,7 @@ func TestListAllShowsNoDesc(t *testing.T) { assert.NoError(t, e.Setup()) var title string - e.ListTasks(task.FilterOutInternal()) + e.ListTasks(task.ListOptions{ListAllTasks: true}) for _, title = range []string{ "foo", "voo", @@ -646,7 +646,7 @@ func TestListCanListDescOnly(t *testing.T) { } assert.NoError(t, e.Setup()) - e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc()) + e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}) var title string assert.Contains(t, buff.String(), "foo") From 7ef441f496f4e865c77243d4075eb79504b0713c Mon Sep 17 00:00:00 2001 From: David Alpert Date: Sat, 3 Dec 2022 17:52:01 -0500 Subject: [PATCH 2/3] accept code review suggestion Co-authored-by: Andrey Nering --- cmd/task/task.go | 6 ++++-- help.go | 28 ++++++++++++++++++---------- internal/editors/output.go | 15 ++++----------- task.go | 4 ++-- task_test.go | 12 +++++++++--- 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/cmd/task/task.go b/cmd/task/task.go index fe26430f91..842f85074b 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -184,8 +184,10 @@ func main() { } if listOptions.ShouldListTasks() { - _ = e.ListTasks(listOptions) - return // if we are listing tasks, we don't expect a task to run + if foundTasks, err := e.ListTasks(listOptions); !foundTasks || err != nil { + os.Exit(1) + } + return } var ( diff --git a/help.go b/help.go index 9c93fac865..131a237412 100644 --- a/help.go +++ b/help.go @@ -36,11 +36,11 @@ func (o ListOptions) ShouldListTasks() bool { } // Validate validates that the collection of list-related options are in a valid configuration -func (o ListOptions) Validate() interface{} { +func (o ListOptions) Validate() error { if o.ListOnlyTasksWithDescriptions && o.ListAllTasks { return fmt.Errorf("task: cannot use --list and --list-all at the same time") } - if o.FormatTaskListAsJSON && !(o.ShouldListTasks()) { + if o.FormatTaskListAsJSON && !o.ShouldListTasks() { return fmt.Errorf("task: --json only applies to --list or --list-all") } return nil @@ -60,12 +60,18 @@ func (o ListOptions) Filters() []FilterFunc { // ListTasks prints a list of tasks. // Tasks that match the given filters will be excluded from the list. -// The function returns a boolean indicating whether tasks were found. -func (e *Executor) ListTasks(o ListOptions) bool { +// The function returns a boolean indicating whether tasks were found +// and an error if one was encountered while preparing the output. +func (e *Executor) ListTasks(o ListOptions) (bool, error) { tasks := e.GetTaskList(o.Filters()...) if o.FormatTaskListAsJSON { - _ = json.NewEncoder(os.Stdout).Encode(editors.ToOutput(tasks)) - return len(tasks) > 0 + encoder := json.NewEncoder(e.Stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(editors.ToOutput(tasks)); err != nil { + return false, err + } + + return len(tasks) > 0, nil } if len(tasks) == 0 { if o.ListOnlyTasksWithDescriptions { @@ -73,7 +79,7 @@ func (e *Executor) ListTasks(o ListOptions) bool { } else if o.ListAllTasks { e.Logger.Outf(logger.Yellow, "task: No tasks available") } - return false + return false, nil } e.Logger.Outf(logger.Default, "task: Available tasks for this project:") @@ -86,10 +92,12 @@ func (e *Executor) ListTasks(o ListOptions) bool { if len(task.Aliases) > 0 { e.Logger.FOutf(w, logger.Cyan, "\t(aliases: %s)", strings.Join(task.Aliases, ", ")) } - fmt.Fprint(w, "\n") + _, _ = fmt.Fprint(w, "\n") + } + if err := w.Flush(); err != nil { + return false, err } - w.Flush() - return true + return true, nil } // ListTaskNames prints only the task names in a Taskfile. diff --git a/internal/editors/output.go b/internal/editors/output.go index 59003799d2..46faabcec0 100644 --- a/internal/editors/output.go +++ b/internal/editors/output.go @@ -9,17 +9,10 @@ type Output struct { // Task describes a single task type Task struct { - Name string `json:"name"` - Desc string `json:"desc"` - Summary string `json:"summary"` - - // "up-to-date" vs. "out-of-date"? Alternatively could be a "UpToDate bool" - //Status string `json:"status"` - UpToDate bool `json:"up_to_date"` - - //// These could be added on the future. Don't need to be on the MVP - //Env map[string]string `json:"env"` - //Vars map[string]string `json:"vars"` + Name string `json:"name"` + Desc string `json:"desc"` + Summary string `json:"summary"` + UpToDate bool `json:"up_to_date"` } func ToOutput(tasks []*taskfile.Task) *Output { diff --git a/task.go b/task.go index e74c4d7525..7c7822d5a5 100644 --- a/task.go +++ b/task.go @@ -72,11 +72,11 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error { for _, call := range calls { task, err := e.GetTask(call) if err != nil { - return fmt.Errorf("task: executor.Run(): executor.GetTask(): %v", err) + return err } if task.Internal { - return fmt.Errorf("task: executor.Run(): %v", &taskInternalError{taskName: call.Task}) + return &taskInternalError{taskName: call.Task} } } diff --git a/task_test.go b/task_test.go index bcad937fca..a54c705c38 100644 --- a/task_test.go +++ b/task_test.go @@ -606,7 +606,9 @@ func TestNoLabelInList(t *testing.T) { Stderr: &buff, } assert.NoError(t, e.Setup()) - e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}) + if _, err := e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil { + t.Error(err) + } assert.Contains(t, buff.String(), "foo") } @@ -624,7 +626,9 @@ func TestListAllShowsNoDesc(t *testing.T) { assert.NoError(t, e.Setup()) var title string - e.ListTasks(task.ListOptions{ListAllTasks: true}) + if _, err := e.ListTasks(task.ListOptions{ListAllTasks: true}); err != nil { + t.Error(err) + } for _, title = range []string{ "foo", "voo", @@ -646,7 +650,9 @@ func TestListCanListDescOnly(t *testing.T) { } assert.NoError(t, e.Setup()) - e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}) + if _, err := e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil { + t.Error(err) + } var title string assert.Contains(t, buff.String(), "foo") From 3c0ae65128fff2334ab5689c85c2da3608ae9909 Mon Sep 17 00:00:00 2001 From: David Alpert Date: Sat, 3 Dec 2022 17:21:53 -0600 Subject: [PATCH 3/3] implement upToDate moved the editors.ToOutput function into the task package so that it would have access to the Executor and be able to look up task status --- help.go | 28 +++++++++++++++++++++++++++- internal/editors/output.go | 17 ----------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/help.go b/help.go index 131a237412..b9b57aee85 100644 --- a/help.go +++ b/help.go @@ -1,8 +1,10 @@ package task import ( + "context" "encoding/json" "fmt" + "github.com/go-task/task/v3/taskfile" "io" "log" "os" @@ -65,9 +67,14 @@ func (o ListOptions) Filters() []FilterFunc { func (e *Executor) ListTasks(o ListOptions) (bool, error) { tasks := e.GetTaskList(o.Filters()...) if o.FormatTaskListAsJSON { + output, err := e.ToEditorOutput(tasks) + if err != nil { + return false, err + } + encoder := json.NewEncoder(e.Stdout) encoder.SetIndent("", " ") - if err := encoder.Encode(editors.ToOutput(tasks)); err != nil { + if err := encoder.Encode(output); err != nil { return false, err } @@ -132,3 +139,22 @@ func (e *Executor) ListTaskNames(allTasks bool) { fmt.Fprintln(w, t) } } + +func (e *Executor) ToEditorOutput(tasks []*taskfile.Task) (*editors.Output, error) { + o := &editors.Output{ + Tasks: make([]editors.Task, len(tasks)), + } + for i, t := range tasks { + upToDate, err := e.isTaskUpToDate(context.Background(), t) + if err != nil { + return nil, err + } + o.Tasks[i] = editors.Task{ + Name: t.Name(), + Desc: t.Desc, + Summary: t.Summary, + UpToDate: upToDate, + } + } + return o, nil +} diff --git a/internal/editors/output.go b/internal/editors/output.go index 46faabcec0..18997304c1 100644 --- a/internal/editors/output.go +++ b/internal/editors/output.go @@ -1,7 +1,5 @@ package editors -import "github.com/go-task/task/v3/taskfile" - // Output wraps task list output for use in editor integrations (e.g. VSCode, etc) type Output struct { Tasks []Task `json:"tasks"` @@ -14,18 +12,3 @@ type Task struct { Summary string `json:"summary"` UpToDate bool `json:"up_to_date"` } - -func ToOutput(tasks []*taskfile.Task) *Output { - o := &Output{ - Tasks: make([]Task, len(tasks)), - } - for i, t := range tasks { - o.Tasks[i] = Task{ - Name: t.Name(), - Desc: t.Desc, - Summary: t.Summary, - UpToDate: false, // TODO - } - } - return o -}