Skip to content

feat(completion): unify shell completions behind task __complete#2897

Open
vmaerten wants to merge 18 commits into
mainfrom
feat/completion-engine
Open

feat(completion): unify shell completions behind task __complete#2897
vmaerten wants to merge 18 commits into
mainfrom
feat/completion-engine

Conversation

@vmaerten

@vmaerten vmaerten commented Jun 29, 2026

Copy link
Copy Markdown
Member

Summary

Adds a unified shell completion engine: a single task __complete command, backed by a shared Go package (internal/complete), that drives Bash, Fish, Zsh and PowerShell. The shell scripts become thin wrappers that forward the current words to task __complete and render the suggestions, so every shell gets the same, richer behaviour and there is no more per-shell drift.

The wire protocol mirrors cobra v2 (value\tdescription lines followed by a :<directive> bitfield) behavior.

Why

It's painful to maintain 4 (or more) different completion scripts, and it's also difficult to test them. With this approach, all new flags will be included in every shell by default.

Opt-in rollout

Rather than switching every user at once (millions of users, 4 shells, multiple OSes), the new engine ships opt-in:

  • task --completion <shell> is unchanged and keeps returning the current, stable scripts (still what goreleaser/Homebrew package).
  • task --new-completion <shell> returns the new __complete-powered wrappers.
  • The new wrappers live under completion/next/; the stable ones stay at their canonical completion/<shell>/ paths.

--new-completion will become the default of --completion in a future release, at which point the next/ wrappers move to the canonical paths and the flag is removed.

What the engine completes

  • Task names (with descriptions) and aliases
  • Flags (--xxx) and their shorthands
  • Flag values: file extensions for --taskfile, directories for --dir, enums for --output / --sort / --completion / --new-completion
  • Per-task required variables, including their enum values (task build VAR=)
  • Both separate (--taskfile foo) and inline (--taskfile=foo) flag forms, including nested paths (--dir=sub/…)

The engine skips loading the Taskfile entirely when only flags/flag-values are being completed (NeedsTaskfile), and remote-Taskfile completion never blocks: the approval prompt is a no-op without a TTY, so completion degrades to flag-only suggestions until the remote is approved by running task normally.

Testing

Following the cobra approach, the protocol is tested in Go and the generated scripts are only smoke-tested for how they route each directive:

  • internal/complete/complete_test.go: white-box tests of the engine.
  • completion/protocol_test.go: black-box test of the real binary's __complete output (candidates + directive).
  • completion/tests/: thin per-shell smokes (bash/zsh/fish/powershell) that assert directive routing (no-files, dir-only, extension filter, inline --flag=, …), runnable via task test:completion.
  • A completion CI job runs the smokes on Ubuntu and macOS (with fish and PowerShell installed, strict mode).

Disclaimer: I used AI to write the completion scripts & the testing protocol (in bash).

@vmaerten
vmaerten force-pushed the feat/completion-engine branch 3 times, most recently from b13cc89 to f9f2ecb Compare June 29, 2026 15:42
@vmaerten

Copy link
Copy Markdown
Member Author

@arturict, please don't comment on draft PRs. Also, your comments appear to be AI-generated. We're still open to meaningful, human-written comments. Finally, please avoid giving instructions to maintainers as if you were one of them.

@arturict

Copy link
Copy Markdown

Understood, and sorry about that. I should not have commented on a draft or phrased the comment as an instruction. I'll step back.

vmaerten added 10 commits July 19, 2026 17:38
Engine:
- Emit DirectiveKeepOrder for task variables so the `requires` declaration
  order is preserved instead of being sorted by the shell.
- Return full `--flag=value` candidates for the inline `--output=` form so all
  shells match against the whole current token.
- Add `--no-aliases` / `--no-descriptions` completion flags (via complete.Options)
  parsed from the __complete invocation; the zsh wrapper maps its show-aliases
  and verbose zstyles onto them.
- Skip Taskfile setup when completing flags (NeedsTaskfile) and load the task
  list lazily; drop unused parameters; document exported identifiers.

Shell wrappers:
- zsh: reindent to tabs (.editorconfig), bridge zstyles to engine flags.
- fish: reindent to 2 spaces, handle every file-completion directive in the
  wrapper (--no-files disables the native fallback), drop the duplicated
  yml/yaml extension list now that it lives only in the engine.
- bash: prefix-filter by hand to preserve values containing spaces, exclude `=`
  from word breaks so `--output=` reaches the engine as one token.
- powershell: filter candidates by the current word, fall back to file
  completion for DirectiveDefault.

NoSpace is not representable in fish/PowerShell completion APIs; documented in
the wrappers.
Add completion/tests/ harnesses that exercise the engine protocol and every
shell wrapper without a TTY: the engine via `task __complete`, bash/zsh by
stubbing the completion-system helpers, fish via `complete -C`, and PowerShell
via the completion API. A `test:completion` task and a matching CI job (with a
strict mode that fails when an expected shell is missing) run them all.

Writing the suite surfaced and fixed real wrapper bugs:
- fish: `math` has no bitwise `&`, so every directive check errored; test bits
  with integer division + modulo instead. Also filter FilterFileExt results
  ourselves, as __fish_complete_suffix only prioritizes the extension.
- powershell: `Get-ChildItem -Include` is ignored without -Recurse, so file
  extension filtering returned nothing; filter with Where-Object instead.
- bash: guard empty-array expansion so completion with zero candidates does not
  trip `set -u` on bash 3.2.

Also name the completion directive bits (NO_SPACE, FILTER_FILE_EXT, …) in every
wrapper instead of using raw numbers.
Follow cobra's testing philosophy: the completion protocol (candidates +
directive) is business logic and belongs in Go, while the shell wrappers only
need to be checked for how they interpret each directive.

- Add completion/protocol_test.go: a table-driven black-box test that runs the
  real `task __complete` binary and asserts the offered values and the emitted
  directive. Unlike the in-process engine tests, it exercises the actual
  entrypoint dispatch, runComplete wiring and — crucially — the real flag set,
  so it catches drift between the completion enum/directive maps and the flag
  definitions. Runs on every OS, including Windows where the shell smokes don't.
- Delete completion/tests/engine.sh (the protocol is now covered in Go) and drop
  it from run.sh.
- Reduce the bash/zsh/fish/powershell smokes to directive routing only (files vs
  dirs vs no-files vs no-space), removing the task/alias/var assertions that the
  Go tests already own.
Move the thin __complete wrappers under completion/next/ and restore the
stable, hand-written scripts at their canonical paths so the default output
of `task --completion <shell>` (and the goreleaser/brew packaged scripts)
is unchanged. Repoint the cross-shell test harness at completion/next/.

This is the groundwork for offering the new engine as opt-in before it
becomes the default.
Embed the completion/next/ wrappers and expose them through a new
`task --new-completion <shell>` flag, alongside the unchanged
`task --completion <shell>`. This lets users try the unified __complete
engine (same suggestions across bash/zsh/fish/powershell: task names,
aliases, flags, flag values and requires vars) while it is opt-in; it will
become the default of --completion in a future release.
Add a subsection to the installation guide explaining how to try the new
completion engine by swapping --completion for --new-completion, and noting
it will become the default in a future release.
@vmaerten
vmaerten force-pushed the feat/completion-engine branch from 1521c9e to 32cfaec Compare July 19, 2026 15:47
vmaerten added 8 commits July 19, 2026 18:08
PowerShell replaces the whole token being completed with the CompletionResult
text, so returning `$_.Name` (the basename) for file/dir suggestions dropped
any directory the user had already typed (e.g. `task --dir sub/<TAB>` turned
`sub/` into `foo`). Prepend the typed path prefix to each candidate and make
the default file fallback honor the current word instead of always listing the
working directory. Covered by a nested-path smoke assertion.
…st word

The task-var detection evaluated taskNames(e) — a full walk of every task and
alias — unconditionally, even though detectTaskName bails out when there is no
prior word. Guarding on len(args) > 1 avoids that walk (and a second one via
GetTaskList) for the common `task <TAB>` case, which matters on large monorepo
Taskfiles where completion latency is user-visible.
The --new-completion flag takes a shell name (bash/zsh/fish/powershell) just
like --completion, but it was missing from flagEnums, so `task --new-completion
<TAB>` offered nothing. Add the matching entry.
sanitize built a new strings.Replacer on every call (twice per suggestion).
Hoist it to a package var.
Inline path flags (`--dir=foo`, `--taskfile=foo`, `--cacert=foo`) were broken:
the shells ran file/dir completion against the whole `--flag=value` token, so
nothing matched. Each wrapper now strips the `--flag=` prefix before completing
and re-applies it to the results (zsh via `compset -P '*='`, the others by hand).

Also fixes two smaller wrapper issues in the process: PowerShell prefix matching
is now case-insensitive, and the fish wrapper erases inherited completion rules
before registering (fish accumulates them, unlike bash/zsh/PowerShell). Adds
inline smoke assertions for bash, fish and PowerShell.
The requires field added to the --list --json output is unused by the new
completion engine (which reads requires via FastCompiledTask), untested, and did
not handle enum.ref. Revert it to keep the JSON API change out of this branch.
…text)

Use slices.Contains for the after-dash scan (modernize), and exec.CommandContext
in the protocol test (noctx), with a targeted gosec exception for launching the
test-built binary with test-controlled args.
…nt label

The Options doc claimed the zero value shows everything, but it shows neither
aliases nor descriptions (DefaultOptions enables both). Also remove the
redundant `# FilterDirs` label in the PowerShell wrapper.
@vmaerten
vmaerten marked this pull request as ready for review July 19, 2026 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants