feat(completion): unify shell completions behind an opt-in `task __complete` engine
Bash, Fish, Zsh, Nushell and PowerShell now share a single backend: `task __complete` returns the suggestions plus a directive, and every wrapper is a thin shim around it. All five shells offer the same suggestions — task names, aliases, flags, flag values and per-task CLI variables. The Zsh `show-aliases` and `verbose` zstyles keep working, now backed by the `--no-aliases` and `--no-descriptions` completion flags.
The engine is opt-in via `task --new-completion <shell>`, leaving `--completion` and the legacy scripts untouched; it will become the default in a future release. The new wrappers live under `completion/next/`.
Completing a keystroke never reaches the network, never blocks on a stdin entrypoint, and honors every flag that decides how the Taskfile is loaded. Ref resolution shared by `requires` and enum completion moved to `internal/refs`.
A cross-shell test suite exercises the protocol in Go with thin shell smoke tests, and runs in CI.
2026-08-20 13:56:01 +02:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
# Builds the task binary and a fixture Taskfile, then runs every installed shell
|
|
|
|
|
# wrapper against them. The engine itself is covered by the Go tests.
|
|
|
|
|
set -u
|
|
|
|
|
|
fix(completion): complete task names and required vars together
CLI variables are global to the invocation, not scoped to a task: `args.Parse` turns every word holding `=` into a global and every other word into a call, so `task build ENV=dev deploy` and `task build deploy ENV=dev` are the same command. The engine assumed the opposite and, as soon as a word matched a task, served only that task's variables — nothing at all when it had none. `task build <TAB>` and even `task build de<TAB>` went silent, where all five legacy wrappers offered task names at every position.
The engine now unions the still-unset requirements of every task named on the line, and falls through to task names once they are all set. The line resolves itself: fill in what blocks execution, then add another task. Keeping the two families exclusive means each keeps a coherent directive, so nothing loses its trailing space.
Task words are matched with FindMatchingTasks instead of a hand-built list of names truncated at their first `*`, which is why `task wildcard-foo <TAB>` used to offer task names rather than the variables of `wildcard-*`. Completion also disables fuzzy matching: a suggestion list has no "did you mean".
Three fixes ride along. `--sort default` left the sorter nil and cleared the one NewExecutor had set, so completion listed tasks in Taskfile order while `--list` sorted them — and a single templated description silently restored the sort through GetTaskList. The bash wrapper never defined KeepOrder, losing the declaration order of `requires`; it now passes `compopt -o nosort`, which bash 3.2 ignores as it already ignores nospace. And the shell suite unsets TASK_EXE and GO_TASK_PROGNAME: fish, Nushell and PowerShell resolve the binary through them, so an ambient value silently tested something other than the binary just built.
2026-08-20 16:11:49 +02:00
|
|
|
# fish, Nushell and PowerShell resolve the binary through these; an ambient value
|
|
|
|
|
# would silently test something other than the binary built below.
|
|
|
|
|
unset TASK_EXE GO_TASK_PROGNAME
|
|
|
|
|
|
feat(completion): unify shell completions behind an opt-in `task __complete` engine
Bash, Fish, Zsh, Nushell and PowerShell now share a single backend: `task __complete` returns the suggestions plus a directive, and every wrapper is a thin shim around it. All five shells offer the same suggestions — task names, aliases, flags, flag values and per-task CLI variables. The Zsh `show-aliases` and `verbose` zstyles keep working, now backed by the `--no-aliases` and `--no-descriptions` completion flags.
The engine is opt-in via `task --new-completion <shell>`, leaving `--completion` and the legacy scripts untouched; it will become the default in a future release. The new wrappers live under `completion/next/`.
Completing a keystroke never reaches the network, never blocks on a stdin entrypoint, and honors every flag that decides how the Taskfile is loaded. Ref resolution shared by `requires` and enum completion moved to `internal/refs`.
A cross-shell test suite exercises the protocol in Go with thin shell smoke tests, and runs in CI.
2026-08-20 13:56:01 +02:00
|
|
|
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
|
|
|
|
root=$(cd "$here/../.." && pwd)
|
|
|
|
|
|
|
|
|
|
bindir=$(mktemp -d)
|
|
|
|
|
fixture=$(mktemp -d)
|
|
|
|
|
trap 'rm -rf "$bindir" "$fixture"' EXIT
|
|
|
|
|
|
|
|
|
|
if ! go build -o "$bindir/task" "$root/cmd/task"; then
|
|
|
|
|
echo "failed to build task binary" >&2
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
export TASK_BIN="$bindir/task"
|
|
|
|
|
# fish and PowerShell register completion for the command name `task`.
|
|
|
|
|
export PATH="$bindir:$PATH"
|
|
|
|
|
|
|
|
|
|
cat > "$fixture/Taskfile.yml" <<'YML'
|
|
|
|
|
version: '3'
|
|
|
|
|
|
|
|
|
|
tasks:
|
|
|
|
|
build:
|
|
|
|
|
desc: Build it
|
|
|
|
|
deploy:
|
|
|
|
|
desc: Deploy it
|
|
|
|
|
aliases: [dep]
|
|
|
|
|
requires:
|
|
|
|
|
vars:
|
|
|
|
|
- name: ENV
|
|
|
|
|
enum: [dev, prod]
|
|
|
|
|
- REGION
|
|
|
|
|
docs:serve:
|
|
|
|
|
desc: Serve docs
|
|
|
|
|
YML
|
|
|
|
|
touch "$fixture/extra.yaml" "$fixture/notes.txt"
|
|
|
|
|
mkdir -p "$fixture/sub" "$fixture/other"
|
|
|
|
|
# Nested path completion must keep the directory prefix.
|
|
|
|
|
touch "$fixture/sub/nested.yml"
|
|
|
|
|
# Shells must pass a quoted `--dir` value to the engine unquoted, and quote it
|
|
|
|
|
# back on insert.
|
|
|
|
|
mkdir -p "$fixture/with space"
|
|
|
|
|
cat > "$fixture/with space/Taskfile.yml" <<'YML'
|
|
|
|
|
version: '3'
|
|
|
|
|
|
|
|
|
|
tasks:
|
|
|
|
|
spaced:
|
|
|
|
|
desc: Task from the spaced dir
|
|
|
|
|
YML
|
|
|
|
|
export TASK_FIXTURE="$fixture"
|
|
|
|
|
|
|
|
|
|
# Strict mode (CI) turns a missing shell into a failure instead of a skip, so an
|
|
|
|
|
# absent pwsh never reads as a pass.
|
|
|
|
|
strict=${TASK_COMPLETION_STRICT:-}
|
|
|
|
|
|
|
|
|
|
fails=0
|
|
|
|
|
run() { # LABEL COMMAND...
|
|
|
|
|
echo "== $1 =="
|
|
|
|
|
"${@:2}" || fails=$((fails + 1))
|
|
|
|
|
echo
|
|
|
|
|
}
|
|
|
|
|
run_if() { # BIN LABEL COMMAND...
|
|
|
|
|
if command -v "$1" >/dev/null 2>&1; then run "${@:2}"; else skip "$2"; fi
|
|
|
|
|
}
|
|
|
|
|
skip() { # LABEL
|
|
|
|
|
if [[ -n "$strict" ]]; then
|
|
|
|
|
echo "== $1 == (MISSING — required under TASK_COMPLETION_STRICT)"
|
|
|
|
|
fails=$((fails + 1))
|
|
|
|
|
else
|
|
|
|
|
echo "== $1 == (skipped: not installed)"
|
|
|
|
|
fi
|
|
|
|
|
echo
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
run "bash wrapper" bash "$here/wrapper.bash"
|
|
|
|
|
run_if zsh "zsh wrapper" zsh "$here/wrapper.zsh"
|
|
|
|
|
run_if fish "fish wrapper" fish "$here/wrapper.fish"
|
|
|
|
|
# --no-config-file: the user's own external completer must not interfere.
|
|
|
|
|
run_if nu "nu wrapper" nu --no-config-file "$here/wrapper.nu"
|
|
|
|
|
|
|
|
|
|
pwsh_bin=$(command -v pwsh || command -v pwsh-preview || true)
|
|
|
|
|
if [[ -n "$pwsh_bin" ]]; then
|
|
|
|
|
run "powershell wrapper" "$pwsh_bin" -NoProfile -File "$here/wrapper.ps1"
|
|
|
|
|
else
|
|
|
|
|
skip "powershell wrapper"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
if ((fails)); then
|
|
|
|
|
echo "completion tests: $fails suite(s) failed"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
echo "completion tests: all suites passed"
|