From 321f7b59d8dc2e41a66e0c68d71caed53a88abe5 Mon Sep 17 00:00:00 2001 From: David Alpert Date: Sat, 17 Dec 2022 07:31:00 -0600 Subject: [PATCH] Add --json flag to be used by editor extensions (#936) --- cmd/task/task.go | 22 ++++---- help.go | 103 ++++++++++++++++++++++++++++++++++--- internal/editors/output.go | 14 +++++ task.go | 4 +- task_test.go | 12 +++-- 5 files changed, 131 insertions(+), 24 deletions(-) create mode 100644 internal/editors/output.go diff --git a/cmd/task/task.go b/cmd/task/task.go index d69b6b1c..842f8507 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,16 +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") + if listOptions.ShouldListTasks() { + if foundTasks, err := e.ListTasks(listOptions); !foundTasks || err != nil { + os.Exit(1) } return } diff --git a/help.go b/help.go index 57cbc80f..b9b57aee 100644 --- a/help.go +++ b/help.go @@ -1,7 +1,10 @@ package task import ( + "context" + "encoding/json" "fmt" + "github.com/go-task/task/v3/taskfile" "io" "log" "os" @@ -9,16 +12,81 @@ 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() 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() { + 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 +// 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 { + output, err := e.ToEditorOutput(tasks) + if err != nil { + return false, err + } + + encoder := json.NewEncoder(e.Stdout) + encoder.SetIndent("", " ") + if err := encoder.Encode(output); err != nil { + return false, err + } + + return len(tasks) > 0, nil + } if len(tasks) == 0 { - return false + 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, nil } e.Logger.Outf(logger.Default, "task: Available tasks for this project:") @@ -31,10 +99,12 @@ func (e *Executor) ListTasks(filters ...FilterFunc) 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") } - w.Flush() - return true + if err := w.Flush(); err != nil { + return false, err + } + return true, nil } // ListTaskNames prints only the task names in a Taskfile. @@ -69,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 new file mode 100644 index 00000000..18997304 --- /dev/null +++ b/internal/editors/output.go @@ -0,0 +1,14 @@ +package editors + +// 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"` + UpToDate bool `json:"up_to_date"` +} diff --git a/task.go b/task.go index 65524181..7c7822d5 100644 --- a/task.go +++ b/task.go @@ -72,12 +72,10 @@ 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 } if task.Internal { - e.ListTasks(FilterOutInternal(), FilterOutNoDesc()) return &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 7094423a..b6dd72a9 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.FilterOutInternal(), task.FilterOutNoDesc()) + 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.FilterOutInternal()) + 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.FilterOutInternal(), task.FilterOutNoDesc()) + if _, err := e.ListTasks(task.ListOptions{ListOnlyTasksWithDescriptions: true}); err != nil { + t.Error(err) + } var title string assert.Contains(t, buff.String(), "foo")