Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4a2f3bb
feat(completion): unify shell wrappers behind `task __complete`
vmaerten Jun 29, 2026
b37305e
chore: changelog for completion engine
vmaerten Jun 29, 2026
e0c049c
fix(completion): harden the __complete engine and shell wrappers
vmaerten Jul 3, 2026
efeafce
test(completion): add cross-shell completion test suite and CI job
vmaerten Jul 3, 2026
0534045
test(completion): clean up temp dirs via EXIT trap in run.sh
vmaerten Jul 3, 2026
8d77fd2
test(completion): test the __complete protocol in Go, thin shell smokes
vmaerten Jul 3, 2026
ff9c99a
chore(completion): trim redundant comments in tests and wrappers
vmaerten Jul 3, 2026
43d2907
refactor(completion): serve the __complete engine from completion/next/
vmaerten Jul 19, 2026
20b1153
feat(completion): add --new-completion to opt into the new engine
vmaerten Jul 19, 2026
eb9784c
docs(completion): document the opt-in --new-completion engine
vmaerten Jul 19, 2026
449e824
fix(completion): keep the directory prefix in PowerShell path completion
vmaerten Jul 19, 2026
4d77675
perf(completion): skip building the task list when completing the fir…
vmaerten Jul 19, 2026
6457585
fix(completion): complete shell values for --new-completion
vmaerten Jul 19, 2026
8e12d56
perf(completion): reuse a package-level output sanitizer
vmaerten Jul 19, 2026
2ed7918
fix(completion): support inline --flag=path completion across shells
vmaerten Jul 19, 2026
10136df
chore(editors): drop the unused requires field from --json task output
vmaerten Jul 19, 2026
8505530
chore(completion): satisfy golangci-lint (slices.Contains, CommandCon…
vmaerten Jul 19, 2026
f37f0f1
chore(completion): fix a wrong Options doc comment and drop a redunda…
vmaerten Jul 19, 2026
ebe91bc
feat(completion): add a Nushell wrapper for the completion engine
vmaerten Aug 10, 2026
ab01cf5
refactor(complete): make the Options zero value the default set
vmaerten Aug 11, 2026
738e80e
refactor(complete): strip the trailing colon in one helper
vmaerten Aug 11, 2026
65e3199
perf(complete): only compile tasks when a description is templated
vmaerten Aug 11, 2026
d74e4a4
refactor(complete): reuse the enum ref resolution of the root package
vmaerten Aug 11, 2026
6be2fad
refactor(complete): classify the completion context in one place
vmaerten Aug 11, 2026
092875a
chore(complete): drop the no-op flagDirective entries
vmaerten Aug 11, 2026
f4a4adf
refactor: serve both completion generations from one shell list
vmaerten Aug 11, 2026
43c3f0e
refactor(complete): lean on stdlib and slicesext helpers
vmaerten Aug 11, 2026
263f53a
perf(complete): buffer the suggestion output
vmaerten Aug 11, 2026
a16d4e3
refactor(complete): resolve the completion dir before building the ex…
vmaerten Aug 11, 2026
aefa8cc
chore(complete): drop the redundant flagset usage stub
vmaerten Aug 11, 2026
f07c8f7
chore(completion): drop unreachable guards and unread constants from …
vmaerten Aug 11, 2026
936a266
refactor(completion): build the PowerShell path candidates in one place
vmaerten Aug 11, 2026
d3a8ece
test(completion): dedupe the shell availability checks
vmaerten Aug 11, 2026
f5c0f13
fix(completion): pass quoted PowerShell arguments to the engine unquoted
vmaerten Aug 11, 2026
1a87366
fix(completion): honor every flag that decides how the Taskfile is lo…
vmaerten Aug 11, 2026
04dc861
fix(completion): never reach the network on a keystroke
vmaerten Aug 11, 2026
b2ab9c7
fix(completion): skip a stdin entrypoint instead of hanging the shell
vmaerten Aug 11, 2026
a24a0af
refactor: move ref resolution to internal/refs
vmaerten Aug 11, 2026
9536fd6
refactor(complete): keep the shell list out of the public API
vmaerten Aug 11, 2026
0fdd529
docs(complete): say why DirectiveError is never emitted
vmaerten Aug 11, 2026
41eeb51
fix(completion): pass zsh KeepOrder to _describe, not to compadd
vmaerten Aug 11, 2026
94261a1
fix(completion): suggest the prefix of wildcard task names
vmaerten Aug 12, 2026
b389d27
chore(completion): silence gosec on the stdin-entrypoint test command
vmaerten Aug 12, 2026
94ef621
fix(completion): keep the legacy wrappers in step with the official r…
vmaerten Aug 13, 2026
d0a53f4
test(completion): guard the offline keystroke path now that remote is…
vmaerten Aug 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,49 @@ jobs:
- name: 🧪 Test
run: task test --output group --output-group-begin '::group::{{.TASK}}' --output-group-end '::endgroup::'

completion:
name: 🐚 Completion (${{ matrix.platform }})
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: 📥 Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: ⬇️ Setup Go
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: 1.26.x

- name: ⬇️ Setup Task
uses: go-task/setup-task@v2

# zsh and pwsh are preinstalled on the runners; only fish is missing
# (plus zsh on the Linux image).
- name: ⬇️ Install shells (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y zsh fish

- name: ⬇️ Install shells (macOS)
if: runner.os == 'macOS'
run: brew install fish

# Nushell ships in no runner image and is not packaged by apt, so it comes
# from its own release archives.
- name: ⬇️ Install Nushell
uses: hustcer/setup-nu@f3fd65374ffc4d60974c0dd2f7263c6c5c285f81 # v3.26
with:
version: "*"

- name: 🧪 Test completion
# Strict mode fails the run if any shell is missing, so we never get a
# false pass when a runner image stops shipping one (e.g. pwsh).
env:
TASK_COMPLETION_STRICT: "1"
run: task test:completion

lint:
name: 🔍 Lint (${{ matrix.go-version }})
strategy:
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@
reports exit code `124`. Callers that join a `run: once` or `when_changed`
task already running now honor their own `timeout`, and inherit that task's
failure instead of being told it succeeded (#1569, #2898 by @vmaerten).
- Added a new completion engine that unifies Bash, Fish, Zsh, Nushell and
PowerShell behind a single `task __complete` command, so every shell offers
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. It is
opt-in for now via `task --new-completion <shell>`, leaving `--completion`
unchanged, and will become the default in a future release (#2897 by
@vmaerten).

## v3.52.0 - 2026-07-02

Expand Down
9 changes: 9 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,15 @@ tasks:
cmds:
- go test -bench=. -benchmem -tags=fsbench -run=^$ ./...

test:completion:
desc: Tests the shell completion engine and wrappers (bash, zsh, fish, nu, powershell)
sources:
- internal/complete/**/*.go
- cmd/task/**/*.go
- completion/**/*
cmds:
- bash completion/tests/run.sh

goreleaser:test:
desc: Tests release process without publishing
cmds:
Expand Down
50 changes: 50 additions & 0 deletions cmd/task/complete_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package main

import (
"bufio"
"io"
"os"
"strings"

"github.com/spf13/pflag"

"github.com/go-task/task/v3"
"github.com/go-task/task/v3/internal/complete"
"github.com/go-task/task/v3/internal/flags"
)

func runComplete(args []string) error {
// Strip the completion-control flags the wrapper prepends; the rest is the
// user's command line to complete.
opts, args := complete.ParseOptions(args)

// WithFlags carries every flag that decides which Taskfile is loaded, as the
// flag package parsed them from the words being completed. The overrides come
// after it: a keystroke must stay silent and must never hit the network,
// prompt for trust or write to the remote cache, whatever remote flags the
// user typed.
e := task.NewExecutor(
flags.WithFlags(),
task.WithStdout(io.Discard),
task.WithStderr(io.Discard),
task.WithStdin(strings.NewReader("")),
task.WithVersionCheck(false),
task.WithOffline(true),
task.WithDownload(false),
)

// Loading the Taskfile parses YAML, so skip it entirely when completing flags
// or their values. Best-effort: a missing or broken Taskfile must not break
// completion. A `-` entrypoint is skipped as well, since reading the Taskfile
// from standard input would hang the shell on a keystroke.
if complete.NeedsTaskfile(args, pflag.CommandLine) && flags.Entrypoint != "-" {
_ = e.Setup()
}

suggs, dirv := complete.Complete(e, pflag.CommandLine, args, opts)

// Buffered: the whole candidate list is written on every keystroke.
out := bufio.NewWriter(os.Stdout)
complete.Write(out, suggs, dirv)
return out.Flush()
}
16 changes: 16 additions & 0 deletions cmd/task/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"github.com/go-task/task/v3/args"
"github.com/go-task/task/v3/errors"
"github.com/go-task/task/v3/experiments"
"github.com/go-task/task/v3/internal/complete"
"github.com/go-task/task/v3/internal/filepathext"
"github.com/go-task/task/v3/internal/flags"
"github.com/go-task/task/v3/internal/logger"
Expand Down Expand Up @@ -58,6 +59,12 @@ func emitCIErrorAnnotation(err 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{
Stdout: os.Stdout,
Stderr: os.Stderr,
Expand Down Expand Up @@ -126,6 +133,15 @@ func run() error {
return nil
}

if flags.NewCompletion != "" {
script, err := task.CompletionNext(flags.NewCompletion)
if err != nil {
return err
}
fmt.Println(script)
return nil
}

e := task.NewExecutor(
flags.WithFlags(),
task.WithVersionCheck(true),
Expand Down
69 changes: 54 additions & 15 deletions completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,59 @@ var completionPowershell string
//go:embed completion/zsh/_task
var completionZsh string

func Completion(completion string) (string, error) {
// Get the file extension for the selected shell
switch completion {
case "bash":
return completionBash, nil
case "fish":
return completionFish, nil
case "nu", "nushell":
return completionNu, nil
case "powershell":
return completionPowershell, nil
case "zsh":
return completionZsh, nil
default:
return "", fmt.Errorf("unknown shell: %s", completion)
// The completion/next/* scripts are thin wrappers around the `task __complete`
// engine. They are served only via `--new-completion` for now (opt-in) and will
// replace the scripts above once the engine becomes the default.

//go:embed completion/next/bash/task.bash
var completionBashNext string

//go:embed completion/next/fish/task.fish
var completionFishNext string

//go:embed completion/next/nu/task-completions.nu
var completionNuNext string

//go:embed completion/next/ps/task.ps1
var completionPowershellNext string

//go:embed completion/next/zsh/_task
var completionZshNext string

// The maps accept `nushell` as an alias of `nu`.
var completionScripts = map[string]string{
"bash": completionBash,
"fish": completionFish,
"nu": completionNu,
"nushell": completionNu,
"powershell": completionPowershell,
"zsh": completionZsh,
}

var completionScriptsNext = map[string]string{
"bash": completionBashNext,
"fish": completionFishNext,
"nu": completionNuNext,
"nushell": completionNuNext,
"powershell": completionPowershellNext,
"zsh": completionZshNext,
}

// Completion returns the default (stable) completion script for the given shell.
func Completion(shell string) (string, error) {
return completionScript(completionScripts, shell)
}

// CompletionNext returns the new `task __complete` engine wrapper for the given
// shell, exposed via `--new-completion` while the engine is opt-in.
func CompletionNext(shell string) (string, error) {
return completionScript(completionScriptsNext, shell)
}

func completionScript(scripts map[string]string, shell string) (string, error) {
script, ok := scripts[shell]
if !ok {
return "", fmt.Errorf("unknown shell: %s", shell)
}
return script, nil
}
95 changes: 95 additions & 0 deletions completion/next/bash/task.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# 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.

TASK_CMD="${TASK_EXE:-task}"

# Wraps _filedir so an inline `--flag=` prefix is stripped before completion and
# re-applied to the results. `=` is kept inside the current word (see the
# `_init_completion -n =:` below), so the whole `--flag=value` token would
# otherwise be treated as the path and never match.
_task_filedir() {
local fpfx="" savecur="$cur"
if [[ "$cur" == -*=* ]]; then
fpfx="${cur%%=*}="
cur="${cur#*=}"
fi
_filedir ${1:+"$1"}
cur="$savecur"
if [[ -n "$fpfx" ]]; then
COMPREPLY=( ${COMPREPLY[@]+"${COMPREPLY[@]/#/$fpfx}"} )
fi
}

_task() {
local cur prev words cword

# Completion directives, mirroring internal/complete/complete.go.
local -ri NO_SPACE=2 NO_FILE_COMP=4 FILTER_FILE_EXT=8 FILTER_DIRS=16

# Exclude both `=` and `:` from the word breaks so `--output=` and
# `docs:serve` reach the engine as single tokens.
_init_completion -n =: || return

local -a args=( "${words[@]:1:cword}" )
if (( ${#args[@]} == 0 )); then
args=( "" )
fi

local output
output=$("$TASK_CMD" __complete "${args[@]}" 2>/dev/null)
if [[ -z "$output" ]]; then
_task_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 & FILTER_FILE_EXT )); then
local exts=""
# ${arr[@]+…} guards against "unbound variable" on an empty array under
# `set -u` in bash 3.2 (macOS).
for line in ${lines[@]+"${lines[@]}"}; do
exts+="${exts:+|}$line"
done
_task_filedir "@($exts)"
return
fi

if (( directive & FILTER_DIRS )); then
_task_filedir -d
return
fi

# Prefix-filter by hand instead of `compgen -W`: the latter joins/splits the
# word list on IFS, which mangles any suggestion value containing a space.
local value
COMPREPLY=()
for line in ${lines[@]+"${lines[@]}"}; do
value="${line%%$'\t'*}"
if [[ -z "$cur" || "$value" == "$cur"* ]]; then
COMPREPLY+=( "$value" )
fi
done

if (( directive & NO_SPACE )); then
compopt -o nospace 2>/dev/null
fi

__ltrim_colon_completions "$cur"

if (( ${#COMPREPLY[@]} == 0 )) && ! (( directive & NO_FILE_COMP )); then
_task_filedir
fi
}

complete -F _task "$TASK_CMD"
Loading
Loading