Files
task/internal/refs/refs.go
Valentin Maerten ed7a206e19 feat(completion): unify shell completions behind an opt-in task __complete engine
Bash, Fish, Zsh, Nushell and PowerShell now share a single backend: `task __complete` returns the suggestions plus a directive, and every wrapper is a thin shim around it. All five shells offer the same suggestions — task names, aliases, flags, flag values and per-task CLI variables. The Zsh `show-aliases` and `verbose` zstyles keep working, now backed by the `--no-aliases` and `--no-descriptions` completion flags.

The engine is opt-in via `task --new-completion <shell>`, leaving `--completion` and the legacy scripts untouched; it will become the default in a future release. The new wrappers live under `completion/next/`.

Completing a keystroke never reaches the network, never blocks on a stdin entrypoint, and honors every flag that decides how the Taskfile is loaded. Ref resolution shared by `requires` and enum completion moved to `internal/refs`.

A cross-shell test suite exercises the protocol in Go with thin shell smoke tests, and runs in CI.
2026-08-20 14:18:54 +02:00

65 lines
1.7 KiB
Go

// Package refs resolves the `ref` fields of a Taskfile into concrete values.
package refs
import (
"fmt"
"github.com/go-task/task/v3/internal/slicesext"
"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile/ast"
)
// Declared lists resolve to a []any, but `keys` and `splitList` return a []string.
func AsList(v any) ([]any, bool) {
switch value := v.(type) {
case []any:
return value, true
case []string:
return slicesext.AsAny(value), true
case []int:
return slicesext.AsAny(value), true
}
return nil, false
}
func ResolveEnums(requires *ast.Requires, cache *templater.Cache) error {
if requires == nil || len(requires.Vars) == 0 {
return nil
}
for _, v := range requires.Vars {
if v.Enum == nil || v.Enum.Ref == "" {
continue
}
resolved := templater.ResolveRef(v.Enum.Ref, cache)
if cache.Err() != nil {
return cache.Err()
}
arr, ok := AsList(resolved)
if !ok {
return fmt.Errorf("enum reference %q must resolve to a list", v.Enum.Ref)
}
strValues := make([]string, 0, len(arr))
for _, item := range arr {
s, ok := item.(string)
if !ok {
return fmt.Errorf("enum reference %q must contain only strings", v.Enum.Ref)
}
strValues = append(strValues, s)
}
v.Enum.Value = strValues
}
return nil
}
// A ref depending on dynamic vars may not resolve: the copy then has no enum
// value, which the interactive prompter treats as free-form input.
func ResolveEnum(v *ast.VarsWithValidation, vars *ast.Vars) *ast.VarsWithValidation {
if v.Enum == nil || v.Enum.Ref == "" || len(v.Enum.Value) > 0 {
return v
}
vCopy := v.DeepCopy()
cache := &templater.Cache{Vars: vars}
_ = ResolveEnums(&ast.Requires{Vars: []*ast.VarsWithValidation{vCopy}}, cache)
return vCopy
}