mirror of
https://github.com/go-task/task.git
synced 2026-06-30 16:14:19 +00:00
Compare commits
2 Commits
nightly
...
feat/compl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f9f2ecb8be | ||
|
|
46201bcac9 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -10,11 +10,11 @@
|
|||||||
by @Legimity).
|
by @Legimity).
|
||||||
- PowerShell completions now work with aliases of the `task` command, not just
|
- PowerShell completions now work with aliases of the `task` command, not just
|
||||||
the `task` binary itself (#2852 by @kojiishi).
|
the `task` binary itself (#2852 by @kojiishi).
|
||||||
- Fixed task and namespace aliases not being completed by the Zsh completion. A
|
- Fixed task and namespace aliases not being completed by the Zsh completion.
|
||||||
`show-aliases` zstyle can turn this off (#2865, #2864 by @vmaerten).
|
A `show-aliases` zstyle can turn this off (#2865, #2864 by @vmaerten).
|
||||||
- Fixed task names containing certain characters (e.g. `\`, `_`, `^`) leaking
|
- Fixed task names containing certain characters (e.g. `\`, `_`, `^`) leaking
|
||||||
into checksum/timestamp filenames, breaking `sources:`/`generates:` up-to-date
|
into checksum/timestamp filenames, breaking `sources:`/`generates:`
|
||||||
detection (#2886 by @s3onghyun).
|
up-to-date detection (#2886 by @s3onghyun).
|
||||||
- Fixed `for: matrix:` loops using `ref:` rows producing wrong values when the
|
- Fixed `for: matrix:` loops using `ref:` rows producing wrong values when the
|
||||||
same task was run concurrently (e.g. by parallel `deps`) with different vars
|
same task was run concurrently (e.g. by parallel `deps`) with different vars
|
||||||
(#2890, #2894 by @amitmishra11).
|
(#2890, #2894 by @amitmishra11).
|
||||||
@@ -23,15 +23,16 @@
|
|||||||
- Added the `use_gitignore` setting (global or per-task) to skip files matched
|
- Added the `use_gitignore` setting (global or per-task) to skip files matched
|
||||||
by your `.gitignore` when fingerprinting `sources`/`generates` and when
|
by your `.gitignore` when fingerprinting `sources`/`generates` and when
|
||||||
watching (#2773 by @vmaerten).
|
watching (#2773 by @vmaerten).
|
||||||
- Added support for configuring output flags (`--output`,
|
- Added support for configuring output flags (`--output`, `--output-group-begin`,
|
||||||
`--output-group-begin`, `--output-group-end`, `--output-group-error-only`) via
|
`--output-group-end`, `--output-group-error-only`) via the `TASK_OUTPUT*`
|
||||||
the `TASK_OUTPUT*` environment variables (#2873 by @liiight).
|
environment variables (#2873 by @liiight).
|
||||||
- Added a `--temp-dir` flag (with `TASK_TEMP_DIR` env var and `temp-dir` taskrc
|
- Added a `--temp-dir` flag (with `TASK_TEMP_DIR` env var and `temp-dir` taskrc
|
||||||
config) to customise the directory where Task stores temporary files such as
|
config) to customise the directory where Task stores temporary files such as
|
||||||
checksums. Relative paths are resolved against the root Taskfile (#2891 by
|
checksums. Relative paths are resolved against the root Taskfile (#2891 by
|
||||||
@kjasn).
|
@kjasn).
|
||||||
- Defined environment variable behavior for remote taskfiles (#2267, #2847 by
|
- Unified Bash, Fish, Zsh and PowerShell completions behind a single `task
|
||||||
@vmaerten).
|
__complete` engine, so every shell offers the same suggestions: task names,
|
||||||
|
aliases, flags, flag values and per-task CLI variables (#2897 by @vmaerten).
|
||||||
|
|
||||||
## v3.51.1 - 2026-05-16
|
## v3.51.1 - 2026-05-16
|
||||||
|
|
||||||
|
|||||||
47
cmd/task/complete_cmd.go
Normal file
47
cmd/task/complete_cmd.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3"
|
||||||
|
"github.com/go-task/task/v3/internal/complete"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runComplete(args []string) error {
|
||||||
|
dir, entrypoint, global := extractTaskfileFlags(args)
|
||||||
|
|
||||||
|
e := task.NewExecutor(
|
||||||
|
task.WithDir(dir),
|
||||||
|
task.WithEntrypoint(entrypoint),
|
||||||
|
task.WithStdout(io.Discard),
|
||||||
|
task.WithStderr(io.Discard),
|
||||||
|
task.WithVersionCheck(false),
|
||||||
|
)
|
||||||
|
if global {
|
||||||
|
if home, err := os.UserHomeDir(); err == nil {
|
||||||
|
e.Options(task.WithDir(home))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Best-effort: a missing or broken Taskfile must not break completion.
|
||||||
|
_ = e.Setup()
|
||||||
|
|
||||||
|
suggs, dirv := complete.Complete(e, pflag.CommandLine, args)
|
||||||
|
complete.Write(os.Stdout, suggs, dirv)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTaskfileFlags(args []string) (dir, entrypoint string, global bool) {
|
||||||
|
fs := pflag.NewFlagSet("complete", pflag.ContinueOnError)
|
||||||
|
fs.SetOutput(io.Discard)
|
||||||
|
fs.ParseErrorsAllowlist.UnknownFlags = true
|
||||||
|
fs.Usage = func() {}
|
||||||
|
fs.StringVarP(&dir, "dir", "d", "", "")
|
||||||
|
fs.StringVarP(&entrypoint, "taskfile", "t", "", "")
|
||||||
|
fs.BoolVarP(&global, "global", "g", false, "")
|
||||||
|
_ = fs.Parse(args)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/go-task/task/v3/args"
|
"github.com/go-task/task/v3/args"
|
||||||
"github.com/go-task/task/v3/errors"
|
"github.com/go-task/task/v3/errors"
|
||||||
"github.com/go-task/task/v3/experiments"
|
"github.com/go-task/task/v3/experiments"
|
||||||
|
"github.com/go-task/task/v3/internal/complete"
|
||||||
"github.com/go-task/task/v3/internal/filepathext"
|
"github.com/go-task/task/v3/internal/filepathext"
|
||||||
"github.com/go-task/task/v3/internal/flags"
|
"github.com/go-task/task/v3/internal/flags"
|
||||||
"github.com/go-task/task/v3/internal/logger"
|
"github.com/go-task/task/v3/internal/logger"
|
||||||
@@ -58,6 +59,12 @@ func emitCIErrorAnnotation(err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run() error {
|
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:])
|
||||||
|
}
|
||||||
|
|
||||||
log := &logger.Logger{
|
log := &logger.Logger{
|
||||||
Stdout: os.Stdout,
|
Stdout: os.Stdout,
|
||||||
Stderr: os.Stderr,
|
Stderr: os.Stderr,
|
||||||
|
|||||||
27
compiler.go
27
compiler.go
@@ -15,7 +15,6 @@ import (
|
|||||||
"github.com/go-task/task/v3/internal/logger"
|
"github.com/go-task/task/v3/internal/logger"
|
||||||
"github.com/go-task/task/v3/internal/templater"
|
"github.com/go-task/task/v3/internal/templater"
|
||||||
"github.com/go-task/task/v3/internal/version"
|
"github.com/go-task/task/v3/internal/version"
|
||||||
"github.com/go-task/task/v3/taskfile"
|
|
||||||
"github.com/go-task/task/v3/taskfile/ast"
|
"github.com/go-task/task/v3/taskfile/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -207,19 +206,10 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e
|
|||||||
// Use filepath.ToSlash for all paths to ensure consistent forward slashes
|
// Use filepath.ToSlash for all paths to ensure consistent forward slashes
|
||||||
// across platforms. This prevents issues with backslashes being interpreted
|
// across platforms. This prevents issues with backslashes being interpreted
|
||||||
// as escape sequences when paths are used in shell commands on Windows.
|
// as escape sequences when paths are used in shell commands on Windows.
|
||||||
var rootTaskfile, rootDir string
|
|
||||||
if taskfile.IsRemoteEntrypoint(c.Entrypoint) {
|
|
||||||
rootTaskfile = c.Entrypoint
|
|
||||||
rootDir = ""
|
|
||||||
} else {
|
|
||||||
rootTaskfile = filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint))
|
|
||||||
rootDir = filepath.ToSlash(c.Dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
allVars := map[string]string{
|
allVars := map[string]string{
|
||||||
"TASK_EXE": filepath.ToSlash(os.Args[0]),
|
"TASK_EXE": filepath.ToSlash(os.Args[0]),
|
||||||
"ROOT_TASKFILE": rootTaskfile,
|
"ROOT_TASKFILE": filepath.ToSlash(filepathext.SmartJoin(c.Dir, c.Entrypoint)),
|
||||||
"ROOT_DIR": rootDir,
|
"ROOT_DIR": filepath.ToSlash(c.Dir),
|
||||||
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir),
|
"USER_WORKING_DIR": filepath.ToSlash(c.UserWorkingDir),
|
||||||
"TASK_VERSION": version.GetVersion(),
|
"TASK_VERSION": version.GetVersion(),
|
||||||
"PATH_LIST_SEPARATOR": string(os.PathListSeparator),
|
"PATH_LIST_SEPARATOR": string(os.PathListSeparator),
|
||||||
@@ -227,22 +217,9 @@ func (c *Compiler) getSpecialVars(t *ast.Task, call *Call) (map[string]string, e
|
|||||||
}
|
}
|
||||||
if t != nil {
|
if t != nil {
|
||||||
allVars["TASK"] = t.Task
|
allVars["TASK"] = t.Task
|
||||||
if taskfile.IsRemoteEntrypoint(t.Location.Taskfile) {
|
|
||||||
allVars["TASKFILE"] = t.Location.Taskfile
|
|
||||||
allVars["TASKFILE_DIR"] = ""
|
|
||||||
switch {
|
|
||||||
case t.Dir == "":
|
|
||||||
allVars["TASK_DIR"] = filepath.ToSlash(c.UserWorkingDir)
|
|
||||||
case filepath.IsAbs(t.Dir):
|
|
||||||
allVars["TASK_DIR"] = filepath.ToSlash(t.Dir)
|
|
||||||
default:
|
|
||||||
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.UserWorkingDir, t.Dir))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
|
allVars["TASK_DIR"] = filepath.ToSlash(filepathext.SmartJoin(c.Dir, t.Dir))
|
||||||
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
|
allVars["TASKFILE"] = filepath.ToSlash(t.Location.Taskfile)
|
||||||
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
|
allVars["TASKFILE_DIR"] = filepath.ToSlash(filepath.Dir(t.Location.Taskfile))
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
allVars["TASK"] = ""
|
allVars["TASK"] = ""
|
||||||
allVars["TASK_DIR"] = ""
|
allVars["TASK_DIR"] = ""
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
package task
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
"github.com/go-task/task/v3/internal/filepathext"
|
|
||||||
"github.com/go-task/task/v3/taskfile/ast"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGetSpecialVarsRemote(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
uwd := t.TempDir()
|
|
||||||
uwdSlash := filepath.ToSlash(uwd)
|
|
||||||
localProj := filepath.Join(uwd, "proj")
|
|
||||||
localProjSlash := filepath.ToSlash(localProj)
|
|
||||||
localTaskfile := filepath.Join(localProj, "Taskfile.yml")
|
|
||||||
localTaskfileSlash := filepath.ToSlash(localTaskfile)
|
|
||||||
absTaskDir := filepath.Join(uwd, "opt", "work")
|
|
||||||
absTaskDirSlash := filepath.ToSlash(absTaskDir)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
entrypoint string
|
|
||||||
compilerDir string
|
|
||||||
taskDir string
|
|
||||||
taskfileLocation string
|
|
||||||
wantRootTaskfile string
|
|
||||||
wantRootDir string
|
|
||||||
wantTaskfile string
|
|
||||||
wantTaskfileDir string
|
|
||||||
wantTaskDir string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "local entrypoint, local task",
|
|
||||||
entrypoint: localTaskfile,
|
|
||||||
compilerDir: localProj,
|
|
||||||
taskDir: "",
|
|
||||||
taskfileLocation: localTaskfile,
|
|
||||||
wantRootTaskfile: localTaskfileSlash,
|
|
||||||
wantRootDir: localProjSlash,
|
|
||||||
wantTaskfile: localTaskfileSlash,
|
|
||||||
wantTaskfileDir: localProjSlash,
|
|
||||||
wantTaskDir: localProjSlash,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "https entrypoint, empty task.dir",
|
|
||||||
entrypoint: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
compilerDir: "",
|
|
||||||
taskDir: "",
|
|
||||||
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootDir: "",
|
|
||||||
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantTaskfileDir: "",
|
|
||||||
wantTaskDir: uwdSlash,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "https entrypoint, relative task.dir",
|
|
||||||
entrypoint: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
compilerDir: "",
|
|
||||||
taskDir: "subdir",
|
|
||||||
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootDir: "",
|
|
||||||
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantTaskfileDir: "",
|
|
||||||
wantTaskDir: filepath.ToSlash(filepathext.SmartJoin(uwd, "subdir")),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "https entrypoint, absolute task.dir",
|
|
||||||
entrypoint: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
compilerDir: "",
|
|
||||||
taskDir: absTaskDir,
|
|
||||||
taskfileLocation: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantRootDir: "",
|
|
||||||
wantTaskfile: "https://taskfile.dev/Taskfile.yml",
|
|
||||||
wantTaskfileDir: "",
|
|
||||||
wantTaskDir: absTaskDirSlash,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "git entrypoint",
|
|
||||||
entrypoint: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
|
|
||||||
compilerDir: "",
|
|
||||||
taskDir: "",
|
|
||||||
taskfileLocation: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
|
|
||||||
wantRootTaskfile: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
|
|
||||||
wantRootDir: "",
|
|
||||||
wantTaskfile: "https://github.com/foo/bar.git//Taskfile.yml?ref=main",
|
|
||||||
wantTaskfileDir: "",
|
|
||||||
wantTaskDir: uwdSlash,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "local root, remote included task",
|
|
||||||
entrypoint: localTaskfile,
|
|
||||||
compilerDir: localProj,
|
|
||||||
taskDir: "",
|
|
||||||
taskfileLocation: "https://taskfile.dev/included.yml",
|
|
||||||
wantRootTaskfile: localTaskfileSlash,
|
|
||||||
wantRootDir: localProjSlash,
|
|
||||||
wantTaskfile: "https://taskfile.dev/included.yml",
|
|
||||||
wantTaskfileDir: "",
|
|
||||||
wantTaskDir: uwdSlash,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
c := &Compiler{
|
|
||||||
Dir: tt.compilerDir,
|
|
||||||
Entrypoint: tt.entrypoint,
|
|
||||||
UserWorkingDir: uwd,
|
|
||||||
}
|
|
||||||
task := &ast.Task{
|
|
||||||
Task: "mytask",
|
|
||||||
Dir: tt.taskDir,
|
|
||||||
Location: &ast.Location{Taskfile: tt.taskfileLocation},
|
|
||||||
}
|
|
||||||
|
|
||||||
vars, err := c.getSpecialVars(task, nil)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, tt.wantRootTaskfile, vars["ROOT_TASKFILE"], "ROOT_TASKFILE")
|
|
||||||
assert.Equal(t, tt.wantRootDir, vars["ROOT_DIR"], "ROOT_DIR")
|
|
||||||
assert.Equal(t, tt.wantTaskfile, vars["TASKFILE"], "TASKFILE")
|
|
||||||
assert.Equal(t, tt.wantTaskfileDir, vars["TASKFILE_DIR"], "TASKFILE_DIR")
|
|
||||||
assert.Equal(t, tt.wantTaskDir, vars["TASK_DIR"], "TASK_DIR")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +1,69 @@
|
|||||||
# vim: set tabstop=2 shiftwidth=2 expandtab:
|
# vim: set tabstop=2 shiftwidth=2 expandtab:
|
||||||
|
#
|
||||||
|
# Thin wrapper around `task __complete`. All suggestion logic lives in the
|
||||||
|
# Go engine — do not add completion logic here.
|
||||||
|
|
||||||
_GO_TASK_COMPLETION_LIST_OPTION='--list-all'
|
|
||||||
TASK_CMD="${TASK_EXE:-task}"
|
TASK_CMD="${TASK_EXE:-task}"
|
||||||
|
|
||||||
function _task()
|
_task() {
|
||||||
{
|
|
||||||
local cur prev words cword
|
local cur prev words cword
|
||||||
_init_completion -n : || return
|
_init_completion -n : || return
|
||||||
|
|
||||||
# Check for `--` within command-line and quit or strip suffix.
|
local -a args=()
|
||||||
local i
|
if (( cword > 0 )); then
|
||||||
for i in "${!words[@]}"; do
|
args=( "${words[@]:1:cword}" )
|
||||||
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
|
fi
|
||||||
|
if (( ${#args[@]} == 0 )); then
|
||||||
|
args=( "" )
|
||||||
|
fi
|
||||||
|
|
||||||
|
local output
|
||||||
|
output=$("$TASK_CMD" __complete "${args[@]}" 2>/dev/null)
|
||||||
|
if [[ -z "$output" ]]; then
|
||||||
|
_filedir
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a lines=()
|
||||||
|
local line
|
||||||
|
while IFS= read -r line; do
|
||||||
|
lines+=( "$line" )
|
||||||
|
done <<< "$output"
|
||||||
|
|
||||||
|
local last_idx=$(( ${#lines[@]} - 1 ))
|
||||||
|
local directive="${lines[$last_idx]#:}"
|
||||||
|
unset 'lines[$last_idx]'
|
||||||
|
|
||||||
|
if (( directive & 8 )); then
|
||||||
|
local exts=""
|
||||||
|
for line in "${lines[@]}"; do
|
||||||
|
exts+="${exts:+|}$line"
|
||||||
|
done
|
||||||
|
_filedir "@($exts)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if (( directive & 16 )); then
|
||||||
|
_filedir -d
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
local -a values=()
|
||||||
|
for line in "${lines[@]}"; do
|
||||||
|
values+=( "${line%%$'\t'*}" )
|
||||||
done
|
done
|
||||||
|
|
||||||
# Handle special arguments of options.
|
COMPREPLY=( $( compgen -W "${values[*]}" -- "$cur" ) )
|
||||||
case "$prev" in
|
|
||||||
-d|--dir|--remote-cache-dir)
|
|
||||||
_filedir -d
|
|
||||||
return $?
|
|
||||||
;;
|
|
||||||
--cacert|--cert|--cert-key)
|
|
||||||
_filedir
|
|
||||||
return $?
|
|
||||||
;;
|
|
||||||
-t|--taskfile)
|
|
||||||
_filedir yaml || return $?
|
|
||||||
_filedir yml
|
|
||||||
return $?
|
|
||||||
;;
|
|
||||||
-o|--output)
|
|
||||||
COMPREPLY=( $( compgen -W "interleaved group prefixed" -- $cur ) )
|
|
||||||
return 0
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Handle normal options.
|
if (( directive & 2 )); then
|
||||||
case "$cur" in
|
compopt -o nospace 2>/dev/null
|
||||||
-*)
|
fi
|
||||||
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"
|
__ltrim_colon_completions "$cur"
|
||||||
|
|
||||||
|
if (( ${#COMPREPLY[@]} == 0 )) && ! (( directive & 4 )); then
|
||||||
|
_filedir
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
complete -F _task "$TASK_CMD"
|
complete -F _task "$TASK_CMD"
|
||||||
|
|||||||
@@ -1,120 +1,46 @@
|
|||||||
|
# Thin wrapper around `task __complete`. All suggestion logic lives in the
|
||||||
|
# Go engine — do not add completion logic here.
|
||||||
|
|
||||||
set -l GO_TASK_PROGNAME (if set -q GO_TASK_PROGNAME; echo $GO_TASK_PROGNAME; else if set -q TASK_EXE; echo $TASK_EXE; else; echo task; end)
|
set -l GO_TASK_PROGNAME (if set -q GO_TASK_PROGNAME; echo $GO_TASK_PROGNAME; else if set -q TASK_EXE; echo $TASK_EXE; else; echo task; end)
|
||||||
|
|
||||||
# Cache variables for experiments (global)
|
function __task_complete --inherit-variable GO_TASK_PROGNAME
|
||||||
set -g __task_experiments_cache ""
|
set -l tokens (commandline -opc)
|
||||||
set -g __task_experiments_cache_time 0
|
set -l current (commandline -ct)
|
||||||
|
set -l args
|
||||||
|
if test (count $tokens) -gt 1
|
||||||
|
set args $tokens[2..-1]
|
||||||
|
end
|
||||||
|
set args $args $current
|
||||||
|
|
||||||
# Helper function to get experiments with 1-second cache
|
set -l output ($GO_TASK_PROGNAME __complete $args 2>/dev/null)
|
||||||
function __task_get_experiments --inherit-variable GO_TASK_PROGNAME
|
set -l count (count $output)
|
||||||
set -l now (date +%s)
|
if test $count -eq 0
|
||||||
set -l ttl 1 # Cache for 1 second only
|
|
||||||
|
|
||||||
# Return cached value if still valid
|
|
||||||
if test (math "$now - $__task_experiments_cache_time") -lt $ttl
|
|
||||||
printf '%s\n' $__task_experiments_cache
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Refresh cache
|
set -l last $output[$count]
|
||||||
set -g __task_experiments_cache ($GO_TASK_PROGNAME --experiments 2>/dev/null)
|
if not string match -q ':*' -- $last
|
||||||
set -g __task_experiments_cache_time $now
|
# Protocol violation: emit raw lines as a fallback.
|
||||||
printf '%s\n' $__task_experiments_cache
|
for line in $output
|
||||||
end
|
echo $line
|
||||||
|
|
||||||
# Helper function to check if an experiment is enabled
|
|
||||||
function __task_is_experiment_enabled
|
|
||||||
set -l experiment $argv[1]
|
|
||||||
__task_get_experiments | string match -qr "^\* $experiment:.*on"
|
|
||||||
end
|
|
||||||
|
|
||||||
function __task_get_tasks --description "Prints all available tasks with their description" --inherit-variable GO_TASK_PROGNAME
|
|
||||||
# Check if the global task is requested
|
|
||||||
set -l global_task false
|
|
||||||
commandline --current-process | read --tokenize --list --local cmd_args
|
|
||||||
for arg in $cmd_args
|
|
||||||
if test "_$arg" = "_--"
|
|
||||||
break # ignore arguments to be passed to the task
|
|
||||||
end
|
end
|
||||||
if test "_$arg" = "_--global" -o "_$arg" = "_-g"
|
|
||||||
set global_task true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Read the list of tasks (and potential errors)
|
|
||||||
if $global_task
|
|
||||||
$GO_TASK_PROGNAME --global --list-all
|
|
||||||
else
|
|
||||||
$GO_TASK_PROGNAME --list-all
|
|
||||||
end 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
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
# Grab names and descriptions (if any) of the tasks
|
set -l directive (string replace -r '^:' '' -- $last)
|
||||||
set -l output (echo $rawOutput | sed -e '1d; s/\* \(.*\):[[:space:]]\{2,\}\(.*\)[[:space:]]\{2,\}(\(aliases.*\))/\1\t\2\t\3/' -e 's/\* \(.*\):[[:space:]]\{2,\}\(.*\)/\1\t\2/'| string split0)
|
# FilterFileExt / FilterDirs are handled by fish's native file completion
|
||||||
if test $output
|
# via the separate `complete` registrations below.
|
||||||
echo $output
|
if test (math "$directive & 8") -ne 0; or test (math "$directive & 16") -ne 0
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
if test $count -gt 1
|
||||||
|
for line in $output[1..(math $count - 1)]
|
||||||
|
echo $line
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
complete -c $GO_TASK_PROGNAME \
|
complete -c $GO_TASK_PROGNAME --no-files -a "(__task_complete)"
|
||||||
-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.' \
|
complete -c $GO_TASK_PROGNAME -s t -l taskfile -r -k -a "(__fish_complete_suffix .yml .yaml)"
|
||||||
-xa "(__task_get_tasks)" \
|
complete -c $GO_TASK_PROGNAME -s d -l dir -xa "(__fish_complete_directories)"
|
||||||
-n "not __fish_seen_subcommand_from --"
|
|
||||||
|
|
||||||
# Standard flags
|
|
||||||
complete -c $GO_TASK_PROGNAME -s a -l list-all -d 'list all tasks'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s c -l color -d 'colored output (default true)'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s C -l concurrency -d 'limit number of concurrent tasks'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l completion -d 'generate shell completion script' -xa "bash zsh fish powershell"
|
|
||||||
complete -c $GO_TASK_PROGNAME -s d -l dir -d 'set directory of execution'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l disable-fuzzy -d 'disable fuzzy matching for task names'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s n -l dry -d 'compile and print tasks without executing'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s x -l exit-code -d 'pass-through exit code of task command'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l experiments -d 'list available experiments'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s F -l failfast -d 'when running tasks in parallel, stop all tasks if one fails'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s f -l force -d 'force execution even when up-to-date'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s g -l global -d 'run global Taskfile from home directory'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s h -l help -d 'show help'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s i -l init -d 'create new Taskfile'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l insecure -d 'allow insecure Taskfile downloads'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s I -l interval -d 'interval to watch for changes'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s j -l json -d 'format task list as JSON'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s l -l list -d 'list tasks with descriptions'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l nested -d 'nest namespaces when listing as JSON'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l no-status -d 'ignore status when listing as JSON'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l interactive -d 'prompt for missing required variables'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s o -l output -d 'set output style' -xa "interleaved group prefixed"
|
|
||||||
complete -c $GO_TASK_PROGNAME -l output-group-begin -d 'message template before grouped output'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l output-group-end -d 'message template after grouped output'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l output-group-error-only -d 'hide output from successful tasks'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s p -l parallel -d 'execute tasks in parallel'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s s -l silent -d 'disable echoing'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l sort -d 'set task sorting order' -xa "default alphanumeric none"
|
|
||||||
complete -c $GO_TASK_PROGNAME -l status -d 'exit non-zero if tasks not up-to-date'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l summary -d 'show task summary'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s t -l taskfile -d 'choose Taskfile to run'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s v -l verbose -d 'verbose output'
|
|
||||||
complete -c $GO_TASK_PROGNAME -l version -d 'show version'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s w -l watch -d 'watch mode, re-run on changes'
|
|
||||||
complete -c $GO_TASK_PROGNAME -s y -l yes -d 'assume yes to all prompts'
|
|
||||||
|
|
||||||
# Experimental flags (dynamically checked at completion time via -n condition)
|
|
||||||
# GentleForce experiment
|
|
||||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled GENTLE_FORCE" -l force-all -d 'force execution of task and all dependencies'
|
|
||||||
|
|
||||||
# RemoteTaskfiles experiment - Options
|
|
||||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l offline -d 'use only local or cached Taskfiles'
|
|
||||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l timeout -d 'timeout for remote Taskfile downloads'
|
|
||||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l expiry -d 'cache expiry duration'
|
|
||||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)"
|
|
||||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cacert -d 'custom CA certificate for TLS' -r
|
|
||||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert -d 'client certificate for mTLS' -r
|
|
||||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l cert-key -d 'client certificate private key' -r
|
|
||||||
|
|
||||||
# RemoteTaskfiles experiment - Operations
|
|
||||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l download -d 'download remote Taskfile'
|
|
||||||
complete -c $GO_TASK_PROGNAME -n "__task_is_experiment_enabled REMOTE_TASKFILES" -l clear-cache -d 'clear remote Taskfile cache'
|
|
||||||
|
|||||||
@@ -1,94 +1,61 @@
|
|||||||
using namespace System.Management.Automation
|
using namespace System.Management.Automation
|
||||||
|
|
||||||
|
# Thin wrapper around `task __complete`. All suggestion logic lives in the
|
||||||
|
# Go engine — do not add completion logic here.
|
||||||
|
|
||||||
$cmdNames = @('task') + (Get-Alias -Definition task,task.exe,*\task,*\task.exe -ErrorAction SilentlyContinue).Name | Select-Object -Unique
|
$cmdNames = @('task') + (Get-Alias -Definition task,task.exe,*\task,*\task.exe -ErrorAction SilentlyContinue).Name | Select-Object -Unique
|
||||||
|
|
||||||
Register-ArgumentCompleter -CommandName $cmdNames -ScriptBlock {
|
Register-ArgumentCompleter -Native -CommandName $cmdNames -ScriptBlock {
|
||||||
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
|
param($wordToComplete, $commandAst, $cursorPosition)
|
||||||
|
|
||||||
if ($commandName.StartsWith('-')) {
|
$TaskExe = if ($env:TASK_EXE) { $env:TASK_EXE } else { 'task' }
|
||||||
$completions = @(
|
|
||||||
# Standard flags (alphabetical order)
|
|
||||||
[CompletionResult]::new('-a', '-a', [CompletionResultType]::ParameterName, 'list all tasks'),
|
|
||||||
[CompletionResult]::new('--list-all', '--list-all', [CompletionResultType]::ParameterName, 'list all tasks'),
|
|
||||||
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'colored output'),
|
|
||||||
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'colored output'),
|
|
||||||
[CompletionResult]::new('-C', '-C', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
|
|
||||||
[CompletionResult]::new('--concurrency', '--concurrency', [CompletionResultType]::ParameterName, 'limit concurrent tasks'),
|
|
||||||
[CompletionResult]::new('--completion', '--completion', [CompletionResultType]::ParameterName, 'generate shell completion'),
|
|
||||||
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'set directory'),
|
|
||||||
[CompletionResult]::new('--dir', '--dir', [CompletionResultType]::ParameterName, 'set directory'),
|
|
||||||
[CompletionResult]::new('--disable-fuzzy', '--disable-fuzzy', [CompletionResultType]::ParameterName, 'disable fuzzy matching'),
|
|
||||||
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'dry run'),
|
|
||||||
[CompletionResult]::new('--dry', '--dry', [CompletionResultType]::ParameterName, 'dry run'),
|
|
||||||
[CompletionResult]::new('-x', '-x', [CompletionResultType]::ParameterName, 'pass-through exit code'),
|
|
||||||
[CompletionResult]::new('--exit-code', '--exit-code', [CompletionResultType]::ParameterName, 'pass-through exit code'),
|
|
||||||
[CompletionResult]::new('--experiments', '--experiments', [CompletionResultType]::ParameterName, 'list experiments'),
|
|
||||||
[CompletionResult]::new('-F', '-F', [CompletionResultType]::ParameterName, 'fail fast on pallalel tasks'),
|
|
||||||
[CompletionResult]::new('--failfast', '--failfast', [CompletionResultType]::ParameterName, 'force execution'),
|
|
||||||
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'force execution'),
|
|
||||||
[CompletionResult]::new('--force', '--force', [CompletionResultType]::ParameterName, 'force execution'),
|
|
||||||
[CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'run global Taskfile'),
|
|
||||||
[CompletionResult]::new('--global', '--global', [CompletionResultType]::ParameterName, 'run global Taskfile'),
|
|
||||||
[CompletionResult]::new('-h', '-h', [CompletionResultType]::ParameterName, 'show help'),
|
|
||||||
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'show help'),
|
|
||||||
[CompletionResult]::new('-i', '-i', [CompletionResultType]::ParameterName, 'create new Taskfile'),
|
|
||||||
[CompletionResult]::new('--init', '--init', [CompletionResultType]::ParameterName, 'create new Taskfile'),
|
|
||||||
[CompletionResult]::new('--insecure', '--insecure', [CompletionResultType]::ParameterName, 'allow insecure downloads'),
|
|
||||||
[CompletionResult]::new('-I', '-I', [CompletionResultType]::ParameterName, 'watch interval'),
|
|
||||||
[CompletionResult]::new('--interval', '--interval', [CompletionResultType]::ParameterName, 'watch interval'),
|
|
||||||
[CompletionResult]::new('-j', '-j', [CompletionResultType]::ParameterName, 'format as JSON'),
|
|
||||||
[CompletionResult]::new('--json', '--json', [CompletionResultType]::ParameterName, 'format as JSON'),
|
|
||||||
[CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'list tasks'),
|
|
||||||
[CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'list tasks'),
|
|
||||||
[CompletionResult]::new('--nested', '--nested', [CompletionResultType]::ParameterName, 'nest namespaces in JSON'),
|
|
||||||
[CompletionResult]::new('--no-status', '--no-status', [CompletionResultType]::ParameterName, 'ignore status in JSON'),
|
|
||||||
[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'prompt for missing required variables'),
|
|
||||||
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'set output style'),
|
|
||||||
[CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'set output style'),
|
|
||||||
[CompletionResult]::new('--output-group-begin', '--output-group-begin', [CompletionResultType]::ParameterName, 'template before group'),
|
|
||||||
[CompletionResult]::new('--output-group-end', '--output-group-end', [CompletionResultType]::ParameterName, 'template after group'),
|
|
||||||
[CompletionResult]::new('--output-group-error-only', '--output-group-error-only', [CompletionResultType]::ParameterName, 'hide successful output'),
|
|
||||||
[CompletionResult]::new('-p', '-p', [CompletionResultType]::ParameterName, 'execute in parallel'),
|
|
||||||
[CompletionResult]::new('--parallel', '--parallel', [CompletionResultType]::ParameterName, 'execute in parallel'),
|
|
||||||
[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'silent mode'),
|
|
||||||
[CompletionResult]::new('--silent', '--silent', [CompletionResultType]::ParameterName, 'silent mode'),
|
|
||||||
[CompletionResult]::new('--sort', '--sort', [CompletionResultType]::ParameterName, 'task sorting order'),
|
|
||||||
[CompletionResult]::new('--status', '--status', [CompletionResultType]::ParameterName, 'check task status'),
|
|
||||||
[CompletionResult]::new('--summary', '--summary', [CompletionResultType]::ParameterName, 'show task summary'),
|
|
||||||
[CompletionResult]::new('-t', '-t', [CompletionResultType]::ParameterName, 'choose Taskfile'),
|
|
||||||
[CompletionResult]::new('--taskfile', '--taskfile', [CompletionResultType]::ParameterName, 'choose Taskfile'),
|
|
||||||
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'verbose output'),
|
|
||||||
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'verbose output'),
|
|
||||||
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'show version'),
|
|
||||||
[CompletionResult]::new('-w', '-w', [CompletionResultType]::ParameterName, 'watch mode'),
|
|
||||||
[CompletionResult]::new('--watch', '--watch', [CompletionResultType]::ParameterName, 'watch mode'),
|
|
||||||
[CompletionResult]::new('-y', '-y', [CompletionResultType]::ParameterName, 'assume yes'),
|
|
||||||
[CompletionResult]::new('--yes', '--yes', [CompletionResultType]::ParameterName, 'assume yes')
|
|
||||||
)
|
|
||||||
|
|
||||||
# Experimental flags (dynamically added based on enabled experiments)
|
# Words after the program name, truncated to the cursor.
|
||||||
$experiments = & task --experiments 2>$null | Out-String
|
$argsToPass = @()
|
||||||
|
$elements = $commandAst.CommandElements
|
||||||
if ($experiments -match '\* GENTLE_FORCE:.*on') {
|
if ($elements.Count -gt 1) {
|
||||||
$completions += [CompletionResult]::new('--force-all', '--force-all', [CompletionResultType]::ParameterName, 'force all dependencies')
|
for ($i = 1; $i -lt $elements.Count; $i++) {
|
||||||
|
$el = $elements[$i]
|
||||||
|
if ($el.Extent.StartOffset -ge $cursorPosition) { break }
|
||||||
|
$argsToPass += $el.ToString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# The trailing word (possibly empty) must reach the engine so it knows
|
||||||
|
# the cursor sits on a fresh word.
|
||||||
|
if ($argsToPass.Count -gt 0 -and $argsToPass[-1] -eq $wordToComplete) {
|
||||||
|
$argsToPass[-1] = $wordToComplete
|
||||||
|
} else {
|
||||||
|
$argsToPass += $wordToComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($experiments -match '\* REMOTE_TASKFILES:.*on') {
|
$output = & $TaskExe __complete @argsToPass 2>$null
|
||||||
# Options
|
if (-not $output) { return }
|
||||||
$completions += [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles')
|
|
||||||
$completions += [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout')
|
$lines = @($output)
|
||||||
$completions += [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry')
|
if ($lines.Count -eq 0) { return }
|
||||||
$completions += [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory')
|
$last = $lines[-1]
|
||||||
$completions += [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate')
|
if (-not $last.StartsWith(':')) { return }
|
||||||
$completions += [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate')
|
|
||||||
$completions += [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key')
|
$directive = [int]($last.Substring(1))
|
||||||
# Operations
|
$data = if ($lines.Count -gt 1) { $lines[0..($lines.Count - 2)] } else { @() }
|
||||||
$completions += [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile')
|
|
||||||
$completions += [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache')
|
# FilterFileExt
|
||||||
|
if ($directive -band 8) {
|
||||||
|
$patterns = $data | ForEach-Object { "*.$_" }
|
||||||
|
return Get-ChildItem -Path . -Include $patterns -File -ErrorAction SilentlyContinue |
|
||||||
|
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderItem, $_.Name) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return $completions.Where{ $_.CompletionText.StartsWith($commandName) }
|
# FilterDirs
|
||||||
|
if ($directive -band 16) {
|
||||||
|
return Get-ChildItem -Path . -Directory -ErrorAction SilentlyContinue |
|
||||||
|
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderContainer, $_.Name) }
|
||||||
}
|
}
|
||||||
|
|
||||||
return $(task --list-all --silent) | Where-Object { $_.StartsWith($commandName) } | ForEach-Object { return $_ + " " }
|
return $data | ForEach-Object {
|
||||||
|
$parts = $_ -split "`t", 2
|
||||||
|
$value = $parts[0]
|
||||||
|
$desc = if ($parts.Count -gt 1 -and $parts[1]) { $parts[1] } else { $value }
|
||||||
|
[CompletionResult]::new($value, $value, [CompletionResultType]::ParameterValue, $desc)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,171 +1,65 @@
|
|||||||
#compdef task
|
#compdef task
|
||||||
typeset -A opt_args
|
#
|
||||||
|
# Thin wrapper around `task __complete`. All suggestion logic lives in the
|
||||||
|
# Go engine — do not add completion logic here.
|
||||||
|
|
||||||
TASK_CMD="${TASK_EXE:-task}"
|
TASK_CMD="${TASK_EXE:-task}"
|
||||||
compdef _task "$TASK_CMD"
|
|
||||||
|
|
||||||
_GO_TASK_COMPLETION_LIST_OPTION="${GO_TASK_COMPLETION_LIST_OPTION:---list-all}"
|
|
||||||
|
|
||||||
# Check if an experiment is enabled
|
|
||||||
function __task_is_experiment_enabled() {
|
|
||||||
local experiment=$1
|
|
||||||
task --experiments 2>/dev/null | grep -q "^\* ${experiment}:.*on"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Listing commands from Taskfile.yml
|
|
||||||
function __task_list() {
|
|
||||||
local -a scripts cmd task_aliases match mbegin mend
|
|
||||||
local -i enabled=0
|
|
||||||
local taskfile item task desc task_alias
|
|
||||||
|
|
||||||
cmd=($TASK_CMD)
|
|
||||||
taskfile=${(Qv)opt_args[(i)-t|--taskfile]}
|
|
||||||
taskfile=${taskfile//\~/$HOME}
|
|
||||||
|
|
||||||
for arg in "${words[@]:0:$CURRENT}"; do
|
|
||||||
if [[ "$arg" = "--" ]]; then
|
|
||||||
# Use default completion for words after `--` as they are CLI_ARGS.
|
|
||||||
_default
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ -n "$taskfile" && -f "$taskfile" ]]; then
|
|
||||||
cmd+=(--taskfile "$taskfile")
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if global flag is set
|
|
||||||
if (( ${+opt_args[-g]} || ${+opt_args[--global]} )); then
|
|
||||||
cmd+=(--global)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if output=$("${cmd[@]}" $_GO_TASK_COMPLETION_LIST_OPTION 2>/dev/null); then
|
|
||||||
enabled=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
(( enabled )) || return 0
|
|
||||||
|
|
||||||
scripts=()
|
|
||||||
|
|
||||||
# Read zstyle verbose option (default = true via -T)
|
|
||||||
local show_desc
|
|
||||||
zstyle -T ":completion:${curcontext}:" verbose && show_desc=true || show_desc=false
|
|
||||||
|
|
||||||
# Read zstyle show-aliases option (default = true via -T)
|
|
||||||
local show_aliases
|
|
||||||
zstyle -T ":completion:${curcontext}:" show-aliases && show_aliases=true || show_aliases=false
|
|
||||||
|
|
||||||
for item in "${(@)${(f)output}[2,-1]#\* }"; do
|
|
||||||
task="${item%%:[[:space:]]*}"
|
|
||||||
|
|
||||||
# Extract the aliases listed in the trailing "(aliases: a, b)" column.
|
|
||||||
# NB: `aliases` is a reserved zsh parameter, so use a different name.
|
|
||||||
task_aliases=()
|
|
||||||
if [[ "$show_aliases" == "true" && "$item" == (#b)*'(aliases: '(*)')' ]]; then
|
|
||||||
task_aliases=( "${(@s:, :)match[1]}" )
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$show_desc" == "true" ]]; then
|
|
||||||
local desc="${item##[^[:space:]]##[[:space:]]##}"
|
|
||||||
scripts+=( "${task//:/\\:}:$desc" )
|
|
||||||
for task_alias in $task_aliases; do
|
|
||||||
scripts+=( "${task_alias//:/\\:}:$desc (alias of $task)" )
|
|
||||||
done
|
|
||||||
else
|
|
||||||
scripts+=( "$task" )
|
|
||||||
for task_alias in $task_aliases; do
|
|
||||||
scripts+=( "$task_alias" )
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$show_desc" == "true" ]]; then
|
|
||||||
_describe 'Task to run' scripts
|
|
||||||
else
|
|
||||||
compadd -Q -a scripts
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
_task() {
|
_task() {
|
||||||
local -a standard_args operation_args
|
local -a args lines completions opts
|
||||||
|
local output directive line
|
||||||
|
|
||||||
standard_args=(
|
# (@) preserves a trailing empty string, which the engine relies on to
|
||||||
'(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: '
|
# know the cursor is on a fresh word.
|
||||||
'(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]'
|
args=("${(@)words[2,CURRENT]}")
|
||||||
'(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]'
|
(( ${#args} == 0 )) && args=("")
|
||||||
'(-f --force)'{-f,--force}'[run even if task is up-to-date]'
|
|
||||||
'(-c --color)'{-c,--color}'[colored output]'
|
|
||||||
'(--completion)--completion[generate shell completion script]:shell:(bash zsh fish powershell)'
|
|
||||||
'(-d --dir)'{-d,--dir}'[dir to run in]:execution dir:_dirs'
|
|
||||||
'(--disable-fuzzy)--disable-fuzzy[disable fuzzy matching for task names]'
|
|
||||||
'(-n --dry)'{-n,--dry}'[compiles and prints tasks without executing]'
|
|
||||||
'(--dry)--dry[dry-run mode, compile and print tasks only]'
|
|
||||||
'(-x --exit-code)'{-x,--exit-code}'[pass-through exit code of task command]'
|
|
||||||
'(--experiments)--experiments[list available experiments]'
|
|
||||||
'(-g --global)'{-g,--global}'[run global Taskfile from home directory]'
|
|
||||||
'(--insecure)--insecure[allow insecure Taskfile downloads]'
|
|
||||||
'(-I --interval)'{-I,--interval}'[interval to watch for changes]:duration: '
|
|
||||||
'(-j --json)'{-j,--json}'[format task list as JSON]'
|
|
||||||
'(--nested)--nested[nest namespaces when listing as JSON]'
|
|
||||||
'(--no-status)--no-status[ignore status when listing as JSON]'
|
|
||||||
'(--interactive)--interactive[prompt for missing required variables]'
|
|
||||||
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)'
|
|
||||||
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: '
|
|
||||||
'(--output-group-end)--output-group-end[message template after grouped output]:template text: '
|
|
||||||
'(--output-group-error-only)--output-group-error-only[hide output from successful tasks]'
|
|
||||||
'(-s --silent)'{-s,--silent}'[disable echoing]'
|
|
||||||
'(--sort)--sort[set task sorting order]:order:(default alphanumeric none)'
|
|
||||||
'(--status)--status[exit non-zero if supplied tasks not up-to-date]'
|
|
||||||
'(--summary)--summary[show summary\: field from tasks instead of running them]'
|
|
||||||
'(-t --taskfile)'{-t,--taskfile}'[specify a different taskfile]:taskfile:_files'
|
|
||||||
'(-v --verbose)'{-v,--verbose}'[verbose mode]'
|
|
||||||
'(-w --watch)'{-w,--watch}'[watch-mode for given tasks, re-run when inputs change]'
|
|
||||||
'(-y --yes)'{-y,--yes}'[assume yes to all prompts]'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Experimental flags (dynamically added based on enabled experiments)
|
output=$("$TASK_CMD" __complete "${args[@]}" 2>/dev/null)
|
||||||
# Options (modify behavior)
|
if [[ -z "$output" ]]; then
|
||||||
if __task_is_experiment_enabled "GENTLE_FORCE"; then
|
_files
|
||||||
standard_args+=('(--force-all)--force-all[force execution of task and all dependencies]')
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if __task_is_experiment_enabled "REMOTE_TASKFILES"; then
|
lines=("${(f)output}")
|
||||||
standard_args+=(
|
directive="${lines[-1]#:}"
|
||||||
'(--offline --download)--offline[use only local or cached Taskfiles]'
|
lines=("${(@)lines[1,-2]}")
|
||||||
'(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: '
|
|
||||||
'(--expiry)--expiry[cache expiry duration]:duration: '
|
if (( directive & 8 )); then
|
||||||
'(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs'
|
local -a globs
|
||||||
'(--cacert)--cacert[custom CA certificate for TLS]:file:_files'
|
for line in "${lines[@]}"; do
|
||||||
'(--cert)--cert[client certificate for mTLS]:file:_files'
|
globs+=("*.${line}")
|
||||||
'(--cert-key)--cert-key[client certificate private key]:file:_files'
|
done
|
||||||
)
|
_files -g "(${(j:|:)globs})"
|
||||||
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
operation_args=(
|
if (( directive & 16 )); then
|
||||||
# Task names completion (can be specified multiple times)
|
_path_files -/
|
||||||
'(operation)*: :__task_list'
|
return
|
||||||
# Operational args completion (mutually exclusive)
|
|
||||||
+ '(operation)'
|
|
||||||
'(*)'{-l,--list}'[list describable tasks]'
|
|
||||||
'(*)'{-a,--list-all}'[list all tasks]'
|
|
||||||
'(*)'{-i,--init}'[create new Taskfile.yml]'
|
|
||||||
'(- *)'{-h,--help}'[show help]'
|
|
||||||
'(- *)--version[show version and exit]'
|
|
||||||
)
|
|
||||||
|
|
||||||
# Experimental operations (dynamically added based on enabled experiments)
|
|
||||||
if __task_is_experiment_enabled "REMOTE_TASKFILES"; then
|
|
||||||
standard_args+=(
|
|
||||||
'(--offline --clear-cache)--download[download remote Taskfile]'
|
|
||||||
)
|
|
||||||
operation_args+=(
|
|
||||||
'(* --download)--clear-cache[clear remote Taskfile cache]'
|
|
||||||
)
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_arguments -S $standard_args $operation_args
|
# `:` inside the value must be escaped: _describe splits on the first
|
||||||
|
# unescaped colon (e.g. "docs:serve" would otherwise become value "docs").
|
||||||
|
local value desc
|
||||||
|
for line in "${lines[@]}"; do
|
||||||
|
if [[ "$line" == *$'\t'* ]]; then
|
||||||
|
value="${line%%$'\t'*}"
|
||||||
|
desc="${line#*$'\t'}"
|
||||||
|
completions+=("${value//:/\\:}:$desc")
|
||||||
|
else
|
||||||
|
completions+=("${line//:/\\:}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
(( directive & 2 )) && opts+=(-S '')
|
||||||
|
(( directive & 32 )) && opts+=(-V)
|
||||||
|
|
||||||
|
if (( ${#completions} > 0 )); then
|
||||||
|
_describe -t tasks 'task' completions "${opts[@]}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
(( directive & 4 )) && return
|
||||||
|
_files
|
||||||
}
|
}
|
||||||
|
|
||||||
# don't run the completion function when being source-ed or eval-ed
|
compdef _task "$TASK_CMD"
|
||||||
if [ "$funcstack[1]" = "_task" ]; then
|
|
||||||
_task "$@"
|
|
||||||
fi
|
|
||||||
|
|||||||
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)
|
||||||
|
}
|
||||||
@@ -20,6 +20,11 @@ type (
|
|||||||
Aliases []string `json:"aliases"`
|
Aliases []string `json:"aliases"`
|
||||||
UpToDate *bool `json:"up_to_date,omitempty"`
|
UpToDate *bool `json:"up_to_date,omitempty"`
|
||||||
Location *Location `json:"location"`
|
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 describes a task's location in a taskfile
|
||||||
Location struct {
|
Location struct {
|
||||||
@@ -45,9 +50,28 @@ func NewTask(task *ast.Task) Task {
|
|||||||
Column: task.Location.Column,
|
Column: task.Location.Column,
|
||||||
Taskfile: task.Location.Taskfile,
|
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) {
|
func (parent *Namespace) AddNamespace(namespacePath []string, task Task) {
|
||||||
if len(namespacePath) == 0 {
|
if len(namespacePath) == 0 {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/go-task/task/v3"
|
"github.com/go-task/task/v3"
|
||||||
"github.com/go-task/task/v3/errors"
|
"github.com/go-task/task/v3/errors"
|
||||||
"github.com/go-task/task/v3/experiments"
|
"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/env"
|
||||||
"github.com/go-task/task/v3/internal/sort"
|
"github.com/go-task/task/v3/internal/sort"
|
||||||
"github.com/go-task/task/v3/taskfile/ast"
|
"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(&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.")
|
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()
|
pflag.Parse()
|
||||||
|
|
||||||
// Auto-detect color based on environment when not explicitly configured
|
// Auto-detect color based on environment when not explicitly configured
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func NewNode(
|
|||||||
return node, err
|
return node, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsRemoteEntrypoint(entrypoint string) bool {
|
func isRemoteEntrypoint(entrypoint string) bool {
|
||||||
scheme, _ := getScheme(entrypoint)
|
scheme, _ := getScheme(entrypoint)
|
||||||
switch scheme {
|
switch scheme {
|
||||||
case "git", "http", "https":
|
case "git", "http", "https":
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ func (node *FileNode) Read() ([]byte, error) {
|
|||||||
|
|
||||||
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||||
// If the file is remote, we don't need to resolve the path
|
// If the file is remote, we don't need to resolve the path
|
||||||
if IsRemoteEntrypoint(entrypoint) {
|
if isRemoteEntrypoint(entrypoint) {
|
||||||
return entrypoint, nil
|
return entrypoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ func (node *GitNode) ReadContext(ctx context.Context) ([]byte, error) {
|
|||||||
|
|
||||||
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||||
// If the file is remote, we don't need to resolve the path
|
// If the file is remote, we don't need to resolve the path
|
||||||
if IsRemoteEntrypoint(entrypoint) {
|
if isRemoteEntrypoint(entrypoint) {
|
||||||
return entrypoint, nil
|
return entrypoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ func (node *StdinNode) Read() ([]byte, error) {
|
|||||||
|
|
||||||
func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
func (node *StdinNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||||
// If the file is remote, we don't need to resolve the path
|
// If the file is remote, we don't need to resolve the path
|
||||||
if IsRemoteEntrypoint(entrypoint) {
|
if isRemoteEntrypoint(entrypoint) {
|
||||||
return entrypoint, nil
|
return entrypoint, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -190,21 +190,6 @@ includes:
|
|||||||
my-remote-namespace: https://{{.TOKEN}}@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml
|
my-remote-namespace: https://{{.TOKEN}}@raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
## Special Variables
|
|
||||||
|
|
||||||
The file-path [special variables](../reference/templating.md#file-paths) behave
|
|
||||||
differently when a Taskfile is loaded from a remote source, because there is no
|
|
||||||
local file or directory that corresponds 1:1 to the Taskfile:
|
|
||||||
|
|
||||||
| Variable | Value when loaded remotely |
|
|
||||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
||||||
| `TASKFILE` / `ROOT_TASKFILE` | The original URL, unchanged |
|
|
||||||
| `TASKFILE_DIR` / `ROOT_DIR` | Empty string — a directory variable cannot point to a URL |
|
|
||||||
| `TASK_DIR` | Resolved against `USER_WORKING_DIR` (relative `dir:` → joined with `USER_WORKING_DIR`, empty `dir:` → `USER_WORKING_DIR`, absolute `dir:` → kept as-is) |
|
|
||||||
|
|
||||||
If a remote Taskfile includes a local Taskfile (or vice-versa), each variable
|
|
||||||
reflects the source of the Taskfile it refers to.
|
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
### Automatic checksums
|
### Automatic checksums
|
||||||
|
|||||||
@@ -565,9 +565,6 @@ tasks:
|
|||||||
- echo "{{.MULTILINE | catLines}}" # Replace newlines with spaces
|
- echo "{{.MULTILINE | catLines}}" # Replace newlines with spaces
|
||||||
```
|
```
|
||||||
|
|
||||||
In pipeline form, `join` receives the list from the left-hand side. The
|
|
||||||
equivalent non-pipeline form is `{{join " " .WORDS}}`.
|
|
||||||
|
|
||||||
#### Shell Argument Parsing
|
#### Shell Argument Parsing
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
Reference in New Issue
Block a user