Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: completions command #1157

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 16 additions & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,13 @@ tasks:
- go install -v ./cmd/task

generate:
desc: Run all generation tasks
aliases: [g, gen]
deps: [generate:mocks, generate:fixtures]

generate:mocks:
desc: Runs Go generate to create mocks
aliases: [gen, g]
aliases: [g:mocks, gen:mocks]
deps: [install:mockery]
sources:
- "internal/fingerprint/checker.go"
Expand All @@ -38,6 +43,16 @@ tasks:
- "{{.BIN}}/mockery --dir ./internal/fingerprint --name SourcesCheckable"
- "{{.BIN}}/mockery --dir ./internal/fingerprint --name StatusCheckable"

generate:fixtures:
desc: Generates fixtures
aliases: [g:fixtures, gen:fixtures]
sources:
- "internal/completion/templates/*"
generates:
- "internal/completion/testdata/**/*.golden"
cmds:
- go test ./internal/completion -update

install:mockery:
desc: Installs mockgen; a tool to generate mock files
vars:
Expand Down
12 changes: 12 additions & 0 deletions cmd/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/go-task/task/v3"
"github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/internal/completion"
"github.com/go-task/task/v3/internal/logger"
"github.com/go-task/task/v3/internal/sort"
ver "github.com/go-task/task/v3/internal/version"
Expand Down Expand Up @@ -47,6 +48,7 @@ var flags struct {
version bool
help bool
init bool
completion string
list bool
listAll bool
listJson bool
Expand Down Expand Up @@ -104,6 +106,7 @@ func run() error {
pflag.BoolVar(&flags.version, "version", false, "Show Task version.")
pflag.BoolVarP(&flags.help, "help", "h", false, "Shows Task usage.")
pflag.BoolVarP(&flags.init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.")
pflag.StringVar(&flags.completion, "completion", "", "Generates shell completion script.")
pflag.BoolVarP(&flags.list, "list", "l", false, "Lists tasks with description of current Taskfile.")
pflag.BoolVarP(&flags.listAll, "list-all", "a", false, "Lists tasks with or without a description.")
pflag.BoolVarP(&flags.listJson, "json", "j", false, "Formats task list as JSON.")
Expand Down Expand Up @@ -226,6 +229,15 @@ func run() error {
return err
}

if flags.completion != "" {
script, err := completion.Compile(flags.completion, e.Taskfile.Tasks)
if err != nil {
return err
}
fmt.Println(script)
return nil
}

if listOptions.ShouldListTasks() {
foundTasks, err := e.ListTasks(listOptions)
if err != nil {
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/radovskyb/watcher v1.0.7
github.com/sajari/fuzzy v1.0.0
github.com/sebdah/goldie/v2 v2.5.3
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.2
golang.org/x/exp v0.0.0-20230212135524-a684f29349b6
Expand All @@ -24,6 +25,7 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.0.0 // indirect
github.com/stretchr/objx v0.5.0 // indirect
golang.org/x/sys v0.6.0 // indirect
golang.org/x/term v0.3.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,25 @@ github.com/mattn/go-zglob v0.0.4 h1:LQi2iOm0/fGgu80AioIJ/1j9w9Oh+9DZ39J4VAGzHQM=
github.com/mattn/go-zglob v0.0.4/go.mod h1:MxxjyoXXnMxfIpxTK2GAkw1w8glPsQILx3N5wrKakiY=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/radovskyb/watcher v1.0.7 h1:AYePLih6dpmS32vlHfhCeli8127LzkIgwJGcwwe8tUE=
github.com/radovskyb/watcher v1.0.7/go.mod h1:78okwvY5wPdzcb1UYnip1pvrZNIVEIh/Cm+ZuvsUYIg=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo=
github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y=
github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
Expand Down
71 changes: 71 additions & 0 deletions internal/completion/completion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package completion

import (
"bytes"
"embed"
"fmt"
"os"
"path/filepath"
"text/template"

"github.com/spf13/pflag"

"github.com/go-task/task/v3/internal/templater"
"github.com/go-task/task/v3/taskfile"
)

type TemplateValues struct {
Entrypoint string
Flags []*pflag.Flag
Tasks []*taskfile.Task
}

//go:embed templates/*
var templates embed.FS

func Compile(completion string, tasks taskfile.Tasks) (string, error) {
// Get the file extension for the selected shell
var ext string
switch completion {
case "bash":
ext = "bash"
case "fish":
ext = "fish"
case "powershell":
ext = "ps1"
case "zsh":
ext = "zsh"
default:
return "", fmt.Errorf("unknown completion shell: %s", completion)
}

// Load the template
templateName := fmt.Sprintf("task.tpl.%s", ext)
tpl, err := template.New(templateName).
Funcs(templater.TemplateFuncs).
ParseFS(templates, filepath.Join("templates", templateName))
if err != nil {
return "", err
}

values := TemplateValues{
Entrypoint: os.Args[0],
Flags: getFlagNames(),
Tasks: tasks.Values(),
}

var buf bytes.Buffer
if err := tpl.Execute(&buf, values); err != nil {
return "", err
}

return buf.String(), nil
}

func getFlagNames() []*pflag.Flag {
var flags []*pflag.Flag
pflag.VisitAll(func(flag *pflag.Flag) {
flags = append(flags, flag)
})
return flags
}
72 changes: 72 additions & 0 deletions internal/completion/completion_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package completion

import (
"os"
"testing"

"github.com/sebdah/goldie/v2"
"github.com/spf13/pflag"
"github.com/stretchr/testify/require"

"github.com/go-task/task/v3/internal/orderedmap"
"github.com/go-task/task/v3/taskfile"
)

var (
tasks = taskfile.Tasks{
OrderedMap: orderedmap.FromMapWithOrder(
map[string]*taskfile.Task{
"foo": {
Task: "foo",
Cmds: []*taskfile.Cmd{
{Cmd: "echo foo"},
},
Desc: "Prints foo",
Aliases: []string{"f"},
},
"bar": {
Task: "bar",
Cmds: []*taskfile.Cmd{
{Cmd: "echo bar"},
},
Desc: "Prints bar",
Aliases: []string{"b"},
},
},
[]string{"foo", "bar"},
),
}
flags struct {
foo string
bar int
noDesc bool
}
)

func init() {
os.Args[0] = "task"
pflag.StringVarP(&flags.foo, "foo", "f", "default", "A regular flag")
pflag.IntVar(&flags.bar, "bar", 99, "A flag with no short variant")
pflag.BoolVar(&flags.noDesc, "no-desc", true, "")
}

func TestCompile(t *testing.T) {
tests := []struct {
shell string
}{
{shell: "bash"},
{shell: "fish"},
{shell: "powershell"},
{shell: "zsh"},
}

for _, tt := range tests {
t.Run(tt.shell, func(t *testing.T) {
completion, err := Compile(tt.shell, tasks)
require.NoError(t, err)

g := goldie.New(t, goldie.WithTestNameForDir(true))
g.Assert(t, tt.shell, []byte(completion))
})
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
55 changes: 55 additions & 0 deletions internal/completion/testdata/TestCompile/bash.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# vim: set tabstop=2 shiftwidth=2 expandtab:

_GO_TASK_COMPLETION_LIST_OPTION='--list-all'

function _task()
{
local cur prev words cword
_init_completion -n : || return

# Check for `--` within command-line and quit or strip suffix.
local i
for i in "${!words[@]}"; do
if [ "${words[$i]}" == "--" ]; then
# Do not complete words following `--` passed to CLI_ARGS.
[ $cword -gt $i ] && return
# Remove the words following `--` to not put --list in CLI_ARGS.
words=( "${words[@]:0:$i}" )
break
fi
done

# Handle special arguments of options.
case "$prev" in
-d|--dir)
_filedir -d
return $?
;;
-t|--taskfile)
_filedir yaml || return $?
_filedir yml
return $?
;;
-o|--output)
COMPREPLY=( $( compgen -W "interleaved group prefixed" -- $cur ) )
return 0
;;
esac

# Handle normal options.
case "$cur" in
-*)
COMPREPLY=( $( compgen -W "$(_parse_help $1)" -- $cur ) )
return 0
;;
esac

# Prepare task name completions.
local tasks=( $( "${words[@]}" --silent $_GO_TASK_COMPLETION_LIST_OPTION 2> /dev/null ) )
COMPREPLY=( $( compgen -W "${tasks[*]}" -- "$cur" ) )

# Post-process because task names might contain colons.
__ltrim_colon_completions "$cur"
}

complete -F _task task
37 changes: 37 additions & 0 deletions internal/completion/testdata/TestCompile/fish.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
set GO_TASK_PROGNAME task

function __task_get_tasks --description "Prints all available tasks with their description"
# Read the list of tasks (and potential errors)
$GO_TASK_PROGNAME --list-all 2>&1 | read -lz rawOutput

# Return on non-zero exit code (for cases when there is no Taskfile found or etc.)
if test $status -ne 0
return
end

# Grab names and descriptions (if any) of the tasks
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):\s*\(.*\)\s*(aliases.*/\1\t\2/' -e 's/\* \(.*\):\s*\(.*\)/\1\t\2/'| string split0)
if test $output
echo $output
end
end

complete -c $GO_TASK_PROGNAME -d 'Runs the specified task(s). Falls back to the "default" task if no task name was specified, or lists all tasks if an unknown task name was
specified.' -xa "(__task_get_tasks)"

complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)'
complete -c $GO_TASK_PROGNAME -s d -l dir -d 'sets directory of execution'
complete -c $GO_TASK_PROGNAME -l dry -d 'compiles and prints tasks in the order that they would be run, without executing them'
complete -c $GO_TASK_PROGNAME -s f -l force -d 'forces execution even when the task is up-to-date'
complete -c $GO_TASK_PROGNAME -s h -l help -d 'shows Task usage'
complete -c $GO_TASK_PROGNAME -s i -l init -d 'creates a new Taskfile.yml in the current folder'
complete -c $GO_TASK_PROGNAME -s l -l list -d 'lists tasks with description of current Taskfile'
complete -c $GO_TASK_PROGNAME -s o -l output -d 'sets output style: [interleaved|group|prefixed]' -xa "interleaved group prefixed"
complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'executes tasks provided on command line in parallel'
complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disables echoing'
complete -c $GO_TASK_PROGNAME -l status -d 'exits with non-zero exit code if any of the given tasks is not up-to-date'
complete -c $GO_TASK_PROGNAME -l summary -d 'show summary about a task'
complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose which Taskfile to run. Defaults to "Taskfile.yml"'
complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'enables verbose mode'
complete -c $GO_TASK_PROGNAME -l version -d 'show Task version'
complete -c $GO_TASK_PROGNAME -s w -l watch -d 'enables watch of the given task'
8 changes: 8 additions & 0 deletions internal/completion/testdata/TestCompile/powershell.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
$scriptBlock = {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters )
$reg = "\* ($commandName.+?):"
$listOutput = $(task --list-all)
$listOutput | Select-String $reg -AllMatches | ForEach-Object { $_.Matches.Groups[1].Value }
}

Register-ArgumentCompleter -CommandName task -ScriptBlock $scriptBlock