From fa105a8a93d26c01dc80b51053973b861674c4d4 Mon Sep 17 00:00:00 2001 From: Pete Davison Date: Wed, 2 Nov 2022 14:38:26 +0000 Subject: [PATCH] refactor: implement task list filtering --- CHANGELOG.md | 2 + cmd/task/task.go | 8 +++- help.go | 58 +++-------------------- task.go | 76 +++++++++++++++++++++++++++++- task_test.go | 25 +++------- testdata/alias/alias-duplicate.txt | 1 - 6 files changed, 95 insertions(+), 75 deletions(-) delete mode 100644 testdata/alias/alias-duplicate.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c7b2e940..358dffe825 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Tasks in the root Taskfile will now be displayed first in `--list`/`--list-all` + output ([#806](https://github.com/go-task/task/pull/806), [#890](https://github.com/go-task/task/pull/890)). - It's now possible to call a `default` task in an included Taskfile by using just the namespace. For example: `docs:default` is now automatically aliased to `docs` diff --git a/cmd/task/task.go b/cmd/task/task.go index c7c25deea5..d69b6b1c2f 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -177,12 +177,16 @@ func main() { } if list { - e.ListTasksWithDesc() + 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 { - e.ListAllTasks() + if ok := e.ListTasks(task.FilterOutInternal()); !ok { + e.Logger.Outf(logger.Yellow, "task: No tasks available") + } return } diff --git a/help.go b/help.go index eed0a27cc7..cb9bad1999 100644 --- a/help.go +++ b/help.go @@ -10,34 +10,15 @@ import ( "text/tabwriter" "github.com/go-task/task/v3/internal/logger" - "github.com/go-task/task/v3/taskfile" ) -// ListTasksWithDesc reports tasks that have a description spec. -func (e *Executor) ListTasksWithDesc() { - e.printTasks(false) -} - -// ListAllTasks reports all tasks, with or without a description spec. -func (e *Executor) ListAllTasks() { - e.printTasks(true) -} - -func (e *Executor) printTasks(listAll bool) { - var tasks []*taskfile.Task - if listAll { - tasks = e.allTaskNames() - } else { - tasks = e.tasksWithDesc() - } - +// 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...) if len(tasks) == 0 { - if listAll { - e.Logger.Outf(logger.Yellow, "task: No tasks available") - } else { - e.Logger.Outf(logger.Yellow, "task: No tasks with description available. Try --list-all to list all tasks") - } - return + return false } e.Logger.Outf(logger.Default, "task: Available tasks for this project:") @@ -53,32 +34,7 @@ func (e *Executor) printTasks(listAll bool) { fmt.Fprint(w, "\n") } w.Flush() -} - -func (e *Executor) allTaskNames() (tasks []*taskfile.Task) { - tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks)) - for _, task := range e.Taskfile.Tasks { - if !task.Internal { - tasks = append(tasks, task) - } - } - sort.Slice(tasks, func(i, j int) bool { return tasks[i].Task < tasks[j].Task }) - return -} - -func (e *Executor) tasksWithDesc() (tasks []*taskfile.Task) { - tasks = make([]*taskfile.Task, 0, len(e.Taskfile.Tasks)) - for _, task := range e.Taskfile.Tasks { - if !task.Internal && task.Desc != "" { - compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task}) - if err == nil { - task = compiledTask - } - tasks = append(tasks, task) - } - } - sort.Slice(tasks, func(i, j int) bool { return tasks[i].Task < tasks[j].Task }) - return + return true } // ListTaskNames prints only the task names in a Taskfile. diff --git a/task.go b/task.go index a534caa016..65524181ee 100644 --- a/task.go +++ b/task.go @@ -5,6 +5,8 @@ import ( "fmt" "io" "os" + "sort" + "strings" "sync" "sync/atomic" @@ -70,12 +72,12 @@ func (e *Executor) Run(ctx context.Context, calls ...taskfile.Call) error { for _, call := range calls { task, err := e.GetTask(call) if err != nil { - e.ListTasksWithDesc() + e.ListTasks(FilterOutInternal(), FilterOutNoDesc()) return err } if task.Internal { - e.ListTasksWithDesc() + e.ListTasks(FilterOutInternal(), FilterOutNoDesc()) return &taskInternalError{taskName: call.Task} } } @@ -374,3 +376,73 @@ func (e *Executor) GetTask(call taskfile.Call) (*taskfile.Task, error) { return matchingTask, nil } + +type FilterFunc func(tasks []*taskfile.Task) []*taskfile.Task + +func (e *Executor) GetTaskList(filters ...FilterFunc) []*taskfile.Task { + tasks := make([]*taskfile.Task, 0, len(e.Taskfile.Tasks)) + + // Fetch and compile the list of tasks + for _, task := range e.Taskfile.Tasks { + compiledTask, err := e.FastCompiledTask(taskfile.Call{Task: task.Task}) + if err == nil { + task = compiledTask + } + tasks = append(tasks, task) + } + + // Filter the tasks + for _, filter := range filters { + tasks = filter(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 { + iContainsColon := strings.Contains(tasks[i].Task, ":") + jContainsColon := strings.Contains(tasks[j].Task, ":") + if iContainsColon == jContainsColon { + return tasks[i].Task < tasks[j].Task + } + if !iContainsColon && jContainsColon { + return true + } + return false + }) + + return tasks +} + +// Filter is a generic task filtering function. It will remove each task in the +// slice where the result of the given function is true. +func Filter(f func(task *taskfile.Task) bool) FilterFunc { + return func(tasks []*taskfile.Task) []*taskfile.Task { + shift := 0 + for _, task := range tasks { + if !f(task) { + tasks[shift] = task + shift++ + } + } + // This loop stops any memory leaks + for j := shift; j < len(tasks); j++ { + tasks[j] = nil + } + return slices.Clip(tasks[:shift]) + } +} + +// FilterOutNoDesc removes all tasks that do not contain a description. +func FilterOutNoDesc() FilterFunc { + return Filter(func(task *taskfile.Task) bool { + return task.Desc == "" + }) +} + +// FilterOutInternal removes all tasks that are marked as internal. +func FilterOutInternal() FilterFunc { + return Filter(func(task *taskfile.Task) bool { + return task.Internal + }) +} diff --git a/task_test.go b/task_test.go index 30d90a540a..e391a48ee1 100644 --- a/task_test.go +++ b/task_test.go @@ -497,9 +497,6 @@ func TestAlias(t *testing.T) { func TestDuplicateAlias(t *testing.T) { const dir = "testdata/alias" - data, err := os.ReadFile(filepathext.SmartJoin(dir, "alias-duplicate.txt")) - assert.NoError(t, err) - var buff bytes.Buffer e := task.Executor{ Dir: dir, @@ -508,7 +505,7 @@ func TestDuplicateAlias(t *testing.T) { } assert.NoError(t, e.Setup()) assert.Error(t, e.Run(context.Background(), taskfile.Call{Task: "x"})) - assert.Equal(t, string(data), buff.String()) + assert.Equal(t, "", buff.String()) } func TestAliasSummary(t *testing.T) { @@ -609,7 +606,7 @@ func TestNoLabelInList(t *testing.T) { Stderr: &buff, } assert.NoError(t, e.Setup()) - e.ListTasksWithDesc() + e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc()) assert.Contains(t, buff.String(), "foo") } @@ -627,7 +624,7 @@ func TestListAllShowsNoDesc(t *testing.T) { assert.NoError(t, e.Setup()) var title string - e.ListAllTasks() + e.ListTasks(task.FilterOutInternal()) for _, title = range []string{ "foo", "voo", @@ -649,7 +646,7 @@ func TestListCanListDescOnly(t *testing.T) { } assert.NoError(t, e.Setup()) - e.ListTasksWithDesc() + e.ListTasks(task.FilterOutInternal(), task.FilterOutNoDesc()) var title string assert.Contains(t, buff.String(), "foo") @@ -1037,12 +1034,7 @@ func TestIncludesInternal(t *testing.T) { }{ {"included internal task via task", "task-1", false, "Hello, World!\n"}, {"included internal task via dep", "task-2", false, "Hello, World!\n"}, - { - "included internal direct", - "included:task-3", - true, - "task: No tasks with description available. Try --list-all to list all tasks\n", - }, + {"included internal direct", "included:task-3", true, ""}, } for _, test := range tests { @@ -1077,12 +1069,7 @@ func TestInternalTask(t *testing.T) { }{ {"internal task via task", "task-1", false, "Hello, World!\n"}, {"internal task via dep", "task-2", false, "Hello, World!\n"}, - { - "internal direct", - "task-3", - true, - "task: No tasks with description available. Try --list-all to list all tasks\n", - }, + {"internal direct", "task-3", true, ""}, } for _, test := range tests { diff --git a/testdata/alias/alias-duplicate.txt b/testdata/alias/alias-duplicate.txt deleted file mode 100644 index 56e8128e82..0000000000 --- a/testdata/alias/alias-duplicate.txt +++ /dev/null @@ -1 +0,0 @@ -task: No tasks with description available. Try --list-all to list all tasks