mirror of
https://github.com/go-task/task.git
synced 2026-07-01 16:44:34 +00:00
feat(completion): unify shell wrappers behind task __complete
This commit is contained in:
30
internal/complete/complete.go
Normal file
30
internal/complete/complete.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Package complete implements the `task __complete` protocol consumed by the
|
||||
// shell completion wrappers. The protocol mirrors cobra v2 so a future
|
||||
// migration stays cheap.
|
||||
package complete
|
||||
|
||||
import "os"
|
||||
|
||||
const CommandName = "__complete"
|
||||
|
||||
func IsActive() bool {
|
||||
return len(os.Args) >= 2 && os.Args[1] == CommandName
|
||||
}
|
||||
|
||||
// Directive mirrors cobra's ShellCompDirective bitfield.
|
||||
type Directive int
|
||||
|
||||
const (
|
||||
DirectiveDefault Directive = 0
|
||||
DirectiveError Directive = 1 << 0
|
||||
DirectiveNoSpace Directive = 1 << 1
|
||||
DirectiveNoFileComp Directive = 1 << 2
|
||||
DirectiveFilterFileExt Directive = 1 << 3
|
||||
DirectiveFilterDirs Directive = 1 << 4
|
||||
DirectiveKeepOrder Directive = 1 << 5
|
||||
)
|
||||
|
||||
type Suggestion struct {
|
||||
Value string
|
||||
Description string
|
||||
}
|
||||
279
internal/complete/complete_test.go
Normal file
279
internal/complete/complete_test.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package complete_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/internal/complete"
|
||||
)
|
||||
|
||||
func newTestFlagSet() *pflag.FlagSet {
|
||||
fs := pflag.NewFlagSet("test", pflag.ContinueOnError)
|
||||
var b bool
|
||||
var s string
|
||||
fs.BoolVarP(&b, "list-all", "a", false, "Lists all tasks")
|
||||
fs.BoolVarP(&b, "list", "l", false, "Lists tasks with descriptions")
|
||||
fs.BoolVarP(&b, "verbose", "v", false, "Verbose mode")
|
||||
fs.StringVarP(&s, "taskfile", "t", "", "Taskfile path")
|
||||
fs.StringVarP(&s, "dir", "d", "", "Run dir")
|
||||
fs.StringVarP(&s, "output", "o", "", "Output style")
|
||||
fs.StringVar(&s, "sort", "", "Sort order")
|
||||
fs.StringVar(&s, "cacert", "", "CA cert path")
|
||||
return fs
|
||||
}
|
||||
|
||||
const testTaskfile = `version: '3'
|
||||
|
||||
vars:
|
||||
ALLOWED_ENVS:
|
||||
- dev
|
||||
- staging
|
||||
- prod
|
||||
|
||||
tasks:
|
||||
deploy:
|
||||
desc: Deploy the application
|
||||
aliases: [dep, ship]
|
||||
requires:
|
||||
vars:
|
||||
- name: ENV
|
||||
enum:
|
||||
- dev
|
||||
- staging
|
||||
- prod
|
||||
- REGION
|
||||
cmds:
|
||||
- 'echo {{.ENV}} {{.REGION}}'
|
||||
|
||||
build:
|
||||
desc: Build it
|
||||
cmds:
|
||||
- 'echo build'
|
||||
|
||||
dynenum:
|
||||
desc: Dynamic enum
|
||||
requires:
|
||||
vars:
|
||||
- name: ENV
|
||||
enum:
|
||||
ref: .ALLOWED_ENVS
|
||||
cmds:
|
||||
- 'echo {{.ENV}}'
|
||||
|
||||
docs:serve:
|
||||
desc: Serve docs locally
|
||||
cmds:
|
||||
- 'echo serving'
|
||||
`
|
||||
|
||||
func setupExecutor(t *testing.T) *task.Executor {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "Taskfile.yml"), []byte(testTaskfile), 0o644))
|
||||
|
||||
e := task.NewExecutor(
|
||||
task.WithDir(dir),
|
||||
task.WithStdout(io.Discard),
|
||||
task.WithStderr(io.Discard),
|
||||
task.WithVersionCheck(false),
|
||||
)
|
||||
require.NoError(t, e.Setup())
|
||||
return e
|
||||
}
|
||||
|
||||
func TestComplete_TaskNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{""})
|
||||
|
||||
require.ElementsMatch(t,
|
||||
[]string{"build", "deploy", "dep", "ship", "dynenum", "docs:serve"},
|
||||
values(suggs),
|
||||
)
|
||||
require.Equal(t, complete.DirectiveNoFileComp, dir)
|
||||
require.Contains(t, descriptions(suggs), "Deploy the application")
|
||||
}
|
||||
|
||||
func TestComplete_AliasResolvesToTaskVars(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"dep", ""})
|
||||
require.Equal(t, []string{"ENV=dev", "ENV=staging", "ENV=prod", "REGION="}, values(suggs))
|
||||
require.Equal(t, complete.DirectiveNoSpace|complete.DirectiveNoFileComp, dir)
|
||||
}
|
||||
|
||||
func TestComplete_StaticEnum(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"deploy", ""})
|
||||
|
||||
require.Equal(t, []string{"ENV=dev", "ENV=staging", "ENV=prod", "REGION="}, values(suggs))
|
||||
require.Equal(t, complete.DirectiveNoSpace|complete.DirectiveNoFileComp, dir)
|
||||
}
|
||||
|
||||
func TestComplete_EnumRef(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, _ := complete.Complete(e, newTestFlagSet(), []string{"dynenum", ""})
|
||||
require.Equal(t, []string{"ENV=dev", "ENV=staging", "ENV=prod"}, values(suggs))
|
||||
}
|
||||
|
||||
func TestComplete_NoRequires(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"build", ""})
|
||||
require.Empty(t, suggs)
|
||||
require.Equal(t, complete.DirectiveNoFileComp, dir)
|
||||
}
|
||||
|
||||
func TestComplete_FlagValueNotConfusedWithTaskName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--dir", "deploy", ""})
|
||||
require.ElementsMatch(t,
|
||||
[]string{"build", "deploy", "dep", "ship", "dynenum", "docs:serve"},
|
||||
values(suggs),
|
||||
)
|
||||
require.Equal(t, complete.DirectiveNoFileComp, dir)
|
||||
}
|
||||
|
||||
func TestComplete_NamespacedTaskName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"docs:serve", ""})
|
||||
require.Empty(t, suggs)
|
||||
require.Equal(t, complete.DirectiveNoFileComp, dir)
|
||||
}
|
||||
|
||||
func TestComplete_FlagValueInlineEquals(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--output="})
|
||||
require.Equal(t, []string{"interleaved", "group", "prefixed"}, values(suggs))
|
||||
require.Equal(t, complete.DirectiveNoFileComp, dir)
|
||||
}
|
||||
|
||||
func TestComplete_AfterDash(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"deploy", "--", ""})
|
||||
require.Empty(t, suggs)
|
||||
require.Equal(t, complete.DirectiveDefault, dir)
|
||||
}
|
||||
|
||||
func TestComplete_FlagNames(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"-"})
|
||||
require.NotEmpty(t, suggs)
|
||||
require.Equal(t, complete.DirectiveNoFileComp, dir)
|
||||
|
||||
vals := values(suggs)
|
||||
require.Contains(t, vals, "--list-all")
|
||||
require.Contains(t, vals, "--taskfile")
|
||||
require.Contains(t, vals, "-a")
|
||||
}
|
||||
|
||||
func TestComplete_EnumFlagValue_Output(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--output", ""})
|
||||
require.Equal(t, []string{"interleaved", "group", "prefixed"}, values(suggs))
|
||||
require.Equal(t, complete.DirectiveNoFileComp, dir)
|
||||
}
|
||||
|
||||
func TestComplete_EnumFlagValue_Sort(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, _ := complete.Complete(e, newTestFlagSet(), []string{"--sort", ""})
|
||||
require.Equal(t, []string{"default", "alphanumeric", "none"}, values(suggs))
|
||||
}
|
||||
|
||||
func TestComplete_PathFlag_Taskfile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--taskfile", ""})
|
||||
require.Equal(t, []string{"yml", "yaml"}, values(suggs))
|
||||
require.Equal(t, complete.DirectiveFilterFileExt, dir)
|
||||
}
|
||||
|
||||
func TestComplete_PathFlag_Dir(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--dir", ""})
|
||||
require.Empty(t, suggs)
|
||||
require.Equal(t, complete.DirectiveFilterDirs, dir)
|
||||
}
|
||||
|
||||
func TestComplete_PathFlag_Cacert(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
e := setupExecutor(t)
|
||||
suggs, dir := complete.Complete(e, newTestFlagSet(), []string{"--cacert", ""})
|
||||
require.Empty(t, suggs)
|
||||
require.Equal(t, complete.DirectiveDefault, dir)
|
||||
}
|
||||
|
||||
func TestComplete_NilExecutor(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
suggs, dir := complete.Complete(nil, newTestFlagSet(), []string{"-"})
|
||||
require.NotEmpty(t, suggs)
|
||||
require.Equal(t, complete.DirectiveNoFileComp, dir)
|
||||
}
|
||||
|
||||
func TestWrite_Format(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
complete.Write(&buf, []complete.Suggestion{
|
||||
{Value: "deploy", Description: "Deploy the app"},
|
||||
{Value: "build"},
|
||||
}, complete.DirectiveNoSpace|complete.DirectiveNoFileComp)
|
||||
require.Equal(t, "deploy\tDeploy the app\nbuild\n:6\n", buf.String())
|
||||
}
|
||||
|
||||
func TestWrite_EmptyWithDirective(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
complete.Write(&buf, nil, complete.DirectiveFilterDirs)
|
||||
require.Equal(t, ":16\n", buf.String())
|
||||
}
|
||||
|
||||
func values(suggs []complete.Suggestion) []string {
|
||||
out := make([]string, 0, len(suggs))
|
||||
for _, s := range suggs {
|
||||
out = append(out, s.Value)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func descriptions(suggs []complete.Suggestion) []string {
|
||||
out := make([]string, 0, len(suggs))
|
||||
for _, s := range suggs {
|
||||
out = append(out, s.Description)
|
||||
}
|
||||
return out
|
||||
}
|
||||
65
internal/complete/context.go
Normal file
65
internal/complete/context.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package complete
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
type completionContext struct {
|
||||
toComplete string
|
||||
prev string
|
||||
taskName string
|
||||
afterDash bool
|
||||
}
|
||||
|
||||
// parseContext infers the cursor position from args. fs is needed to skip the
|
||||
// word following a value-taking flag, otherwise `task --dir deploy` would
|
||||
// mistake "deploy" (the directory) for a task name.
|
||||
func parseContext(args []string, knownTasks []string, fs *pflag.FlagSet) completionContext {
|
||||
ctx := completionContext{}
|
||||
if len(args) == 0 {
|
||||
return ctx
|
||||
}
|
||||
|
||||
ctx.toComplete = args[len(args)-1]
|
||||
if len(args) >= 2 {
|
||||
ctx.prev = args[len(args)-2]
|
||||
}
|
||||
|
||||
known := make(map[string]struct{}, len(knownTasks))
|
||||
for _, t := range knownTasks {
|
||||
known[t] = struct{}{}
|
||||
}
|
||||
|
||||
skipNext := false
|
||||
for _, w := range args[:len(args)-1] {
|
||||
if skipNext {
|
||||
skipNext = false
|
||||
continue
|
||||
}
|
||||
if w == "--" {
|
||||
ctx.afterDash = true
|
||||
continue
|
||||
}
|
||||
if ctx.afterDash {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(w, "-") {
|
||||
if !strings.Contains(w, "=") {
|
||||
if f := matchFlagName(fs, w); f != nil && flagTakesValue(f) {
|
||||
skipNext = true
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.Contains(w, "=") {
|
||||
continue
|
||||
}
|
||||
if _, ok := known[w]; ok {
|
||||
ctx.taskName = w
|
||||
}
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
171
internal/complete/engine.go
Normal file
171
internal/complete/engine.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package complete
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/internal/templater"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
// Complete is the single entry point used by `task __complete`. e may be nil
|
||||
// when the Taskfile failed to load; flag completion still works in that case.
|
||||
func Complete(e *task.Executor, fs *pflag.FlagSet, args []string) ([]Suggestion, Directive) {
|
||||
knownTasks := taskNames(e)
|
||||
ctx := parseContext(args, knownTasks, fs)
|
||||
|
||||
if ctx.afterDash {
|
||||
return nil, DirectiveDefault
|
||||
}
|
||||
|
||||
if ctx.prev != "" {
|
||||
if flag := matchFlagName(fs, ctx.prev); flag != nil && flagTakesValue(flag) {
|
||||
return completeFlagValue(flag.Name, ctx.toComplete)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(ctx.toComplete, "-") {
|
||||
if eqIdx := strings.Index(ctx.toComplete, "="); eqIdx != -1 {
|
||||
flagWord := ctx.toComplete[:eqIdx]
|
||||
partial := ctx.toComplete[eqIdx+1:]
|
||||
if f := matchFlagName(fs, flagWord); f != nil && flagTakesValue(f) {
|
||||
return completeFlagValue(f.Name, partial)
|
||||
}
|
||||
}
|
||||
return listFlags(fs), DirectiveNoFileComp
|
||||
}
|
||||
|
||||
if ctx.taskName != "" && e != nil && e.Taskfile != nil {
|
||||
return completeTaskVars(e, ctx.taskName, ctx.toComplete)
|
||||
}
|
||||
|
||||
return completeTaskNames(e), DirectiveNoFileComp
|
||||
}
|
||||
|
||||
func taskNames(e *task.Executor) []string {
|
||||
if e == nil || e.Taskfile == nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for t := range e.Taskfile.Tasks.Values(nil) {
|
||||
if t.Internal {
|
||||
continue
|
||||
}
|
||||
out = append(out, strings.TrimSuffix(t.Task, ":"))
|
||||
for _, alias := range t.Aliases {
|
||||
out = append(out, strings.TrimSuffix(alias, ":"))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func completeTaskNames(e *task.Executor) []Suggestion {
|
||||
if e == nil || e.Taskfile == nil {
|
||||
return nil
|
||||
}
|
||||
tasks, err := e.GetTaskList(task.FilterOutInternal)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Suggestion, 0, len(tasks))
|
||||
for _, t := range tasks {
|
||||
out = append(out, Suggestion{
|
||||
Value: strings.TrimSuffix(t.Task, ":"),
|
||||
Description: t.Desc,
|
||||
})
|
||||
for _, alias := range t.Aliases {
|
||||
out = append(out, Suggestion{
|
||||
Value: strings.TrimSuffix(alias, ":"),
|
||||
Description: t.Desc,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func completeFlagValue(flagName, toComplete string) ([]Suggestion, Directive) {
|
||||
if dir, ok := flagDirective[flagName]; ok {
|
||||
switch dir {
|
||||
case DirectiveFilterFileExt:
|
||||
suggs := make([]Suggestion, 0, len(taskfileExtensions))
|
||||
for _, ext := range taskfileExtensions {
|
||||
suggs = append(suggs, Suggestion{Value: ext})
|
||||
}
|
||||
return suggs, DirectiveFilterFileExt
|
||||
case DirectiveFilterDirs:
|
||||
return nil, DirectiveFilterDirs
|
||||
default:
|
||||
return nil, DirectiveDefault
|
||||
}
|
||||
}
|
||||
|
||||
if values, ok := flagEnums[flagName]; ok {
|
||||
out := make([]Suggestion, 0, len(values))
|
||||
for _, v := range values {
|
||||
out = append(out, Suggestion{Value: v})
|
||||
}
|
||||
_ = toComplete
|
||||
return out, DirectiveNoFileComp
|
||||
}
|
||||
|
||||
return nil, DirectiveDefault
|
||||
}
|
||||
|
||||
func completeTaskVars(e *task.Executor, taskName, toComplete string) ([]Suggestion, Directive) {
|
||||
compiled, err := e.FastCompiledTask(&task.Call{Task: taskName})
|
||||
if err != nil || compiled == nil || compiled.Requires == nil {
|
||||
return nil, DirectiveNoFileComp
|
||||
}
|
||||
|
||||
cache := &templater.Cache{Vars: compiled.Vars}
|
||||
out := make([]Suggestion, 0, 8)
|
||||
for _, v := range compiled.Requires.Vars {
|
||||
if v == nil || v.Name == "" {
|
||||
continue
|
||||
}
|
||||
values := enumValues(v.Enum, cache)
|
||||
if len(values) == 0 {
|
||||
out = append(out, Suggestion{Value: v.Name + "="})
|
||||
continue
|
||||
}
|
||||
for _, val := range values {
|
||||
out = append(out, Suggestion{Value: v.Name + "=" + val})
|
||||
}
|
||||
}
|
||||
_ = toComplete
|
||||
if len(out) == 0 {
|
||||
return nil, DirectiveNoFileComp
|
||||
}
|
||||
return out, DirectiveNoSpace | DirectiveNoFileComp
|
||||
}
|
||||
|
||||
func enumValues(enum *ast.Enum, cache *templater.Cache) []string {
|
||||
if enum == nil {
|
||||
return nil
|
||||
}
|
||||
if len(enum.Value) > 0 {
|
||||
return enum.Value
|
||||
}
|
||||
if enum.Ref == "" {
|
||||
return nil
|
||||
}
|
||||
resolved := templater.ResolveRef(enum.Ref, cache)
|
||||
if cache.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
arr, ok := resolved.([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(arr))
|
||||
for _, item := range arr {
|
||||
s, ok := item.(string)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
return out
|
||||
}
|
||||
71
internal/complete/flags.go
Normal file
71
internal/complete/flags.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package complete
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// flagEnums lists allowed values for enum-style flags. Keep in sync with the
|
||||
// help strings in internal/flags/flags.go.
|
||||
var flagEnums = map[string][]string{
|
||||
"output": {"interleaved", "group", "prefixed"},
|
||||
"sort": {"default", "alphanumeric", "none"},
|
||||
"completion": {"bash", "zsh", "fish", "powershell"},
|
||||
}
|
||||
|
||||
var flagDirective = map[string]Directive{
|
||||
"taskfile": DirectiveFilterFileExt,
|
||||
"dir": DirectiveFilterDirs,
|
||||
"remote-cache-dir": DirectiveFilterDirs,
|
||||
"cacert": DirectiveDefault,
|
||||
"cert": DirectiveDefault,
|
||||
"cert-key": DirectiveDefault,
|
||||
}
|
||||
|
||||
var taskfileExtensions = []string{"yml", "yaml"}
|
||||
|
||||
// flagTakesValue is false for boolean switches (NoOptDefVal == "true").
|
||||
func flagTakesValue(f *pflag.Flag) bool {
|
||||
return f.NoOptDefVal == ""
|
||||
}
|
||||
|
||||
// listFlags walks fs at call time so experiment-gated flags appear or
|
||||
// disappear based on the active experiments.
|
||||
func listFlags(fs *pflag.FlagSet) []Suggestion {
|
||||
if fs == nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]Suggestion, 0, 64)
|
||||
fs.VisitAll(func(f *pflag.Flag) {
|
||||
if f.Hidden || f.Deprecated != "" {
|
||||
return
|
||||
}
|
||||
out = append(out, Suggestion{
|
||||
Value: "--" + f.Name,
|
||||
Description: f.Usage,
|
||||
})
|
||||
if f.Shorthand != "" {
|
||||
out = append(out, Suggestion{
|
||||
Value: "-" + f.Shorthand,
|
||||
Description: f.Usage,
|
||||
})
|
||||
}
|
||||
})
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Value < out[j].Value })
|
||||
return out
|
||||
}
|
||||
|
||||
func matchFlagName(fs *pflag.FlagSet, word string) *pflag.Flag {
|
||||
if fs == nil {
|
||||
return nil
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(word, "--"):
|
||||
return fs.Lookup(strings.TrimPrefix(word, "--"))
|
||||
case strings.HasPrefix(word, "-") && len(word) == 2:
|
||||
return fs.ShorthandLookup(word[1:])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
28
internal/complete/output.go
Normal file
28
internal/complete/output.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package complete
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Write emits the cobra-v2 completion protocol: one `value\tdescription` (or
|
||||
// bare `value`) per suggestion, followed by a trailing `:<directive>` line
|
||||
// that shell wrappers split off even when there are zero suggestions.
|
||||
func Write(w io.Writer, suggs []Suggestion, dir Directive) {
|
||||
for _, s := range suggs {
|
||||
value := sanitize(s.Value)
|
||||
desc := sanitize(s.Description)
|
||||
if desc == "" {
|
||||
fmt.Fprintln(w, value)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\n", value, desc)
|
||||
}
|
||||
fmt.Fprintf(w, ":%d\n", dir)
|
||||
}
|
||||
|
||||
func sanitize(s string) string {
|
||||
r := strings.NewReplacer("\n", " ", "\r", " ", "\t", " ")
|
||||
return r.Replace(s)
|
||||
}
|
||||
@@ -13,13 +13,18 @@ type (
|
||||
}
|
||||
// Task describes a single task
|
||||
Task struct {
|
||||
Name string `json:"name"`
|
||||
Task string `json:"task"`
|
||||
Desc string `json:"desc"`
|
||||
Summary string `json:"summary"`
|
||||
Aliases []string `json:"aliases"`
|
||||
UpToDate *bool `json:"up_to_date,omitempty"`
|
||||
Location *Location `json:"location"`
|
||||
Name string `json:"name"`
|
||||
Task string `json:"task"`
|
||||
Desc string `json:"desc"`
|
||||
Summary string `json:"summary"`
|
||||
Aliases []string `json:"aliases"`
|
||||
UpToDate *bool `json:"up_to_date,omitempty"`
|
||||
Location *Location `json:"location"`
|
||||
Requires []RequiredVar `json:"requires,omitempty"`
|
||||
}
|
||||
RequiredVar struct {
|
||||
Name string `json:"name"`
|
||||
Enum []string `json:"enum,omitempty"`
|
||||
}
|
||||
// Location describes a task's location in a taskfile
|
||||
Location struct {
|
||||
@@ -45,9 +50,28 @@ func NewTask(task *ast.Task) Task {
|
||||
Column: task.Location.Column,
|
||||
Taskfile: task.Location.Taskfile,
|
||||
},
|
||||
Requires: newRequiredVars(task.Requires),
|
||||
}
|
||||
}
|
||||
|
||||
func newRequiredVars(requires *ast.Requires) []RequiredVar {
|
||||
if requires == nil || len(requires.Vars) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]RequiredVar, 0, len(requires.Vars))
|
||||
for _, v := range requires.Vars {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
rv := RequiredVar{Name: v.Name}
|
||||
if v.Enum != nil && len(v.Enum.Value) > 0 {
|
||||
rv.Enum = append([]string{}, v.Enum.Value...)
|
||||
}
|
||||
out = append(out, rv)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (parent *Namespace) AddNamespace(namespacePath []string, task Task) {
|
||||
if len(namespacePath) == 0 {
|
||||
return
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/go-task/task/v3"
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/experiments"
|
||||
"github.com/go-task/task/v3/internal/complete"
|
||||
"github.com/go-task/task/v3/internal/env"
|
||||
"github.com/go-task/task/v3/internal/sort"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
@@ -177,6 +178,13 @@ func init() {
|
||||
pflag.StringVar(&Cert, "cert", getConfig(config, "REMOTE_CERT", func() *string { return config.Remote.Cert }, ""), "Path to a client certificate for HTTPS connections.")
|
||||
pflag.StringVar(&CertKey, "cert-key", getConfig(config, "REMOTE_CERT_KEY", func() *string { return config.Remote.CertKey }, ""), "Path to a client certificate key for HTTPS connections.")
|
||||
}
|
||||
// In completion mode the user's `--flag` words must reach the engine
|
||||
// untouched. The BoolVar/StringVar calls above already populated
|
||||
// pflag.CommandLine, which is all the engine needs.
|
||||
if complete.IsActive() {
|
||||
return
|
||||
}
|
||||
|
||||
pflag.Parse()
|
||||
|
||||
// Auto-detect color based on environment when not explicitly configured
|
||||
|
||||
Reference in New Issue
Block a user