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`.
This commit is contained in:
Valentin Maerten
2026-08-20 15:12:47 +02:00
parent b00d2bafb6
commit 5fe752d48a
7 changed files with 31 additions and 32 deletions

View File

@@ -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{

View File

@@ -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 `:<n>`.
type Directive int

View File

@@ -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 })
}

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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"}

View File

@@ -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)