From b782d18a4551790b6cee9854efc23c2bc11a4edf Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Sat, 29 Aug 2026 10:36:54 +0200 Subject: [PATCH] feat(completion): make the engine the default behind --completion The engine shipped opt-in behind --new-completion, with the old scripts still on --completion. That split was never released, so flip it now rather than carry a third flag through a deprecation later. --completion serves the engine wrappers; the hand-written scripts move to completion/legacy/ and stay reachable via --legacy-completion for a release or two. .goreleaser.yml needed to follow: it packages the static files from completion/ into the deb/rpm/apk contents and the Homebrew cask, so leaving it untouched would have shipped the engine to anyone running `eval "$(task --completion zsh)"` and the old scripts to everyone installing from a package. The paths it references are unchanged and now resolve to the wrappers. Its archive glob is narrowed at the same time, so the test harness under completion/tests/ stops being shipped in the release archives. --- .goreleaser.yml | 7 +- CHANGELOG.md | 16 +- cmd/task/task.go | 4 +- completion.go | 44 +++-- completion/bash/task.bash | 126 +++++++----- completion/fish/task.fish | 186 ++++++++--------- completion/legacy/bash/task.bash | 60 ++++++ completion/legacy/fish/task.fish | 116 +++++++++++ completion/legacy/nu/task-completions.nu | 180 +++++++++++++++++ completion/legacy/ps/task.ps1 | 89 +++++++++ completion/legacy/zsh/_task | 158 +++++++++++++++ completion/next/bash/task.bash | 94 --------- completion/next/fish/task.fish | 98 --------- completion/next/nu/task-completions.nu | 86 -------- completion/next/ps/task.ps1 | 109 ---------- completion/next/zsh/_task | 76 ------- completion/nu/task-completions.nu | 242 +++++++---------------- completion/ps/task.ps1 | 178 +++++++++-------- completion/tests/wrapper.bash | 2 +- completion/tests/wrapper.fish | 2 +- completion/tests/wrapper.nu | 2 +- completion/tests/wrapper.ps1 | 2 +- completion/tests/wrapper.zsh | 2 +- completion/zsh/_task | 214 +++++++------------- internal/complete/complete_test.go | 6 +- internal/complete/flags.go | 8 +- internal/flags/flags.go | 4 +- website/src/next/docs/installation.md | 87 ++------ 28 files changed, 1078 insertions(+), 1120 deletions(-) create mode 100644 completion/legacy/bash/task.bash create mode 100644 completion/legacy/fish/task.fish create mode 100644 completion/legacy/nu/task-completions.nu create mode 100644 completion/legacy/ps/task.ps1 create mode 100755 completion/legacy/zsh/_task delete mode 100644 completion/next/bash/task.bash delete mode 100644 completion/next/fish/task.fish delete mode 100644 completion/next/nu/task-completions.nu delete mode 100644 completion/next/ps/task.ps1 delete mode 100755 completion/next/zsh/_task diff --git a/.goreleaser.yml b/.goreleaser.yml index 245ac343..cff7c73e 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -43,7 +43,12 @@ archives: files: - README.md - LICENSE - - completion/**/* + - completion/bash/* + - completion/fish/* + - completion/nu/* + - completion/ps/* + - completion/zsh/* + - completion/legacy/**/* format_overrides: - goos: windows formats: [zip] diff --git a/CHANGELOG.md b/CHANGELOG.md index ee68fd02..0fb7dc06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,14 @@ ### 🚀 Features -- 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 `, leaving `--completion` - unchanged, and will become the default in a future release (#2897 by - @vmaerten). +- `task --completion ` now serves 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. The previous hand-written scripts remain + available as `task --legacy-completion `; they are deprecated and will + be removed in a future release (#2897 by @vmaerten). ### 📦 Package API diff --git a/cmd/task/task.go b/cmd/task/task.go index 7b1c19f6..12630b92 100644 --- a/cmd/task/task.go +++ b/cmd/task/task.go @@ -133,8 +133,8 @@ func run() error { return nil } - if flags.NewCompletion != "" { - script, err := task.CompletionNext(flags.NewCompletion) + if flags.LegacyCompletion != "" { + script, err := task.LegacyCompletion(flags.LegacyCompletion) if err != nil { return err } diff --git a/completion.go b/completion.go index 15e68961..e26738f9 100644 --- a/completion.go +++ b/completion.go @@ -5,6 +5,8 @@ import ( "fmt" ) +// Thin wrappers around the `task __complete` engine, served by `--completion`. + //go:embed completion/bash/task.bash var completionBash string @@ -20,23 +22,23 @@ var completionPowershell string //go:embed completion/zsh/_task var completionZsh string -// Thin wrappers around the `task __complete` engine, served via -// `--new-completion` until the engine becomes the default. +// The self-contained scripts that predate the engine, kept behind +// `--legacy-completion` as an escape hatch for a couple of releases. -//go:embed completion/next/bash/task.bash -var completionBashNext string +//go:embed completion/legacy/bash/task.bash +var completionBashLegacy string -//go:embed completion/next/fish/task.fish -var completionFishNext string +//go:embed completion/legacy/fish/task.fish +var completionFishLegacy string -//go:embed completion/next/nu/task-completions.nu -var completionNuNext string +//go:embed completion/legacy/nu/task-completions.nu +var completionNuLegacy string -//go:embed completion/next/ps/task.ps1 -var completionPowershellNext string +//go:embed completion/legacy/ps/task.ps1 +var completionPowershellLegacy string -//go:embed completion/next/zsh/_task -var completionZshNext string +//go:embed completion/legacy/zsh/_task +var completionZshLegacy string // The maps accept `nushell` as an alias of `nu`. var completionScripts = map[string]string{ @@ -48,21 +50,21 @@ var completionScripts = map[string]string{ "zsh": completionZsh, } -var completionScriptsNext = map[string]string{ - "bash": completionBashNext, - "fish": completionFishNext, - "nu": completionNuNext, - "nushell": completionNuNext, - "powershell": completionPowershellNext, - "zsh": completionZshNext, +var completionScriptsLegacy = map[string]string{ + "bash": completionBashLegacy, + "fish": completionFishLegacy, + "nu": completionNuLegacy, + "nushell": completionNuLegacy, + "powershell": completionPowershellLegacy, + "zsh": completionZshLegacy, } func Completion(shell string) (string, error) { return completionScript(completionScripts, shell) } -func CompletionNext(shell string) (string, error) { - return completionScript(completionScriptsNext, shell) +func LegacyCompletion(shell string) (string, error) { + return completionScript(completionScriptsLegacy, shell) } func completionScript(scripts map[string]string, shell string) (string, error) { diff --git a/completion/bash/task.bash b/completion/bash/task.bash index 60e807aa..e1e2a3b8 100644 --- a/completion/bash/task.bash +++ b/completion/bash/task.bash @@ -1,60 +1,94 @@ # vim: set tabstop=2 shiftwidth=2 expandtab: +# +# Thin wrapper around `task __complete`: all suggestion logic lives in the Go engine. -_GO_TASK_COMPLETION_LIST_OPTION='--list-all' TASK_CMD="${TASK_EXE:-task}" -function _task() -{ - local cur prev words cword - _init_completion -n : || return +# `=` stays inside the current word (see `_init_completion -n =:`), so an inline +# `--flag=` prefix must be stripped before _filedir and re-applied after. +_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 +} - # Check for `--` within command-line and quit or strip suffix. - local i - for i in "${!words[@]}"; do - 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 +_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 KEEP_ORDER=32 + + # `=` and `:` out of the word breaks: `--output=`, `docs:serve` stay one token. + _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 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 + + # Not `compgen -W`: it splits the word list on IFS, mangling values with spaces. + local value + COMPREPLY=() + for line in ${lines[@]+"${lines[@]}"}; do + value="${line%%$'\t'*}" + if [[ -z "$cur" || "$value" == "$cur"* ]]; then + COMPREPLY+=( "$value" ) fi done - # Handle special arguments of options. - 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 + if (( directive & NO_SPACE )); then + compopt -o nospace 2>/dev/null + fi - # Handle normal options. - case "$cur" in - -*) - COMPREPLY=( $( compgen -W "$(_parse_help $1)" -- $cur ) ) - return 0 - ;; - esac + # nosort needs bash 4.4; the 3.2 shipped by macOS ignores it and stays sorted. + if (( directive & KEEP_ORDER )); then + compopt -o nosort 2>/dev/null + fi - # 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" + + if (( ${#COMPREPLY[@]} == 0 )) && ! (( directive & NO_FILE_COMP )); then + _task_filedir + fi } complete -F _task "$TASK_CMD" diff --git a/completion/fish/task.fish b/completion/fish/task.fish index 5fd9382c..908f0897 100644 --- a/completion/fish/task.fish +++ b/completion/fish/task.fish @@ -1,116 +1,98 @@ +# Thin wrapper around `task __complete`: all suggestion logic lives in the Go engine. + 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) -set -g __task_experiments_cache "" -set -g __task_experiments_cache_time 0 +# Completion directives, mirroring internal/complete/complete.go. `math` has no +# bitwise operators, hence __task_test_bit. NoSpace (2) and KeepOrder (32) need +# none: fish appends no space and keeps the order. +set -g __task_directive_no_file_comp 4 +set -g __task_directive_filter_file_ext 8 +set -g __task_directive_filter_dirs 16 -# Helper function to get experiments with 1-second cache -function __task_get_experiments --inherit-variable GO_TASK_PROGNAME - set -l now (date +%s) - 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 - end - - # Refresh cache - set -g __task_experiments_cache ($GO_TASK_PROGNAME --experiments 2>/dev/null) - set -g __task_experiments_cache_time $now - printf '%s\n' $__task_experiments_cache +function __task_test_bit --argument-names value bit + test (math "floor($value / $bit) % 2") -eq 1 end -# 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 - if test "_$arg" = "_--global" -o "_$arg" = "_-g" - set global_task true - break - end +function __task_complete --inherit-variable GO_TASK_PROGNAME + set -l tokens (commandline -opc) + set -l current (commandline -ct) + set -l args + if test (count $tokens) -gt 1 + set args $tokens[2..-1] end + set args $args $current - # 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 + set -l output ($GO_TASK_PROGNAME __complete $args 2>/dev/null) + set -l count (count $output) + if test $count -eq 0 return end - # Grab names and descriptions (if any) of the tasks - 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) - if test $output - echo $output + set -l last $output[$count] + if not string match -q ':*' -- $last + # Protocol violation: emit raw lines as a fallback. + printf '%s\n' $output + return + end + + set -l directive (string replace -r '^:' '' -- $last) + set -l data + if test $count -gt 1 + set data $output[1..(math $count - 1)] + end + + # The registration below passes `--no-files`, so every file-completion + # directive must be served here or nothing is offered at all. + + # fish replaces the whole token, so an inline `--flag=` must be kept on every + # candidate. + set -l flagpfx "" + set -l pathcur $current + if string match -qr '^--?[^=]+=' -- $current + set flagpfx (string replace -r '=.*$' '=' -- $current) + set pathcur (string replace -r '^--?[^=]+=' '' -- $current) + end + + # __fish_complete_suffix prioritizes the extension instead of filtering. + if __task_test_bit $directive $__task_directive_filter_file_ext + for entry in (__fish_complete_path $pathcur) + set -l name (string split -f1 \t -- $entry) + if string match -qr '/$' -- $name + printf '%s%s\n' $flagpfx $entry + continue + end + for ext in $data + if string match -qr "\.$ext\$" -- $name + printf '%s%s\n' $flagpfx $entry + break + end + end + end + return + end + + if __task_test_bit $directive $__task_directive_filter_dirs + for entry in (__fish_complete_directories $pathcur) + printf '%s%s\n' $flagpfx $entry + end + return + end + + for line in $data + printf '%s\n' $line + end + + # NoFileComp unset → offer files too (DirectiveDefault). + if not __task_test_bit $directive $__task_directive_no_file_comp + for entry in (__fish_complete_path $pathcur) + printf '%s%s\n' $flagpfx $entry + end end end -complete -c $GO_TASK_PROGNAME \ - -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.' \ - -xa "(__task_get_tasks)" \ - -n "not __fish_seen_subcommand_from --" +# fish accumulates `complete` entries instead of replacing them, so an older +# completion would keep contributing alongside the engine. +complete -c $GO_TASK_PROGNAME -e -# 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 nu" -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' -complete -c $GO_TASK_PROGNAME -l offline -d 'use only local or cached Taskfiles' -complete -c $GO_TASK_PROGNAME -l timeout -d 'timeout for remote Taskfile downloads' -complete -c $GO_TASK_PROGNAME -l expiry -d 'cache expiry duration' -complete -c $GO_TASK_PROGNAME -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)" -complete -c $GO_TASK_PROGNAME -l cacert -d 'custom CA certificate for TLS' -r -complete -c $GO_TASK_PROGNAME -l cert -d 'client certificate for mTLS' -r -complete -c $GO_TASK_PROGNAME -l cert-key -d 'client certificate private key' -r -complete -c $GO_TASK_PROGNAME -l download -d 'download remote Taskfile' -complete -c $GO_TASK_PROGNAME -l clear-cache -d 'clear remote Taskfile cache' - -# 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' +# `--no-files` keeps fish from mixing in files against the engine's directive. +complete -c $GO_TASK_PROGNAME --no-files -a "(__task_complete)" diff --git a/completion/legacy/bash/task.bash b/completion/legacy/bash/task.bash new file mode 100644 index 00000000..60e807aa --- /dev/null +++ b/completion/legacy/bash/task.bash @@ -0,0 +1,60 @@ +# vim: set tabstop=2 shiftwidth=2 expandtab: + +_GO_TASK_COMPLETION_LIST_OPTION='--list-all' +TASK_CMD="${TASK_EXE:-task}" + +function _task() +{ + local cur prev words cword + _init_completion -n : || return + + # Check for `--` within command-line and quit or strip suffix. + local i + for i in "${!words[@]}"; do + 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 + done + + # Handle special arguments of options. + 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. + case "$cur" in + -*) + 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" +} + +complete -F _task "$TASK_CMD" diff --git a/completion/legacy/fish/task.fish b/completion/legacy/fish/task.fish new file mode 100644 index 00000000..5fd9382c --- /dev/null +++ b/completion/legacy/fish/task.fish @@ -0,0 +1,116 @@ +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) +set -g __task_experiments_cache "" +set -g __task_experiments_cache_time 0 + +# Helper function to get experiments with 1-second cache +function __task_get_experiments --inherit-variable GO_TASK_PROGNAME + set -l now (date +%s) + 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 + end + + # Refresh cache + set -g __task_experiments_cache ($GO_TASK_PROGNAME --experiments 2>/dev/null) + set -g __task_experiments_cache_time $now + printf '%s\n' $__task_experiments_cache +end + +# 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 + 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 + end + + # Grab names and descriptions (if any) of the tasks + 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) + if test $output + echo $output + end +end + +complete -c $GO_TASK_PROGNAME \ + -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.' \ + -xa "(__task_get_tasks)" \ + -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 nu" +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' +complete -c $GO_TASK_PROGNAME -l offline -d 'use only local or cached Taskfiles' +complete -c $GO_TASK_PROGNAME -l timeout -d 'timeout for remote Taskfile downloads' +complete -c $GO_TASK_PROGNAME -l expiry -d 'cache expiry duration' +complete -c $GO_TASK_PROGNAME -l remote-cache-dir -d 'directory to cache remote Taskfiles' -xa "(__fish_complete_directories)" +complete -c $GO_TASK_PROGNAME -l cacert -d 'custom CA certificate for TLS' -r +complete -c $GO_TASK_PROGNAME -l cert -d 'client certificate for mTLS' -r +complete -c $GO_TASK_PROGNAME -l cert-key -d 'client certificate private key' -r +complete -c $GO_TASK_PROGNAME -l download -d 'download remote Taskfile' +complete -c $GO_TASK_PROGNAME -l clear-cache -d 'clear remote Taskfile cache' + +# 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' diff --git a/completion/legacy/nu/task-completions.nu b/completion/legacy/nu/task-completions.nu new file mode 100644 index 00000000..63148f19 --- /dev/null +++ b/completion/legacy/nu/task-completions.nu @@ -0,0 +1,180 @@ +# Nushell completions for Task (https://taskfile.dev). +# +# Nushell cannot source a script from stdin, so save this file where Nushell +# picks it up automatically: +# mkdir ($nu.data-dir | path join "vendor/autoload") +# task --completion nu | save --force ($nu.data-dir | path join "vendor/autoload/task-completions.nu") +# +# The file must not be named task.nu: Nushell refuses to export a known external +# named like its module, which would break `use task-completions.nu *`. + +# Name or path of the Task executable, like the other completion scripts. +# The *completed* command is always `task`: an `extern` declaration requires a +# literal name. For a renamed executable, alias it instead: `alias go-task = task`. +def "nu-complete task-exe" [] { + $env.TASK_EXE? | default "task" +} + +def "nu-complete task-words" [context: string] { + $context | split row --regex '\s+' | where {|word| $word != "" } +} + +# Strips the quotes the user may have typed around a value and expands `~`, +# which Nushell does not do for a value coming from a variable. +def "nu-complete task-value" [value: string] { + let unquoted = ($value | str trim --char '"' | str trim --char "'") + if ($unquoted | str starts-with "~") { + $unquoted | path expand --no-symlink + } else { + $unquoted + } +} + +# Rebuilds the flags deciding *which* Taskfile is read, so the task list follows +# the `-t/--taskfile`, `-d/--dir` and `-g/--global` already on the command line. +def "nu-complete task-scope" [words: list] { + mut scope: list = [] + mut pending = "" + + for word in ($words | skip 1) { + if $pending != "" { + $scope = ($scope | append [$pending, (nu-complete task-value $word)]) + $pending = "" + continue + } + + let parts = ($word | split row "=") + let name = ($parts | first) + let inline = if ($parts | length) > 1 { $parts | skip 1 | str join "=" } else { null } + + if $name in ["-g", "--global"] { + $scope = ($scope | append "--global") + } else if $name in ["-t", "--taskfile", "-d", "--dir"] { + let long = if $name in ["-t", "--taskfile"] { "--taskfile" } else { "--dir" } + if $inline != null { + $scope = ($scope | append [$long, (nu-complete task-value $inline)]) + } else { + $pending = $long + } + } + } + + $scope +} + +# Lists the tasks of the targeted Taskfile. `--no-status` keeps completion fast: +# without it Task fingerprints every task's sources on each keystroke. Returns an +# empty list when Task exits non-zero (no Taskfile, invalid Taskfile). +def "nu-complete task-list" [words: list] { + let exe = (nu-complete task-exe) + let args = [...(nu-complete task-scope $words) "--list-all" "--json" "--no-status"] + let result = (try { do { ^$exe ...$args } | complete } catch { null }) + + if ($result | is-empty) or $result.exit_code != 0 { + return [] + } + + try { $result.stdout | from json | get tasks } catch { [] } +} + +def "nu-complete task" [context: string] { + let words = (nu-complete task-words $context) + + # Words after `--` are forwarded to the task as CLI_ARGS: stop offering task + # names and let Nushell fall back to its own file completion. + if "--" in $words { + return null + } + + let completions = ( + nu-complete task-list $words + | each {|item| + # `task` is the invocable name; `name` may be a display-only label. + let name = ($item.task | str trim --right --char ':') + let desc = ($item.desc? | default "") + let aliases = ( + $item.aliases? + | default [] + | each {|alias| { + value: ($alias | str trim --right --char ':') + description: (if ($desc | is-empty) { $"alias of ($name)" } else { $"($desc) \(alias of ($name)\)" }) + } } + ) + [{ value: $name, description: $desc }] | append $aliases + } + | flatten + ) + + # `sort: false` keeps the order Task chose, which honours --sort and .taskrc. + { options: { sort: false }, completions: $completions } +} + +def "nu-complete task-shells" [] { + ["bash", "zsh", "fish", "powershell", "nu"] +} + +def "nu-complete task-output" [] { + ["interleaved", "group", "prefixed"] +} + +def "nu-complete task-sort" [] { + ["default", "alphanumeric", "none"] +} + +# 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. +# +# An `extern` signature is static, so the experimental flag at the bottom is +# always offered; Task rejects it when the experiment is off. Run +# `task --experiments` to see which experiments are enabled. +export extern "task" [ + ...tasks: string@"nu-complete task" # task(s) to run + --list(-l) # list tasks with a description + --list-all(-a) # list all tasks, with or without a description + --json(-j) # format the task list as JSON + --no-status # ignore status when listing tasks as JSON + --nested # nest namespaces when listing tasks as JSON + --sort: string@"nu-complete task-sort" # change the order of the tasks when listed + --init(-i) # create a new Taskfile.yml in the current folder + --completion: string@"nu-complete task-shells" # generate a shell completion script + --taskfile(-t): glob # choose which Taskfile to run + --dir(-d): directory # set the directory in which Task will execute + --global(-g) # run the global Taskfile from $HOME + --temp-dir: directory # directory used to store Task temporary files + --force(-f) # force execution even when the task is up-to-date + --status # exit with a non-zero code if tasks are not up-to-date + --dry(-n) # compile and print the tasks without executing them + --summary # show the summary of a task instead of running it + --watch(-w) # watch the given tasks and re-run them on changes + --interval(-I): string # interval to watch for changes, e.g. 500ms + --parallel(-p) # run the tasks given on the command line in parallel + --concurrency(-C): int # limit the number of tasks run concurrently + --failfast(-F) # when running in parallel, stop everything if one task fails + --exit-code(-x) # pass through the exit code of the task command + --interactive # prompt for missing required variables + --yes(-y) # assume "yes" as the answer to all prompts + --output(-o): string@"nu-complete task-output" # set the output style + --output-group-begin: string # message template printed before a task's grouped output + --output-group-end: string # message template printed after a task's grouped output + --output-group-error-only # swallow the output of successful tasks + --color(-c) # colored output, enabled by default + --silent(-s) # disable echoing + --verbose(-v) # enable verbose mode + --disable-fuzzy # disable fuzzy matching for task names + --download # download a cached version of a remote Taskfile + --offline # only use local or cached Taskfiles + --clear-cache # clear the remote Taskfile cache + --trusted-hosts: string # trusted hosts for remote Taskfiles (comma-separated) + --timeout: string # timeout for downloading remote Taskfiles + --expiry: string # expiry duration for cached remote Taskfiles + --remote-cache-dir: directory # directory used to cache remote Taskfiles + --cacert: path # custom CA certificate for HTTPS connections + --cert: path # client certificate for HTTPS connections + --cert-key: path # client certificate key for HTTPS connections + --insecure # allow Taskfiles to be downloaded over insecure connections + --experiments # list the available experiments and whether they are enabled + --version # show the Task version + --help(-h) # show Task usage + + --force-all # [GENTLE_FORCE] force the called task and all its dependencies +] diff --git a/completion/legacy/ps/task.ps1 b/completion/legacy/ps/task.ps1 new file mode 100644 index 00000000..dd5ed32c --- /dev/null +++ b/completion/legacy/ps/task.ps1 @@ -0,0 +1,89 @@ +using namespace System.Management.Automation + +$cmdNames = @('task') + (Get-Alias -Definition task,task.exe,*\task,*\task.exe -ErrorAction SilentlyContinue).Name | Select-Object -Unique + +Register-ArgumentCompleter -CommandName $cmdNames -ScriptBlock { + param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) + + if ($commandName.StartsWith('-')) { + $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'), + [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles'), + [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout'), + [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry'), + [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory'), + [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate'), + [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate'), + [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key'), + [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile'), + [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache') + ) + + # Experimental flags (dynamically added based on enabled experiments) + $experiments = & task --experiments 2>$null | Out-String + + if ($experiments -match '\* GENTLE_FORCE:.*on') { + $completions += [CompletionResult]::new('--force-all', '--force-all', [CompletionResultType]::ParameterName, 'force all dependencies') + } + + return $completions.Where{ $_.CompletionText.StartsWith($commandName) } + } + + return $(task --list-all --silent) | Where-Object { $_.StartsWith($commandName) } | ForEach-Object { return $_ + " " } +} diff --git a/completion/legacy/zsh/_task b/completion/legacy/zsh/_task new file mode 100755 index 00000000..cd3e43a9 --- /dev/null +++ b/completion/legacy/zsh/_task @@ -0,0 +1,158 @@ +#compdef task +typeset -A opt_args +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() { + local -a standard_args operation_args + + standard_args=( + '(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' + '(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' + '(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]' + '(-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 nu)' + '(-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]' + '(--offline --clear-cache)--download[download remote Taskfile]' + '(--offline --download)--offline[use only local or cached Taskfiles]' + '(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: ' + '(--expiry)--expiry[cache expiry duration]:duration: ' + '(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs' + '(--cacert)--cacert[custom CA certificate for TLS]:file:_files' + '(--cert)--cert[client certificate for mTLS]:file:_files' + '(--cert-key)--cert-key[client certificate private key]:file:_files' + ) + + # Experimental flags (dynamically added based on enabled experiments) + # Options (modify behavior) + if __task_is_experiment_enabled "GENTLE_FORCE"; then + standard_args+=('(--force-all)--force-all[force execution of task and all dependencies]') + fi + + operation_args=( + # Task names completion (can be specified multiple times) + '(operation)*: :__task_list' + # 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]' + '(* --download)--clear-cache[clear remote Taskfile cache]' + ) + + _arguments -S $standard_args $operation_args +} + +# don't run the completion function when being source-ed or eval-ed +if [ "$funcstack[1]" = "_task" ]; then + _task "$@" +fi diff --git a/completion/next/bash/task.bash b/completion/next/bash/task.bash deleted file mode 100644 index e1e2a3b8..00000000 --- a/completion/next/bash/task.bash +++ /dev/null @@ -1,94 +0,0 @@ -# vim: set tabstop=2 shiftwidth=2 expandtab: -# -# Thin wrapper around `task __complete`: all suggestion logic lives in the Go engine. - -TASK_CMD="${TASK_EXE:-task}" - -# `=` stays inside the current word (see `_init_completion -n =:`), so an inline -# `--flag=` prefix must be stripped before _filedir and re-applied after. -_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 KEEP_ORDER=32 - - # `=` and `:` out of the word breaks: `--output=`, `docs:serve` stay one token. - _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 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 - - # Not `compgen -W`: it splits the word list on IFS, mangling values with spaces. - 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 - - # nosort needs bash 4.4; the 3.2 shipped by macOS ignores it and stays sorted. - if (( directive & KEEP_ORDER )); then - compopt -o nosort 2>/dev/null - fi - - __ltrim_colon_completions "$cur" - - if (( ${#COMPREPLY[@]} == 0 )) && ! (( directive & NO_FILE_COMP )); then - _task_filedir - fi -} - -complete -F _task "$TASK_CMD" diff --git a/completion/next/fish/task.fish b/completion/next/fish/task.fish deleted file mode 100644 index 908f0897..00000000 --- a/completion/next/fish/task.fish +++ /dev/null @@ -1,98 +0,0 @@ -# Thin wrapper around `task __complete`: all suggestion logic lives in the Go engine. - -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. `math` has no -# bitwise operators, hence __task_test_bit. NoSpace (2) and KeepOrder (32) need -# none: fish appends no space and keeps the order. -set -g __task_directive_no_file_comp 4 -set -g __task_directive_filter_file_ext 8 -set -g __task_directive_filter_dirs 16 - -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) - set -l args - if test (count $tokens) -gt 1 - set args $tokens[2..-1] - end - set args $args $current - - set -l output ($GO_TASK_PROGNAME __complete $args 2>/dev/null) - set -l count (count $output) - if test $count -eq 0 - return - end - - set -l last $output[$count] - if not string match -q ':*' -- $last - # Protocol violation: emit raw lines as a fallback. - printf '%s\n' $output - return - end - - set -l directive (string replace -r '^:' '' -- $last) - set -l data - if test $count -gt 1 - set data $output[1..(math $count - 1)] - end - - # The registration below passes `--no-files`, so every file-completion - # directive must be served here or nothing is offered at all. - - # fish replaces the whole token, so an inline `--flag=` must be kept on every - # candidate. - set -l flagpfx "" - set -l pathcur $current - if string match -qr '^--?[^=]+=' -- $current - set flagpfx (string replace -r '=.*$' '=' -- $current) - set pathcur (string replace -r '^--?[^=]+=' '' -- $current) - end - - # __fish_complete_suffix prioritizes the extension instead of filtering. - if __task_test_bit $directive $__task_directive_filter_file_ext - for entry in (__fish_complete_path $pathcur) - set -l name (string split -f1 \t -- $entry) - if string match -qr '/$' -- $name - printf '%s%s\n' $flagpfx $entry - continue - end - for ext in $data - if string match -qr "\.$ext\$" -- $name - printf '%s%s\n' $flagpfx $entry - break - end - end - end - return - end - - if __task_test_bit $directive $__task_directive_filter_dirs - for entry in (__fish_complete_directories $pathcur) - printf '%s%s\n' $flagpfx $entry - end - return - end - - for line in $data - printf '%s\n' $line - end - - # NoFileComp unset → offer files too (DirectiveDefault). - if not __task_test_bit $directive $__task_directive_no_file_comp - for entry in (__fish_complete_path $pathcur) - printf '%s%s\n' $flagpfx $entry - end - end -end - -# fish accumulates `complete` entries instead of replacing them, so an older -# completion would keep contributing alongside the engine. -complete -c $GO_TASK_PROGNAME -e - -# `--no-files` keeps fish from mixing in files against the engine's directive. -complete -c $GO_TASK_PROGNAME --no-files -a "(__task_complete)" diff --git a/completion/next/nu/task-completions.nu b/completion/next/nu/task-completions.nu deleted file mode 100644 index 25fd56ab..00000000 --- a/completion/next/nu/task-completions.nu +++ /dev/null @@ -1,86 +0,0 @@ -# Thin wrapper around `task __complete`: all suggestion logic lives in the Go engine. - -# The `{completions, options}` record documented for `def` completers is -# rejected for an external one: return records or null, nothing else. -def task-external-completer [spans: list] { - let exe = ($env.TASK_EXE? | default "task") - - # The trailing empty word tells the engine the cursor is on a fresh word. - let words = ($spans | skip 1) - let args = (if ($words | is-empty) { [""] } else { $words }) - let current = ($args | last) - - # `complete` keeps stderr off the prompt; a missing binary raises, hence `try`. - let result = (try { do { ^$exe "__complete" ...$args } | complete } catch { null }) - if ($result | is-empty) or $result.exit_code != 0 { - return null - } - - let lines = ($result.stdout | lines) - let last = ($lines | last) - # Protocol violation: offer nothing rather than garbage. - if ($last | is-empty) or (not ($last | str starts-with ":")) { - return null - } - let directive = (try { $last | str substring 1.. | into int } catch { 0 }) - let data = ($lines | drop 1) - - # Completion directives, mirroring internal/complete/complete.go. NoSpace (2) - # and KeepOrder (32) need none: no space is appended, order is kept. - let no_file_comp = (($directive | bits and 4) != 0) - let filter_file_ext = (($directive | bits and 8) != 0) - let filter_dirs = (($directive | bits and 16) != 0) - - # Nushell replaces the whole token, so an inline `--flag=` must be re-applied. - let inline = ($current | parse --regex '^(?--?[^=]+=)(?.*)$') - let flag_prefix = (if ($inline | is-empty) { "" } else { $inline.0.flag }) - let path_arg = (if ($inline | is-empty) { $current } else { $inline.0.path }) - - if $filter_file_ext or $filter_dirs { - # `into glob` turns the literal path into a pattern; matching nothing raises. - let entries = (try { ls ($"($path_arg)*" | into glob) } catch { [] }) - let matched = (if $filter_file_ext { - $entries | where {|entry| $entry.type == "dir" or ($entry.name | path parse | get extension) in $data } - } else { - $entries | where type == "dir" - }) - return ($matched | each {|entry| - # Without a trailing separator a second matches the dir again. - let name = (if $entry.type == "dir" { $"($entry.name)(char path_sep)" } else { $entry.name }) - { value: $"($flag_prefix)($name)" } - }) - } - - # Nushell does not filter an external completer's results. - let candidates = ($data - | each {|line| - let parts = ($line | split row --number 2 "\t") - let value = ($parts | first) - if ($parts | length) > 1 { { value: $value, description: ($parts | last) } } else { { value: $value } } - } - | where {|candidate| $candidate.value | str starts-with --ignore-case $current }) - - if ($candidates | is-empty) and (not $no_file_comp) { - return null - } - - $candidates -} - -# Nushell shares one external completer between every command, so chain to the -# installed one instead of breaking every other tool. -let task_previous_completer = ($env.config.completions.external.completer? | default null) - -$env.config.completions.external.completer = {|spans| - let exe = ($env.TASK_EXE? | default "task") - # Compare basenames so `./task`, `/usr/local/bin/task` and `task.exe` match. - let head = ($spans | first | path basename | str replace --regex '(?i)\.exe$' '') - let name = ($exe | path basename | str replace --regex '(?i)\.exe$' '') - if $head == $name { - task-external-completer $spans - } else if $task_previous_completer != null { - do $task_previous_completer $spans - } else { - null - } -} diff --git a/completion/next/ps/task.ps1 b/completion/next/ps/task.ps1 deleted file mode 100644 index 6f19e87c..00000000 --- a/completion/next/ps/task.ps1 +++ /dev/null @@ -1,109 +0,0 @@ -using namespace System.Management.Automation -using namespace System.Management.Automation.Language - -# Thin wrapper around `task __complete`: all suggestion logic lives in the Go engine. - -$cmdNames = @('task') + (Get-Alias -Definition task,task.exe,*\task,*\task.exe -ErrorAction SilentlyContinue).Name | Select-Object -Unique - -Register-ArgumentCompleter -Native -CommandName $cmdNames -ScriptBlock { - param($wordToComplete, $commandAst, $cursorPosition) - - $TaskExe = if ($env:TASK_EXE) { $env:TASK_EXE } else { 'task' } - - # The current word arrives with the quote the user opened. - $current = $wordToComplete - if ($current.Length -ge 1 -and ($current[0] -eq '"' -or $current[0] -eq "'")) { - $quoteChar = $current[0] - $current = $current.Substring(1) - if ($current.EndsWith($quoteChar)) { - $current = $current.Substring(0, $current.Length - 1) - } - } - - # A string element yields its Value, so `--dir "a b"` arrives unquoted. - $argsToPass = @() - $elements = $commandAst.CommandElements - for ($i = 1; $i -lt $elements.Count; $i++) { - $el = $elements[$i] - if ($el.Extent.StartOffset -ge $cursorPosition) { break } - $argsToPass += if ($el -is [StringConstantExpressionAst] -or $el -is [ExpandableStringExpressionAst]) { - $el.Value - } else { - $el.ToString() - } - } - # The trailing word tells the engine the cursor is on a fresh word. - if ($argsToPass.Count -eq 0 -or $argsToPass[-1] -ne $current) { - $argsToPass += $current - } - - $output = & $TaskExe __complete @argsToPass 2>$null - if (-not $output) { return } - - $lines = @($output) - $last = $lines[-1] - if (-not $last.StartsWith(':')) { return } - - $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 - - # PowerShell replaces the whole token, so the flag and directory prefix must - # be prepended back to every candidate. - $flagPrefix = '' - $pathArg = $current - if ($current -match '^(--?[^=]+=)(.*)$') { - $flagPrefix = $Matches[1] - $pathArg = $Matches[2] - } - $pathPrefix = $flagPrefix + ($pathArg -replace '[^\\/]*$', '') - - # DirectiveNoSpace cannot be honored: CompletionResult has no per-item "no - # trailing space" option, so `VAR=` gets one anyway. - - # The text replaces the token as-is, so a value holding a space must be quoted. - $asCompletionText = { - param($text) - if ($text -match '[\s'']') { "'" + $text.Replace("'", "''") + "'" } else { $text } - } - - $asPathResult = { - param($item) - $type = if ($item.PSIsContainer) { [CompletionResultType]::ProviderContainer } else { [CompletionResultType]::ProviderItem } - [CompletionResult]::new((& $asCompletionText "$pathPrefix$($item.Name)"), $item.Name, $type, $item.Name) - } - - # Directories are kept so the user can descend. `-Include` needs `-Recurse`. - if ($directive -band $FilterFileExt) { - $exts = $data | ForEach-Object { ".$_" } - return Get-ChildItem -Path "$pathArg*" -ErrorAction SilentlyContinue | - Where-Object { $_.PSIsContainer -or $exts -contains $_.Extension } | - ForEach-Object { & $asPathResult $_ } - } - - if ($directive -band $FilterDirs) { - return Get-ChildItem -Path "$pathArg*" -Directory -ErrorAction SilentlyContinue | - ForEach-Object { & $asPathResult $_ } - } - - # PowerShell does not filter native argument-completer results itself. - $results = @($data | ForEach-Object { - $parts = $_ -split "`t", 2 - $value = $parts[0] - if ($current -and -not $value.StartsWith($current, [System.StringComparison]::OrdinalIgnoreCase)) { return } - $desc = if ($parts.Count -gt 1 -and $parts[1]) { $parts[1] } else { $value } - [CompletionResult]::new((& $asCompletionText $value), $value, [CompletionResultType]::ParameterValue, $desc) - }) - - # NoFileComp unset and nothing matched → DirectiveDefault, so offer files. - if ($results.Count -eq 0 -and -not ($directive -band $NoFileComp)) { - return Get-ChildItem -Path "$pathArg*" -ErrorAction SilentlyContinue | - ForEach-Object { & $asPathResult $_ } - } - - return $results -} diff --git a/completion/next/zsh/_task b/completion/next/zsh/_task deleted file mode 100755 index 107dea8f..00000000 --- a/completion/next/zsh/_task +++ /dev/null @@ -1,76 +0,0 @@ -#compdef task -# -# Thin wrapper around `task __complete`: all suggestion logic lives in the Go engine. - -TASK_CMD="${TASK_EXE:-task}" - -_task() { - local -a args lines completions describe_opts compadd_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 - - # `-T` is true when the style is unset, so a flag goes out only when it is off. - zstyle -T ":completion:${curcontext}:" show-aliases || ctl+=(--no-aliases) - zstyle -T ":completion:${curcontext}:" verbose || ctl+=(--no-descriptions) - - # (@) preserves the trailing empty word the engine reads as a fresh cursor. - args=("${(@)words[2,CURRENT]}") - (( ${#args} == 0 )) && args=("") - - output=$("$TASK_CMD" __complete "${ctl[@]}" "${args[@]}" 2>/dev/null) - if [[ -z "$output" ]]; then - _files - return - fi - - lines=("${(f)output}") - directive="${lines[-1]#:}" - lines=("${(@)lines[1,-2]}") - - if (( directive & FILTER_FILE_EXT )); then - local -a globs - for line in "${lines[@]}"; do - globs+=("*.${line}") - done - # Inline `--flag=` into IPREFIX so file completion runs on the value. Only - # here: globally it would break `_describe` on inline enums. - compset -P '*=' - _files -g "(${(j:|:)globs})" - return - fi - - if (( directive & FILTER_DIRS )); then - compset -P '*=' - _path_files -/ - return - fi - - # _describe splits on the first unescaped colon: "docs:serve" → "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 - - # -S is a compadd option, passed after the array; -V belongs to _describe. - # In the compadd zone it would take the next argument as a group name. - (( directive & NO_SPACE )) && compadd_opts+=(-S '') - (( directive & KEEP_ORDER )) && describe_opts+=(-V) - - if (( ${#completions} > 0 )); then - _describe "${describe_opts[@]}" -t tasks 'task' completions "${compadd_opts[@]}" - fi - - (( directive & NO_FILE_COMP )) && return - compset -P '*=' - _files -} - -compdef _task "$TASK_CMD" diff --git a/completion/nu/task-completions.nu b/completion/nu/task-completions.nu index 63148f19..25fd56ab 100644 --- a/completion/nu/task-completions.nu +++ b/completion/nu/task-completions.nu @@ -1,180 +1,86 @@ -# Nushell completions for Task (https://taskfile.dev). -# -# Nushell cannot source a script from stdin, so save this file where Nushell -# picks it up automatically: -# mkdir ($nu.data-dir | path join "vendor/autoload") -# task --completion nu | save --force ($nu.data-dir | path join "vendor/autoload/task-completions.nu") -# -# The file must not be named task.nu: Nushell refuses to export a known external -# named like its module, which would break `use task-completions.nu *`. +# Thin wrapper around `task __complete`: all suggestion logic lives in the Go engine. -# Name or path of the Task executable, like the other completion scripts. -# The *completed* command is always `task`: an `extern` declaration requires a -# literal name. For a renamed executable, alias it instead: `alias go-task = task`. -def "nu-complete task-exe" [] { - $env.TASK_EXE? | default "task" -} +# The `{completions, options}` record documented for `def` completers is +# rejected for an external one: return records or null, nothing else. +def task-external-completer [spans: list] { + let exe = ($env.TASK_EXE? | default "task") -def "nu-complete task-words" [context: string] { - $context | split row --regex '\s+' | where {|word| $word != "" } -} - -# Strips the quotes the user may have typed around a value and expands `~`, -# which Nushell does not do for a value coming from a variable. -def "nu-complete task-value" [value: string] { - let unquoted = ($value | str trim --char '"' | str trim --char "'") - if ($unquoted | str starts-with "~") { - $unquoted | path expand --no-symlink - } else { - $unquoted - } -} - -# Rebuilds the flags deciding *which* Taskfile is read, so the task list follows -# the `-t/--taskfile`, `-d/--dir` and `-g/--global` already on the command line. -def "nu-complete task-scope" [words: list] { - mut scope: list = [] - mut pending = "" - - for word in ($words | skip 1) { - if $pending != "" { - $scope = ($scope | append [$pending, (nu-complete task-value $word)]) - $pending = "" - continue - } - - let parts = ($word | split row "=") - let name = ($parts | first) - let inline = if ($parts | length) > 1 { $parts | skip 1 | str join "=" } else { null } - - if $name in ["-g", "--global"] { - $scope = ($scope | append "--global") - } else if $name in ["-t", "--taskfile", "-d", "--dir"] { - let long = if $name in ["-t", "--taskfile"] { "--taskfile" } else { "--dir" } - if $inline != null { - $scope = ($scope | append [$long, (nu-complete task-value $inline)]) - } else { - $pending = $long - } - } - } - - $scope -} - -# Lists the tasks of the targeted Taskfile. `--no-status` keeps completion fast: -# without it Task fingerprints every task's sources on each keystroke. Returns an -# empty list when Task exits non-zero (no Taskfile, invalid Taskfile). -def "nu-complete task-list" [words: list] { - let exe = (nu-complete task-exe) - let args = [...(nu-complete task-scope $words) "--list-all" "--json" "--no-status"] - let result = (try { do { ^$exe ...$args } | complete } catch { null }) + # The trailing empty word tells the engine the cursor is on a fresh word. + let words = ($spans | skip 1) + let args = (if ($words | is-empty) { [""] } else { $words }) + let current = ($args | last) + # `complete` keeps stderr off the prompt; a missing binary raises, hence `try`. + let result = (try { do { ^$exe "__complete" ...$args } | complete } catch { null }) if ($result | is-empty) or $result.exit_code != 0 { - return [] - } - - try { $result.stdout | from json | get tasks } catch { [] } -} - -def "nu-complete task" [context: string] { - let words = (nu-complete task-words $context) - - # Words after `--` are forwarded to the task as CLI_ARGS: stop offering task - # names and let Nushell fall back to its own file completion. - if "--" in $words { return null } - let completions = ( - nu-complete task-list $words - | each {|item| - # `task` is the invocable name; `name` may be a display-only label. - let name = ($item.task | str trim --right --char ':') - let desc = ($item.desc? | default "") - let aliases = ( - $item.aliases? - | default [] - | each {|alias| { - value: ($alias | str trim --right --char ':') - description: (if ($desc | is-empty) { $"alias of ($name)" } else { $"($desc) \(alias of ($name)\)" }) - } } - ) - [{ value: $name, description: $desc }] | append $aliases + let lines = ($result.stdout | lines) + let last = ($lines | last) + # Protocol violation: offer nothing rather than garbage. + if ($last | is-empty) or (not ($last | str starts-with ":")) { + return null + } + let directive = (try { $last | str substring 1.. | into int } catch { 0 }) + let data = ($lines | drop 1) + + # Completion directives, mirroring internal/complete/complete.go. NoSpace (2) + # and KeepOrder (32) need none: no space is appended, order is kept. + let no_file_comp = (($directive | bits and 4) != 0) + let filter_file_ext = (($directive | bits and 8) != 0) + let filter_dirs = (($directive | bits and 16) != 0) + + # Nushell replaces the whole token, so an inline `--flag=` must be re-applied. + let inline = ($current | parse --regex '^(?--?[^=]+=)(?.*)$') + let flag_prefix = (if ($inline | is-empty) { "" } else { $inline.0.flag }) + let path_arg = (if ($inline | is-empty) { $current } else { $inline.0.path }) + + if $filter_file_ext or $filter_dirs { + # `into glob` turns the literal path into a pattern; matching nothing raises. + let entries = (try { ls ($"($path_arg)*" | into glob) } catch { [] }) + let matched = (if $filter_file_ext { + $entries | where {|entry| $entry.type == "dir" or ($entry.name | path parse | get extension) in $data } + } else { + $entries | where type == "dir" + }) + return ($matched | each {|entry| + # Without a trailing separator a second matches the dir again. + let name = (if $entry.type == "dir" { $"($entry.name)(char path_sep)" } else { $entry.name }) + { value: $"($flag_prefix)($name)" } + }) + } + + # Nushell does not filter an external completer's results. + let candidates = ($data + | each {|line| + let parts = ($line | split row --number 2 "\t") + let value = ($parts | first) + if ($parts | length) > 1 { { value: $value, description: ($parts | last) } } else { { value: $value } } } - | flatten - ) + | where {|candidate| $candidate.value | str starts-with --ignore-case $current }) - # `sort: false` keeps the order Task chose, which honours --sort and .taskrc. - { options: { sort: false }, completions: $completions } + if ($candidates | is-empty) and (not $no_file_comp) { + return null + } + + $candidates } -def "nu-complete task-shells" [] { - ["bash", "zsh", "fish", "powershell", "nu"] +# Nushell shares one external completer between every command, so chain to the +# installed one instead of breaking every other tool. +let task_previous_completer = ($env.config.completions.external.completer? | default null) + +$env.config.completions.external.completer = {|spans| + let exe = ($env.TASK_EXE? | default "task") + # Compare basenames so `./task`, `/usr/local/bin/task` and `task.exe` match. + let head = ($spans | first | path basename | str replace --regex '(?i)\.exe$' '') + let name = ($exe | path basename | str replace --regex '(?i)\.exe$' '') + if $head == $name { + task-external-completer $spans + } else if $task_previous_completer != null { + do $task_previous_completer $spans + } else { + null + } } - -def "nu-complete task-output" [] { - ["interleaved", "group", "prefixed"] -} - -def "nu-complete task-sort" [] { - ["default", "alphanumeric", "none"] -} - -# 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. -# -# An `extern` signature is static, so the experimental flag at the bottom is -# always offered; Task rejects it when the experiment is off. Run -# `task --experiments` to see which experiments are enabled. -export extern "task" [ - ...tasks: string@"nu-complete task" # task(s) to run - --list(-l) # list tasks with a description - --list-all(-a) # list all tasks, with or without a description - --json(-j) # format the task list as JSON - --no-status # ignore status when listing tasks as JSON - --nested # nest namespaces when listing tasks as JSON - --sort: string@"nu-complete task-sort" # change the order of the tasks when listed - --init(-i) # create a new Taskfile.yml in the current folder - --completion: string@"nu-complete task-shells" # generate a shell completion script - --taskfile(-t): glob # choose which Taskfile to run - --dir(-d): directory # set the directory in which Task will execute - --global(-g) # run the global Taskfile from $HOME - --temp-dir: directory # directory used to store Task temporary files - --force(-f) # force execution even when the task is up-to-date - --status # exit with a non-zero code if tasks are not up-to-date - --dry(-n) # compile and print the tasks without executing them - --summary # show the summary of a task instead of running it - --watch(-w) # watch the given tasks and re-run them on changes - --interval(-I): string # interval to watch for changes, e.g. 500ms - --parallel(-p) # run the tasks given on the command line in parallel - --concurrency(-C): int # limit the number of tasks run concurrently - --failfast(-F) # when running in parallel, stop everything if one task fails - --exit-code(-x) # pass through the exit code of the task command - --interactive # prompt for missing required variables - --yes(-y) # assume "yes" as the answer to all prompts - --output(-o): string@"nu-complete task-output" # set the output style - --output-group-begin: string # message template printed before a task's grouped output - --output-group-end: string # message template printed after a task's grouped output - --output-group-error-only # swallow the output of successful tasks - --color(-c) # colored output, enabled by default - --silent(-s) # disable echoing - --verbose(-v) # enable verbose mode - --disable-fuzzy # disable fuzzy matching for task names - --download # download a cached version of a remote Taskfile - --offline # only use local or cached Taskfiles - --clear-cache # clear the remote Taskfile cache - --trusted-hosts: string # trusted hosts for remote Taskfiles (comma-separated) - --timeout: string # timeout for downloading remote Taskfiles - --expiry: string # expiry duration for cached remote Taskfiles - --remote-cache-dir: directory # directory used to cache remote Taskfiles - --cacert: path # custom CA certificate for HTTPS connections - --cert: path # client certificate for HTTPS connections - --cert-key: path # client certificate key for HTTPS connections - --insecure # allow Taskfiles to be downloaded over insecure connections - --experiments # list the available experiments and whether they are enabled - --version # show the Task version - --help(-h) # show Task usage - - --force-all # [GENTLE_FORCE] force the called task and all its dependencies -] diff --git a/completion/ps/task.ps1 b/completion/ps/task.ps1 index dd5ed32c..6f19e87c 100644 --- a/completion/ps/task.ps1 +++ b/completion/ps/task.ps1 @@ -1,89 +1,109 @@ using namespace System.Management.Automation +using namespace System.Management.Automation.Language + +# Thin wrapper around `task __complete`: all suggestion logic lives in the Go engine. $cmdNames = @('task') + (Get-Alias -Definition task,task.exe,*\task,*\task.exe -ErrorAction SilentlyContinue).Name | Select-Object -Unique -Register-ArgumentCompleter -CommandName $cmdNames -ScriptBlock { - param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) +Register-ArgumentCompleter -Native -CommandName $cmdNames -ScriptBlock { + param($wordToComplete, $commandAst, $cursorPosition) - if ($commandName.StartsWith('-')) { - $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'), - [CompletionResult]::new('--offline', '--offline', [CompletionResultType]::ParameterName, 'use cached Taskfiles'), - [CompletionResult]::new('--timeout', '--timeout', [CompletionResultType]::ParameterName, 'download timeout'), - [CompletionResult]::new('--expiry', '--expiry', [CompletionResultType]::ParameterName, 'cache expiry'), - [CompletionResult]::new('--remote-cache-dir', '--remote-cache-dir', [CompletionResultType]::ParameterName, 'cache directory'), - [CompletionResult]::new('--cacert', '--cacert', [CompletionResultType]::ParameterName, 'custom CA certificate'), - [CompletionResult]::new('--cert', '--cert', [CompletionResultType]::ParameterName, 'client certificate'), - [CompletionResult]::new('--cert-key', '--cert-key', [CompletionResultType]::ParameterName, 'client private key'), - [CompletionResult]::new('--download', '--download', [CompletionResultType]::ParameterName, 'download remote Taskfile'), - [CompletionResult]::new('--clear-cache', '--clear-cache', [CompletionResultType]::ParameterName, 'clear cache') - ) + $TaskExe = if ($env:TASK_EXE) { $env:TASK_EXE } else { 'task' } - # Experimental flags (dynamically added based on enabled experiments) - $experiments = & task --experiments 2>$null | Out-String - - if ($experiments -match '\* GENTLE_FORCE:.*on') { - $completions += [CompletionResult]::new('--force-all', '--force-all', [CompletionResultType]::ParameterName, 'force all dependencies') + # The current word arrives with the quote the user opened. + $current = $wordToComplete + if ($current.Length -ge 1 -and ($current[0] -eq '"' -or $current[0] -eq "'")) { + $quoteChar = $current[0] + $current = $current.Substring(1) + if ($current.EndsWith($quoteChar)) { + $current = $current.Substring(0, $current.Length - 1) } - - return $completions.Where{ $_.CompletionText.StartsWith($commandName) } } - return $(task --list-all --silent) | Where-Object { $_.StartsWith($commandName) } | ForEach-Object { return $_ + " " } + # A string element yields its Value, so `--dir "a b"` arrives unquoted. + $argsToPass = @() + $elements = $commandAst.CommandElements + for ($i = 1; $i -lt $elements.Count; $i++) { + $el = $elements[$i] + if ($el.Extent.StartOffset -ge $cursorPosition) { break } + $argsToPass += if ($el -is [StringConstantExpressionAst] -or $el -is [ExpandableStringExpressionAst]) { + $el.Value + } else { + $el.ToString() + } + } + # The trailing word tells the engine the cursor is on a fresh word. + if ($argsToPass.Count -eq 0 -or $argsToPass[-1] -ne $current) { + $argsToPass += $current + } + + $output = & $TaskExe __complete @argsToPass 2>$null + if (-not $output) { return } + + $lines = @($output) + $last = $lines[-1] + if (-not $last.StartsWith(':')) { return } + + $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 + + # PowerShell replaces the whole token, so the flag and directory prefix must + # be prepended back to every candidate. + $flagPrefix = '' + $pathArg = $current + if ($current -match '^(--?[^=]+=)(.*)$') { + $flagPrefix = $Matches[1] + $pathArg = $Matches[2] + } + $pathPrefix = $flagPrefix + ($pathArg -replace '[^\\/]*$', '') + + # DirectiveNoSpace cannot be honored: CompletionResult has no per-item "no + # trailing space" option, so `VAR=` gets one anyway. + + # The text replaces the token as-is, so a value holding a space must be quoted. + $asCompletionText = { + param($text) + if ($text -match '[\s'']') { "'" + $text.Replace("'", "''") + "'" } else { $text } + } + + $asPathResult = { + param($item) + $type = if ($item.PSIsContainer) { [CompletionResultType]::ProviderContainer } else { [CompletionResultType]::ProviderItem } + [CompletionResult]::new((& $asCompletionText "$pathPrefix$($item.Name)"), $item.Name, $type, $item.Name) + } + + # Directories are kept so the user can descend. `-Include` needs `-Recurse`. + if ($directive -band $FilterFileExt) { + $exts = $data | ForEach-Object { ".$_" } + return Get-ChildItem -Path "$pathArg*" -ErrorAction SilentlyContinue | + Where-Object { $_.PSIsContainer -or $exts -contains $_.Extension } | + ForEach-Object { & $asPathResult $_ } + } + + if ($directive -band $FilterDirs) { + return Get-ChildItem -Path "$pathArg*" -Directory -ErrorAction SilentlyContinue | + ForEach-Object { & $asPathResult $_ } + } + + # PowerShell does not filter native argument-completer results itself. + $results = @($data | ForEach-Object { + $parts = $_ -split "`t", 2 + $value = $parts[0] + if ($current -and -not $value.StartsWith($current, [System.StringComparison]::OrdinalIgnoreCase)) { return } + $desc = if ($parts.Count -gt 1 -and $parts[1]) { $parts[1] } else { $value } + [CompletionResult]::new((& $asCompletionText $value), $value, [CompletionResultType]::ParameterValue, $desc) + }) + + # NoFileComp unset and nothing matched → DirectiveDefault, so offer files. + if ($results.Count -eq 0 -and -not ($directive -band $NoFileComp)) { + return Get-ChildItem -Path "$pathArg*" -ErrorAction SilentlyContinue | + ForEach-Object { & $asPathResult $_ } + } + + return $results } diff --git a/completion/tests/wrapper.bash b/completion/tests/wrapper.bash index 5ea17454..8fc5c172 100755 --- a/completion/tests/wrapper.bash +++ b/completion/tests/wrapper.bash @@ -22,7 +22,7 @@ _filedir() { CAP+="filedir:$* cur=$cur"$'\n'; } compopt() { CAP+="compopt:$*"$'\n'; } __ltrim_colon_completions() { :; } -source "$(dirname "${BASH_SOURCE[0]}")/../next/bash/task.bash" +source "$(dirname "${BASH_SOURCE[0]}")/../bash/task.bash" run() { CAP="" diff --git a/completion/tests/wrapper.fish b/completion/tests/wrapper.fish index e1a24e55..51eb3173 100755 --- a/completion/tests/wrapper.fish +++ b/completion/tests/wrapper.fish @@ -3,7 +3,7 @@ # Set up by run.sh: TASK_FIXTURE, and `task` on PATH = the binary under test. cd $TASK_FIXTURE -source (dirname (status -f))/../next/fish/task.fish +source (dirname (status -f))/../fish/task.fish set -g fails 0 diff --git a/completion/tests/wrapper.nu b/completion/tests/wrapper.nu index ed47a4c0..019cfa56 100644 --- a/completion/tests/wrapper.nu +++ b/completion/tests/wrapper.nu @@ -4,7 +4,7 @@ # Set up by run.sh: $env.TASK_FIXTURE, and `task` on PATH = the binary under test. # `source` needs a parse-time constant path. -const TASK_NU = (path self "../next/nu/task-completions.nu") +const TASK_NU = (path self "../nu/task-completions.nu") # Installed before the wrapper is sourced, to assert the delegation path. $env.config.completions.external.completer = {|spans| [{ value: $"prev:($spans | first)" }] } diff --git a/completion/tests/wrapper.ps1 b/completion/tests/wrapper.ps1 index 7d896e00..62429e1d 100644 --- a/completion/tests/wrapper.ps1 +++ b/completion/tests/wrapper.ps1 @@ -3,7 +3,7 @@ # the binary under test. Set-Location $env:TASK_FIXTURE -. "$PSScriptRoot/../next/ps/task.ps1" +. "$PSScriptRoot/../ps/task.ps1" $fails = 0 diff --git a/completion/tests/wrapper.zsh b/completion/tests/wrapper.zsh index 820261e4..d52f1a36 100755 --- a/completion/tests/wrapper.zsh +++ b/completion/tests/wrapper.zsh @@ -29,7 +29,7 @@ _files() { CAP+="files:$*"$'\n' } _path_files() { CAP+="path_files:$*"$'\n' } # Sourcing avoids the autoload first-call quirk; `compdef` is stubbed above. -source ${0:A:h}/../next/zsh/_task +source ${0:A:h}/../zsh/_task run() { CAP="" diff --git a/completion/zsh/_task b/completion/zsh/_task index cd3e43a9..107dea8f 100755 --- a/completion/zsh/_task +++ b/completion/zsh/_task @@ -1,158 +1,76 @@ #compdef task -typeset -A opt_args +# +# Thin wrapper around `task __complete`: all suggestion logic lives in the Go engine. + 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() { - local -a standard_args operation_args + local -a args lines completions describe_opts compadd_opts ctl + local output directive line - standard_args=( - '(-C --concurrency)'{-C,--concurrency}'[limit number of concurrent tasks]: ' - '(-p --parallel)'{-p,--parallel}'[run command-line tasks in parallel]' - '(-F --failfast)'{-F,--failfast}'[when running tasks in parallel, stop all tasks if one fails]' - '(-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 nu)' - '(-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]' - '(--offline --clear-cache)--download[download remote Taskfile]' - '(--offline --download)--offline[use only local or cached Taskfiles]' - '(--timeout)--timeout[timeout for remote Taskfile downloads]:duration: ' - '(--expiry)--expiry[cache expiry duration]:duration: ' - '(--remote-cache-dir)--remote-cache-dir[directory to cache remote Taskfiles]:cache dir:_dirs' - '(--cacert)--cacert[custom CA certificate for TLS]:file:_files' - '(--cert)--cert[client certificate for mTLS]:file:_files' - '(--cert-key)--cert-key[client certificate private key]:file:_files' - ) + # 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 - # Experimental flags (dynamically added based on enabled experiments) - # Options (modify behavior) - if __task_is_experiment_enabled "GENTLE_FORCE"; then - standard_args+=('(--force-all)--force-all[force execution of task and all dependencies]') - fi + # `-T` is true when the style is unset, so a flag goes out only when it is off. + zstyle -T ":completion:${curcontext}:" show-aliases || ctl+=(--no-aliases) + zstyle -T ":completion:${curcontext}:" verbose || ctl+=(--no-descriptions) - operation_args=( - # Task names completion (can be specified multiple times) - '(operation)*: :__task_list' - # 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]' - '(* --download)--clear-cache[clear remote Taskfile cache]' - ) + # (@) preserves the trailing empty word the engine reads as a fresh cursor. + args=("${(@)words[2,CURRENT]}") + (( ${#args} == 0 )) && args=("") - _arguments -S $standard_args $operation_args + output=$("$TASK_CMD" __complete "${ctl[@]}" "${args[@]}" 2>/dev/null) + if [[ -z "$output" ]]; then + _files + return + fi + + lines=("${(f)output}") + directive="${lines[-1]#:}" + lines=("${(@)lines[1,-2]}") + + if (( directive & FILTER_FILE_EXT )); then + local -a globs + for line in "${lines[@]}"; do + globs+=("*.${line}") + done + # Inline `--flag=` into IPREFIX so file completion runs on the value. Only + # here: globally it would break `_describe` on inline enums. + compset -P '*=' + _files -g "(${(j:|:)globs})" + return + fi + + if (( directive & FILTER_DIRS )); then + compset -P '*=' + _path_files -/ + return + fi + + # _describe splits on the first unescaped colon: "docs:serve" → "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 + + # -S is a compadd option, passed after the array; -V belongs to _describe. + # In the compadd zone it would take the next argument as a group name. + (( directive & NO_SPACE )) && compadd_opts+=(-S '') + (( directive & KEEP_ORDER )) && describe_opts+=(-V) + + if (( ${#completions} > 0 )); then + _describe "${describe_opts[@]}" -t tasks 'task' completions "${compadd_opts[@]}" + fi + + (( directive & NO_FILE_COMP )) && return + compset -P '*=' + _files } -# don't run the completion function when being source-ed or eval-ed -if [ "$funcstack[1]" = "_task" ]; then - _task "$@" -fi +compdef _task "$TASK_CMD" diff --git a/internal/complete/complete_test.go b/internal/complete/complete_test.go index 390978bc..43d9657e 100644 --- a/internal/complete/complete_test.go +++ b/internal/complete/complete_test.go @@ -29,7 +29,7 @@ func newTestFlagSet() *pflag.FlagSet { fs.StringVar(&s, "sort", "", "Sort order") fs.StringVar(&s, "cacert", "", "CA cert path") fs.StringVar(&s, "completion", "", "Generate a completion script") - fs.StringVar(&s, "new-completion", "", "Generate a completion script") + fs.StringVar(&s, "legacy-completion", "", "Generate a completion script") return fs } @@ -419,7 +419,7 @@ func TestNeedsTaskfile_StdinEntrypoint(t *testing.T) { func TestCompletionShells(t *testing.T) { t.Parallel() - for _, flag := range []string{"--completion", "--new-completion"} { + for _, flag := range []string{"--completion", "--legacy-completion"} { suggs, dir := complete.Complete(nil, newTestFlagSet(), []string{flag, ""}, complete.Options{}) require.Equal(t, complete.DirectiveNoFileComp, dir) require.NotEmpty(t, suggs) @@ -427,7 +427,7 @@ func TestCompletionShells(t *testing.T) { for _, shell := range values(suggs) { _, err := task.Completion(shell) require.NoErrorf(t, err, "%s offers %q", flag, shell) - _, err = task.CompletionNext(shell) + _, err = task.LegacyCompletion(shell) require.NoErrorf(t, err, "%s offers %q", flag, shell) } } diff --git a/internal/complete/flags.go b/internal/complete/flags.go index ca70b3a9..42ba30b8 100644 --- a/internal/complete/flags.go +++ b/internal/complete/flags.go @@ -12,10 +12,10 @@ var completionShells = []string{"bash", "zsh", "fish", "powershell", "nu"} // 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": completionShells, - "new-completion": completionShells, + "output": {"interleaved", "group", "prefixed"}, + "sort": {"default", "alphanumeric", "none"}, + "completion": completionShells, + "legacy-completion": completionShells, } // A flag absent here falls back to the shell's default file completion. diff --git a/internal/flags/flags.go b/internal/flags/flags.go index ccefcc52..d09df3ad 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -49,7 +49,7 @@ var ( Help bool Init bool Completion string - NewCompletion string + LegacyCompletion string List bool ListAll bool ListJson bool @@ -126,7 +126,7 @@ func init() { pflag.BoolVarP(&Help, "help", "h", false, "Shows Task usage.") pflag.BoolVarP(&Init, "init", "i", false, "Creates a new Taskfile.yml in the current folder.") pflag.StringVar(&Completion, "completion", "", "Generates shell completion script.") - pflag.StringVar(&NewCompletion, "new-completion", "", "Generates the new (experimental) shell completion script, powered by the `task __complete` engine.") + pflag.StringVar(&LegacyCompletion, "legacy-completion", "", "Generates the pre-engine shell completion script. Deprecated: use --completion.") pflag.BoolVarP(&List, "list", "l", false, "Lists tasks with description of current Taskfile.") pflag.BoolVarP(&ListAll, "list-all", "a", false, "Lists tasks with or without a description.") pflag.BoolVarP(&ListJson, "json", "j", false, "Formats task list as JSON.") diff --git a/website/src/next/docs/installation.md b/website/src/next/docs/installation.md index f7422f87..989f6435 100644 --- a/website/src/next/docs/installation.md +++ b/website/src/next/docs/installation.md @@ -369,8 +369,14 @@ go tool task {arguments...} Some installation methods will automatically install completions too, but if this isn't working for you or your chosen method doesn't include them, you can run `task --completion ` to output a completion script for any supported -shell. There are a couple of ways these completions can be added to your shell -config: +shell. + +Every shell shares a single source of truth: the script is a thin wrapper that +asks the `task` binary itself what to suggest, so Bash, Zsh, Fish, Nushell and +PowerShell all offer the same task names, aliases, flags, flag values and +`requires` vars. + +There are a couple of ways these completions can be added to your shell config: ### Option 1. Load the completions in your shell's startup config (Recommended) @@ -468,73 +474,10 @@ to an autoload directory. Option 1 rewrites it at every startup, which keeps it in sync with the installed version of Task — the refreshed completions are picked up by the next shell. With option 2, re-run the command after upgrading Task. -The completions are attached to an `extern "task"` declaration, which Nushell -requires to be static. Three consequences are worth knowing: - -- The experimental flags (`--force-all`, `--download`, `--offline`, …) are always - offered, even when the corresponding experiment is disabled. Their description - is prefixed with the experiment name, and `task --experiments` lists the ones - that are enabled. -- Passing a value to a boolean flag with `=` does not work: Nushell forwards - `--color=false` as two arguments, so Task reads `false` as a task name. Use - `NO_COLOR=1`, or bypass the declaration with `^task --color=false`. -- `TASK_EXE` selects the executable that is run, but not the command name the - completions are attached to, which is always `task`. For a renamed executable, - alias it instead: - -```nu -use ($nu.data-dir | path join "vendor/autoload/task-completions.nu") * -alias go-task = task -``` - -### Trying the new completion engine (experimental) - -Task is migrating to a new completion engine, where every shell shares a single -source of truth: the `task __complete` command. This gives Bash, Zsh, Fish, -Nushell and PowerShell the exact same suggestions (task names, aliases, flags, -flag values and `requires` vars, including their enums). It is currently -**opt-in** and will become the default of `--completion` in a future release. - -To try it, swap `--completion` for `--new-completion` in any of the snippets -above, for example: - -::: code-group - -```shell [bash] -# ~/.bashrc -eval "$(task --new-completion bash)" -``` - -```shell [zsh] -# ~/.zshrc -eval "$(task --new-completion zsh)" -``` - -```shell [fish] -# ~/.config/fish/config.fish -task --new-completion fish | source -``` - -```powershell [powershell] -# $PROFILE\Microsoft.PowerShell_profile.ps1 -Invoke-Expression (&task --new-completion powershell | Out-String) -``` - -```nu [nushell] -# ~/.config/nushell/config.nu -mkdir ($nu.data-dir | path join "vendor/autoload") -task --new-completion nu | save --force ($nu.data-dir | path join "vendor/autoload/task-completions.nu") -``` - -::: - -The `verbose` and `show-aliases` zstyles documented above work with the new Zsh -completion too. - Nushell shares a single external completer between every command, so the script -chains to the one already configured — carapace and friends keep working. Load -it from an autoload directory as shown above rather than from `config.nu`, so -that your own completer is the one being chained to. If you would rather wire it +chains to the one already configured — carapace and friends keep working. Load it +from an autoload directory as shown above rather than from `config.nu`, so that +your own completer is the one being chained to. If you would rather wire it yourself, the script also exposes a `task-external-completer` command: ```nu @@ -549,3 +492,11 @@ $env.config.completions.external.completer = {|spans| Two engine directives behave differently under Nushell by design: it never appends a space after an external completion (so `NoSpace` is a no-op) and never re-sorts the results (so `KeepOrder` is always honoured). + +### Legacy completion scripts + +Before the engine, every shell carried its own hand-written completion script, +each with its own idea of what to suggest. Those scripts are still shipped and +available through `task --legacy-completion `, as an escape hatch should +the engine misbehave in your setup. They are deprecated, will not receive further +fixes, and will be removed in a future release.