From 5fe752d48a2f50b45a6e7d09544fafcb36ffc57b Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Thu, 20 Aug 2026 15:12:47 +0200 Subject: [PATCH] refactor(completion): tighten the engine after a cleanup pass `--temp-dir` was missing from the flag-to-directive map, so it fell back to plain file completion while `--dir` and `--remote-cache-dir` offered directories. The rest is dead weight: `listTasks` re-defaulted a sorter `NewExecutor` already sets and scanned descriptions for templates even with `--no-descriptions`; `detectTaskName` had a `--` branch `Complete` returns before reaching; the two flag-value branches built the same suggestion slice twice. `os.Args[2:]` is now sliced in one place, `complete.Words()`, instead of three, and the test helpers reuse `slicesext.Convert`. --- cmd/task/task.go | 2 +- internal/complete/complete.go | 5 +++++ internal/complete/complete_test.go | 22 ++++++++++++---------- internal/complete/context.go | 3 --- internal/complete/engine.go | 28 +++++++++++----------------- internal/complete/flags.go | 1 + internal/flags/flags.go | 2 +- 7 files changed, 31 insertions(+), 32 deletions(-) diff --git a/cmd/task/task.go b/cmd/task/task.go index 23328451..7b1c19f6 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -62,7 +62,7 @@ func run() error { // Dispatched before flag validation: the args after __complete are the // user's command line, not Task's own flags. if complete.IsActive() { - return runComplete(os.Args[2:]) + return runComplete(complete.Words()) } log := &logger.Logger{ diff --git a/internal/complete/complete.go b/internal/complete/complete.go index 5acf14f4..29c8d5a5 100644 --- a/internal/complete/complete.go +++ b/internal/complete/complete.go @@ -10,6 +10,11 @@ func IsActive() bool { return len(os.Args) >= 2 && os.Args[1] == CommandName } +// Words returns the command line being completed: the args after __complete. +func Words() []string { + return os.Args[2:] +} + // Directive mirrors cobra's ShellCompDirective bitfield, emitted as `:`. type Directive int diff --git a/internal/complete/complete_test.go b/internal/complete/complete_test.go index e40b0748..222f059b 100644 --- a/internal/complete/complete_test.go +++ b/internal/complete/complete_test.go @@ -12,6 +12,7 @@ import ( "github.com/go-task/task/v3" "github.com/go-task/task/v3/internal/complete" + "github.com/go-task/task/v3/internal/slicesext" ) func newTestFlagSet() *pflag.FlagSet { @@ -23,6 +24,7 @@ func newTestFlagSet() *pflag.FlagSet { fs.BoolVarP(&b, "verbose", "v", false, "Verbose mode") fs.StringVarP(&s, "taskfile", "t", "", "Taskfile path") fs.StringVarP(&s, "dir", "d", "", "Run dir") + fs.StringVar(&s, "temp-dir", "", "Temp dir") fs.StringVarP(&s, "output", "o", "", "Output style") fs.StringVar(&s, "sort", "", "Sort order") fs.StringVar(&s, "cacert", "", "CA cert path") @@ -282,6 +284,14 @@ func TestComplete_PathFlag_Dir(t *testing.T) { require.Equal(t, complete.DirectiveFilterDirs, dir) } +func TestComplete_PathFlag_TempDir(t *testing.T) { + t.Parallel() + + suggs, dir := complete.Complete(setupExecutor(t), newTestFlagSet(), []string{"--temp-dir", ""}, complete.Options{}) + require.Empty(t, suggs) + require.Equal(t, complete.DirectiveFilterDirs, dir) +} + func TestComplete_PathFlag_Cacert(t *testing.T) { t.Parallel() @@ -435,17 +445,9 @@ func TestWrite_EmptyWithDirective(t *testing.T) { } func values(suggs []complete.Suggestion) []string { - out := make([]string, 0, len(suggs)) - for _, s := range suggs { - out = append(out, s.Value) - } - return out + return slicesext.Convert(suggs, func(s complete.Suggestion) string { return s.Value }) } func descriptions(suggs []complete.Suggestion) []string { - out := make([]string, 0, len(suggs)) - for _, s := range suggs { - out = append(out, s.Description) - } - return out + return slicesext.Convert(suggs, func(s complete.Suggestion) string { return s.Description }) } diff --git a/internal/complete/context.go b/internal/complete/context.go index b6738f71..5616fd1d 100644 --- a/internal/complete/context.go +++ b/internal/complete/context.go @@ -56,9 +56,6 @@ func detectTaskName(args []string, knownTasks []string, fs *pflag.FlagSet) strin skipNext = false continue } - if w == "--" { - return taskName - } if strings.HasPrefix(w, "-") { if !strings.Contains(w, "=") { if f := matchFlagName(fs, w); f != nil && flagTakesValue(f) { diff --git a/internal/complete/engine.go b/internal/complete/engine.go index 6c0b9b27..1994f8bc 100644 --- a/internal/complete/engine.go +++ b/internal/complete/engine.go @@ -8,7 +8,6 @@ import ( "github.com/go-task/task/v3" "github.com/go-task/task/v3/internal/refs" "github.com/go-task/task/v3/internal/slicesext" - "github.com/go-task/task/v3/internal/sort" "github.com/go-task/task/v3/taskfile/ast" ) @@ -123,22 +122,17 @@ func completeTaskNames(e *task.Executor, opts Options) ([]Suggestion, Directive) // GetTaskList compiles every task, on every keystroke, and a description is the // only compiled field read: worth its cost only when one holds a template. func listTasks(e *task.Executor, opts Options) []*ast.Task { - sorter := e.TaskSorter - if sorter == nil { - sorter = sort.AlphaNumericWithRootTasksFirst - } - out := make([]*ast.Task, 0, e.Taskfile.Tasks.Len()) templated := false - for t := range e.Taskfile.Tasks.Values(sorter) { + for t := range e.Taskfile.Tasks.Values(e.TaskSorter) { if t.Internal { continue } - templated = templated || strings.Contains(t.Desc, "{{") + templated = templated || (!opts.NoDescriptions && strings.Contains(t.Desc, "{{")) out = append(out, t) } - if !opts.NoDescriptions && templated { + if templated { // The uncompiled tasks keep one broken task from emptying the list. if compiled, err := e.GetTaskList(task.FilterOutInternal); err == nil { return compiled @@ -160,24 +154,24 @@ func completeFlagValue(flagName, prefix string) ([]Suggestion, Directive) { // An absent key yields DirectiveDefault, falling through to the enums. switch flagDirective[flagName] { case DirectiveFilterFileExt: - exts := slicesext.Convert(taskfileExtensions, func(ext string) Suggestion { - return Suggestion{Value: ext} - }) - return exts, DirectiveFilterFileExt + return suggest("", taskfileExtensions), DirectiveFilterFileExt case DirectiveFilterDirs: return nil, DirectiveFilterDirs } if values, ok := flagEnums[flagName]; ok { - out := slicesext.Convert(values, func(v string) Suggestion { - return Suggestion{Value: prefix + v} - }) - return out, DirectiveNoFileComp + return suggest(prefix, values), DirectiveNoFileComp } return nil, DirectiveDefault } +func suggest(prefix string, values []string) []Suggestion { + return slicesext.Convert(values, func(v string) Suggestion { + return Suggestion{Value: prefix + v} + }) +} + func completeTaskVars(e *task.Executor, taskName string) ([]Suggestion, Directive) { compiled, err := e.FastCompiledTask(&task.Call{Task: taskName}) if err != nil || compiled == nil || compiled.Requires == nil { diff --git a/internal/complete/flags.go b/internal/complete/flags.go index 888eeab5..ca70b3a9 100644 --- a/internal/complete/flags.go +++ b/internal/complete/flags.go @@ -23,6 +23,7 @@ var flagDirective = map[string]Directive{ "taskfile": DirectiveFilterFileExt, "dir": DirectiveFilterDirs, "remote-cache-dir": DirectiveFilterDirs, + "temp-dir": DirectiveFilterDirs, } var taskfileExtensions = []string{"yml", "yaml"} diff --git a/internal/flags/flags.go b/internal/flags/flags.go index 7f870480..04342004 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -181,7 +181,7 @@ func init() { // flags deciding which Taskfile is loaded must still reach the engine. // ContinueOnError keeps what was parsed and prints nothing. if complete.IsActive() { - _, words := complete.ParseOptions(os.Args[2:]) + _, words := complete.ParseOptions(complete.Words()) pflag.CommandLine.Init(pflag.CommandLine.Name(), pflag.ContinueOnError) pflag.CommandLine.ParseErrorsAllowlist.UnknownFlags = true _ = pflag.CommandLine.Parse(words)