test(completion): add cross-shell completion test suite and CI job

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.
This commit is contained in:
Valentin Maerten
2026-07-03 22:29:42 +02:00
parent 57bc6dd8cb
commit 7c90ee35f8
12 changed files with 579 additions and 26 deletions

View File

@@ -31,3 +31,36 @@ jobs:
- name: Test
run: go run ./cmd/task test
completion:
name: Completion
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, macos-latest]
runs-on: ${{matrix.platform}}
steps:
- name: Check out code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version: 1.26.x
# 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
- 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: go run ./cmd/task test:completion

View File

@@ -152,6 +152,15 @@ tasks:
cmds:
- gotestsum -f '{{.GOTESTSUM_FORMAT}}' -tags 'signals watch' ./...
test:completion:
desc: Tests the shell completion engine and wrappers (bash, zsh, fish, powershell)
sources:
- internal/complete/**/*.go
- cmd/task/**/*.go
- completion/**/*
cmds:
- bash completion/tests/run.sh
goreleaser:test:
desc: Tests release process without publishing
cmds:

View File

@@ -7,6 +7,10 @@ TASK_CMD="${TASK_EXE:-task}"
_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
@@ -36,16 +40,18 @@ _task() {
local directive="${lines[$last_idx]#:}"
unset 'lines[$last_idx]'
if (( directive & 8 )); then
if (( directive & FILTER_FILE_EXT )); then
local exts=""
for line in "${lines[@]}"; do
# ${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
_filedir "@($exts)"
return
fi
if (( directive & 16 )); then
if (( directive & FILTER_DIRS )); then
_filedir -d
return
fi
@@ -54,20 +60,20 @@ _task() {
# word list on IFS, which mangles any suggestion value containing a space.
local value
COMPREPLY=()
for line in "${lines[@]}"; do
for line in ${lines[@]+"${lines[@]}"}; do
value="${line%%$'\t'*}"
if [[ -z "$cur" || "$value" == "$cur"* ]]; then
COMPREPLY+=( "$value" )
fi
done
if (( directive & 2 )); then
if (( directive & NO_SPACE )); then
compopt -o nospace 2>/dev/null
fi
__ltrim_colon_completions "$cur"
if (( ${#COMPREPLY[@]} == 0 )) && ! (( directive & 4 )); then
if (( ${#COMPREPLY[@]} == 0 )) && ! (( directive & NO_FILE_COMP )); then
_filedir
fi
}

View File

@@ -3,6 +3,19 @@
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)
# Completion directives, mirroring internal/complete/complete.go. fish's `math`
# has no bitwise operators, so bits are stored as their power-of-two value and
# tested with integer division + modulo via __task_test_bit.
set -g __task_directive_no_space 2
set -g __task_directive_no_file_comp 4
set -g __task_directive_filter_file_ext 8
set -g __task_directive_filter_dirs 16
set -g __task_directive_keep_order 32
function __task_test_bit --argument-names value bit
test (math "floor($value / $bit) % 2") -eq 1
end
function __task_complete --inherit-variable GO_TASK_PROGNAME
set -l tokens (commandline -opc)
set -l current (commandline -ct)
@@ -36,15 +49,27 @@ function __task_complete --inherit-variable GO_TASK_PROGNAME
# served here, otherwise nothing is offered (e.g. `--cacert`, after `--`).
# FilterFileExt: the engine emits the allowed extensions as the data lines.
if test (math "$directive & 8") -ne 0
for ext in $data
__fish_complete_suffix ".$ext"
# __fish_complete_suffix only *prioritizes* the extension, so filter the file
# list ourselves — keeping directories so the user can still descend into them.
if __task_test_bit $directive $__task_directive_filter_file_ext
for entry in (__fish_complete_path $current)
set -l name (string split -f1 \t -- $entry)
if string match -qr '/$' -- $name
printf '%s\n' $entry
continue
end
for ext in $data
if string match -qr "\.$ext\$" -- $name
printf '%s\n' $entry
break
end
end
end
return
end
# FilterDirs: complete directories only.
if test (math "$directive & 16") -ne 0
if __task_test_bit $directive $__task_directive_filter_dirs
__fish_complete_directories $current
return
end
@@ -55,9 +80,9 @@ function __task_complete --inherit-variable GO_TASK_PROGNAME
printf '%s\n' $line
end
# NoFileComp (bit 4) unset → also offer files, since `--no-files` suppressed
# the native fallback. Covers DirectiveDefault (e.g. `--cacert`, after `--`).
if test (math "$directive & 4") -eq 0
# NoFileComp unset → also offer files, since `--no-files` suppressed the
# native fallback. Covers DirectiveDefault (e.g. `--cacert`, after `--`).
if not __task_test_bit $directive $__task_directive_no_file_comp
__fish_complete_path $current
end
end

View File

@@ -38,20 +38,31 @@ Register-ArgumentCompleter -Native -CommandName $cmdNames -ScriptBlock {
$directive = [int]($last.Substring(1))
$data = if ($lines.Count -gt 1) { $lines[0..($lines.Count - 2)] } else { @() }
# Completion directives, mirroring internal/complete/complete.go.
$NoFileComp = 4
$FilterFileExt = 8
$FilterDirs = 16
# Note: DirectiveNoSpace (bit 2) cannot be honored here — PowerShell's
# CompletionResult API has no per-item "no trailing space" option, so a
# suggestion like `VAR=` gets a trailing space. This is a PowerShell limit.
# 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) }
# FilterFileExt: keep files whose extension matches, plus directories so the
# user can still descend into them. `-Include` is unreliable without
# `-Recurse`, so filter with Where-Object instead.
if ($directive -band $FilterFileExt) {
$exts = $data | ForEach-Object { ".$_" }
return Get-ChildItem -Path "$wordToComplete*" -ErrorAction SilentlyContinue |
Where-Object { $_.PSIsContainer -or $exts -contains $_.Extension } |
ForEach-Object {
$type = if ($_.PSIsContainer) { [CompletionResultType]::ProviderContainer } else { [CompletionResultType]::ProviderItem }
[CompletionResult]::new($_.Name, $_.Name, $type, $_.Name)
}
}
# FilterDirs
if ($directive -band 16) {
return Get-ChildItem -Path . -Directory -ErrorAction SilentlyContinue |
if ($directive -band $FilterDirs) {
return Get-ChildItem -Path "$wordToComplete*" -Directory -ErrorAction SilentlyContinue |
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderContainer, $_.Name) }
}
@@ -68,7 +79,7 @@ Register-ArgumentCompleter -Native -CommandName $cmdNames -ScriptBlock {
# NoFileComp (bit 4) unset and nothing matched → fall back to file completion,
# since the engine returned DirectiveDefault (e.g. --cacert, after `--`).
if ($results.Count -eq 0 -and -not ($directive -band 4)) {
if ($results.Count -eq 0 -and -not ($directive -band $NoFileComp)) {
return Get-ChildItem -Path . -ErrorAction SilentlyContinue |
ForEach-Object { [CompletionResult]::new($_.Name, $_.Name, [CompletionResultType]::ProviderItem, $_.Name) }
}

85
completion/tests/engine.sh Executable file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env bash
# Tests the `task __complete` protocol directly (shell-agnostic). This is the
# backbone: it validates the candidates and directive the engine emits, which
# is what drives every shell wrapper.
#
# Requires: TASK_BIN (path to the task binary), TASK_FIXTURE (dir with a
# Taskfile.yml). Exits non-zero on the first failure.
set -u
: "${TASK_BIN:?TASK_BIN must point to the task binary}"
: "${TASK_FIXTURE:?TASK_FIXTURE must point to the fixture directory}"
cd "$TASK_FIXTURE" || exit 1
fails=0
out() { "$TASK_BIN" __complete "$@" 2>/dev/null; }
vals() { out "$@" | sed '$d' | cut -f1; } # candidate values, sans the :N line
dirv() { out "$@" | tail -1; } # the :N directive line
has() { # LABEL VALUE ARGS...
local label=$1 value=$2; shift 2
if vals "$@" | grep -qxF -- "$value"; then
echo " ok $label"
else
echo " FAIL $label — expected value '$value' among: $(vals "$@" | tr '\n' ' ')"
fails=$((fails + 1))
fi
}
hasnot() { # LABEL VALUE ARGS...
local label=$1 value=$2; shift 2
if vals "$@" | grep -qxF -- "$value"; then
echo " FAIL $label — value '$value' should be absent"
fails=$((fails + 1))
else
echo " ok $label"
fi
}
directive() { # LABEL EXPECTED ARGS...
local label=$1 expected=$2; shift 2
local got; got=$(dirv "$@")
if [[ "$got" == "$expected" ]]; then
echo " ok $label"
else
echo " FAIL $label — expected directive '$expected', got '$got'"
fails=$((fails + 1))
fi
}
echo "engine: task names"
has "lists tasks" build ''
has "lists aliases" dep ''
directive "NoFileComp" ':4' ''
echo "engine: completion-control flags"
hasnot "--no-aliases drops aliases" dep --no-aliases ''
has "--no-aliases keeps tasks" deploy --no-aliases ''
echo "engine: flags"
has "lists flags" --taskfile -
directive "flags NoFileComp" ':4' -
echo "engine: flag values"
has "inline --output= is full form" --output=interleaved --output=
directive "inline NoFileComp" ':4' --output=
has "separate --output is bare" interleaved --output ''
has "--sort values" alphanumeric --sort ''
echo "engine: file/dir directives"
has "--taskfile emits yml" yml --taskfile ''
has "--taskfile emits yaml" yaml --taskfile ''
directive "--taskfile FilterFileExt" ':8' --taskfile ''
directive "--dir FilterDirs" ':16' --dir ''
echo "engine: task variables"
has "required var with enum" ENV=dev deploy ''
has "required var without enum" REGION= deploy ''
directive "vars NoSpace|NoFileComp|KeepOrder" ':38' deploy ''
echo "engine: after --"
directive "after -- is default" ':0' deploy -- ''
if (( fails )); then
echo "engine: $fails failure(s)"
exit 1
fi
echo "engine: all passed"

91
completion/tests/run.sh Executable file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# Runs the completion test suite: builds the task binary, creates a fixture
# Taskfile with sample files and directories, then exercises the engine and
# every installed shell wrapper. Skips shells that are not installed.
set -u
here=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
root=$(cd "$here/../.." && pwd)
# Build the binary under test.
bindir=$(mktemp -d)
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`, so make
# `task` on PATH resolve to the binary under test.
export PATH="$bindir:$PATH"
# Fixture: a Taskfile plus files/dirs so file/dir completion has real entries.
fixture=$(mktemp -d)
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"
export TASK_FIXTURE="$fixture"
# In strict mode (set TASK_COMPLETION_STRICT=1, used in CI) a missing shell is
# a failure instead of a skip, so we never get a false pass when a shell the
# environment was expected to provide (e.g. pwsh on CI runners) is absent.
strict=${TASK_COMPLETION_STRICT:-}
fails=0
run() { # LABEL COMMAND...
echo "== $1 =="
if "${@:2}"; then :; else fails=$((fails + 1)); fi
echo
}
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 "engine" bash "$here/engine.sh"
run "bash wrapper" bash "$here/wrapper.bash"
if command -v zsh >/dev/null 2>&1; then
run "zsh wrapper" zsh "$here/wrapper.zsh"
else
skip "zsh wrapper"
fi
if command -v fish >/dev/null 2>&1; then
run "fish wrapper" fish "$here/wrapper.fish"
else
skip "fish wrapper"
fi
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"

86
completion/tests/wrapper.bash Executable file
View File

@@ -0,0 +1,86 @@
#!/usr/bin/env bash
# Tests the bash wrapper by stubbing the bash-completion helpers it calls
# (_init_completion / _filedir / compopt / __ltrim_colon_completions) and
# asserting the resulting COMPREPLY and file routing. Deterministic, no TTY,
# and works without the bash-completion package installed.
#
# Requires: TASK_BIN (task binary), TASK_FIXTURE (dir with a Taskfile.yml).
set -u
: "${TASK_BIN:?}"; : "${TASK_FIXTURE:?}"
export TASK_EXE="$TASK_BIN"
cd "$TASK_FIXTURE" || exit 1
fails=0
CAP=""
# Stubs standing in for the bash-completion runtime.
_init_completion() {
words=("${TEST_WORDS[@]}")
cword=$TEST_CWORD
cur="${TEST_WORDS[$TEST_CWORD]}"
prev="${TEST_WORDS[$((TEST_CWORD - 1))]}"
return 0
}
_filedir() { CAP+="filedir:$*"$'\n'; }
compopt() { CAP+="compopt:$*"$'\n'; }
__ltrim_colon_completions() { :; }
source "$(dirname "${BASH_SOURCE[0]}")/../bash/task.bash"
run() {
CAP=""
TEST_WORDS=("$@")
TEST_CWORD=$((${#TEST_WORDS[@]} - 1))
COMPREPLY=()
_task
}
reply_has() { # LABEL VALUE
local v
for v in "${COMPREPLY[@]}"; do [[ "$v" == "$2" ]] && { echo " ok $1"; return; }; done
echo " FAIL $1 — '$2' missing from COMPREPLY: ${COMPREPLY[*]}"
fails=$((fails + 1))
}
cap_has() { # LABEL PATTERN
if [[ "$CAP" == *"$2"* ]]; then echo " ok $1"; else
echo " FAIL $1 — expected '$2' in: $CAP"; fails=$((fails + 1)); fi
}
cap_hasnot() { # LABEL PATTERN
if [[ "$CAP" == *"$2"* ]]; then
echo " FAIL $1 — '$2' should be absent in: $CAP"; fails=$((fails + 1)); else
echo " ok $1"; fi
}
echo "bash: task names (no file fallback)"
run task ''
reply_has "lists tasks" build
reply_has "lists aliases" dep
cap_hasnot "no file fallback" "filedir:"
echo "bash: task variables"
run task deploy ''
reply_has "required vars" "ENV=dev"
cap_has "NoSpace nospace" "compopt:-o nospace"
echo "bash: inline --output= is full form"
run task '--output='
reply_has "full-form value" "--output=interleaved"
echo "bash: --dir routes to directory completion"
run task --dir ''
cap_has "filedir -d" "filedir:-d"
echo "bash: --taskfile routes to extension-filtered files"
run task --taskfile ''
cap_has "filedir ext glob" "filedir:@(yml|yaml)"
echo "bash: after -- falls back to files"
run task build -- ''
cap_has "filedir after --" "filedir:"
if ((fails)); then
echo "bash: $fails failure(s)"
exit 1
fi
echo "bash: all passed"

61
completion/tests/wrapper.fish Executable file
View File

@@ -0,0 +1,61 @@
#!/usr/bin/env fish
# Tests the fish wrapper end-to-end via `complete -C`, which asks fish for the
# real completions of a command line without a TTY. The `task` command must
# resolve to the binary under test (run.sh puts a symlink on PATH).
#
# Requires: TASK_FIXTURE (dir with a Taskfile.yml and sample files/dirs).
cd $TASK_FIXTURE
source (dirname (status -f))/../fish/task.fish
set -g fails 0
function cands
complete -C $argv[1] | string split -f1 \t
end
function has # LABEL LINE VALUE
if contains -- $argv[3] (cands $argv[2])
echo " ok $argv[1]"
else
echo " FAIL $argv[1] — '$argv[3]' missing from: "(cands $argv[2])
set fails (math $fails + 1)
end
end
function hasnot # LABEL LINE VALUE
if contains -- $argv[3] (cands $argv[2])
echo " FAIL $argv[1] — '$argv[3]' should be absent"
set fails (math $fails + 1)
else
echo " ok $argv[1]"
end
end
echo "fish: task names (no files)"
has "lists tasks" 'task ' build
has "lists aliases" 'task ' dep
hasnot "no files for tasks" 'task ' notes.txt
echo "fish: task variables"
has "required vars" 'task deploy ' ENV=dev
echo "fish: flag values"
has "enum values" 'task --output ' interleaved
echo "fish: --dir completes directories only"
has "dirs offered" 'task --dir ' sub/
hasnot "no plain files" 'task --dir ' notes.txt
echo "fish: --taskfile filters by extension"
has "yaml offered" 'task --taskfile ' Taskfile.yml
hasnot "txt filtered out" 'task --taskfile ' notes.txt
echo "fish: after -- completes files"
has "files after --" 'task build -- ' notes.txt
if test $fails -ne 0
echo "fish: $fails failure(s)"
exit 1
end
echo "fish: all passed"

View File

@@ -0,0 +1,62 @@
# Tests the PowerShell wrapper end-to-end via the completion API, which returns
# the real completions of a command line without a TTY. The `task` command must
# resolve to the binary under test (run.sh puts a symlink on PATH).
#
# Requires: $env:TASK_FIXTURE (dir with a Taskfile.yml and sample files/dirs).
Set-Location $env:TASK_FIXTURE
. "$PSScriptRoot/../ps/task.ps1"
$fails = 0
function Cands($line) {
([System.Management.Automation.CommandCompletion]::CompleteInput($line, $line.Length, $null)).CompletionMatches |
ForEach-Object { $_.CompletionText }
}
function Has($label, $line, $value) {
if ((Cands $line) -contains $value) {
Write-Output " ok $label"
} else {
Write-Output " FAIL $label — '$value' missing from: $((Cands $line) -join ' ')"
$script:fails++
}
}
function HasNot($label, $line, $value) {
if ((Cands $line) -contains $value) {
Write-Output " FAIL $label — '$value' should be absent"
$script:fails++
} else {
Write-Output " ok $label"
}
}
Write-Output "powershell: task names (no files)"
Has "lists tasks" 'task ' 'build'
Has "lists aliases" 'task ' 'dep'
HasNot "no files for tasks" 'task ' 'notes.txt'
Write-Output "powershell: prefix filtering"
Has "filters by prefix" 'task b' 'build'
HasNot "prefix excludes" 'task b' 'deploy'
Write-Output "powershell: task variables"
Has "required vars" 'task deploy ' 'ENV=dev'
Write-Output "powershell: flag values"
Has "enum values" 'task --output ' 'interleaved'
Write-Output "powershell: --dir completes directories only"
Has "dirs offered" 'task --dir ' 'sub'
HasNot "no plain files" 'task --dir ' 'notes.txt'
Write-Output "powershell: --taskfile filters by extension"
Has "yaml offered" 'task --taskfile ' 'Taskfile.yml'
HasNot "txt filtered out" 'task --taskfile ' 'notes.txt'
if ($fails -ne 0) {
Write-Output "powershell: $fails failure(s)"
exit 1
}
Write-Output "powershell: all passed"

81
completion/tests/wrapper.zsh Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env zsh
# Tests the zsh wrapper by stubbing the completion-system functions it calls
# (_describe / _files / _path_files) and asserting how it routes each directive.
# This is deterministic and needs no TTY.
#
# Requires: TASK_BIN (task binary), TASK_FIXTURE (dir with a Taskfile.yml).
export TASK_EXE=$TASK_BIN
cd $TASK_FIXTURE
integer fails=0
local CAP
compdef() { } # no-op: we call _task directly, not through compinit
_describe() {
local arr=$4
CAP+="describe_opts:${@[5,-1]}"$'\n'
local c; for c in ${(P)arr}; do CAP+="cand:$c"$'\n'; done
}
_files() { CAP+="files:$*"$'\n' }
_path_files() { CAP+="path_files:$*"$'\n' }
# Sourcing (not autoloading) defines _task and avoids the autoload first-call
# quirk; the trailing `compdef` call is stubbed above.
source ${0:A:h}/../zsh/_task
run() {
CAP=""
local -a words=("$@")
integer CURRENT=$#words
local curcontext=":completion:complete:task:"
_task
}
has() { # LABEL PATTERN
if [[ "$CAP" == *"$2"* ]]; then
echo " ok $1"
else
echo " FAIL $1 — expected '$2' in:"$'\n'"$CAP"
(( fails++ ))
fi
}
hasnot() { # LABEL PATTERN
if [[ "$CAP" == *"$2"* ]]; then
echo " FAIL $1 — '$2' should be absent in:"$'\n'"$CAP"
(( fails++ ))
else
echo " ok $1"
fi
}
echo "zsh: task names (no file fallback)"
run task ''
has "lists tasks" "cand:build"
has "lists aliases" "cand:dep"
hasnot "no file fallback" "files:"
echo "zsh: task variables"
run task deploy ''
has "required vars" "cand:ENV=dev"
has "NoSpace -> -S" "describe_opts:-S"
has "KeepOrder -> -V" "-V"
echo "zsh: --dir routes to directory completion"
run task --dir ''
has "path_files -/" "path_files:-/"
echo "zsh: --taskfile routes to extension-filtered files"
run task --taskfile ''
has "files glob" "files:"
has "yml in glob" "yml"
echo "zsh: after -- falls back to files"
run task build -- ''
has "files after --" "files:"
if (( fails )); then
echo "zsh: $fails failure(s)"
exit 1
fi
echo "zsh: all passed"

View File

@@ -9,6 +9,9 @@ _task() {
local -a args lines completions opts ctl
local output directive line
# Completion directives, mirroring internal/complete/complete.go.
local -ri NO_SPACE=2 NO_FILE_COMP=4 FILTER_FILE_EXT=8 FILTER_DIRS=16 KEEP_ORDER=32
# Map the zsh completion zstyles to engine flags. `-T` is true when the
# style is unset (its default) or explicitly true, so a flag is only passed
# when the user turned the style off.
@@ -30,7 +33,7 @@ _task() {
directive="${lines[-1]#:}"
lines=("${(@)lines[1,-2]}")
if (( directive & 8 )); then
if (( directive & FILTER_FILE_EXT )); then
local -a globs
for line in "${lines[@]}"; do
globs+=("*.${line}")
@@ -39,7 +42,7 @@ _task() {
return
fi
if (( directive & 16 )); then
if (( directive & FILTER_DIRS )); then
_path_files -/
return
fi
@@ -57,14 +60,14 @@ _task() {
fi
done
(( directive & 2 )) && opts+=(-S '')
(( directive & 32 )) && opts+=(-V)
(( directive & NO_SPACE )) && opts+=(-S '')
(( directive & KEEP_ORDER )) && opts+=(-V)
if (( ${#completions} > 0 )); then
_describe -t tasks 'task' completions "${opts[@]}"
fi
(( directive & 4 )) && return
(( directive & NO_FILE_COMP )) && return
_files
}