From 993508c782eefe02fedf4a89294fa62fceef87be Mon Sep 17 00:00:00 2001 From: Valentin Maerten Date: Tue, 11 Aug 2026 20:12:17 +0200 Subject: [PATCH] feat(completion): add Nushell completions (#2966) --- .goreleaser.yml | 3 + CHANGELOG.md | 3 + completion.go | 5 + completion/fish/task.fish | 2 +- completion/nu/task-completions.nu | 180 ++++++++++++++++++++++++++++++ completion/zsh/_task | 2 +- website/src/docs/installation.md | 40 +++++++ 7 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 completion/nu/task-completions.nu diff --git a/.goreleaser.yml b/.goreleaser.yml index 8cfb3928..811b57d1 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -79,6 +79,8 @@ nfpms: dst: /usr/share/fish/vendor_completions.d/task.fish - src: completion/zsh/_task dst: /usr/local/share/zsh/site-functions/_task + - src: completion/nu/task-completions.nu + dst: /usr/share/nushell/vendor/autoload/task-completions.nu brews: - name: go-task @@ -96,6 +98,7 @@ brews: bash_completion.install "completion/bash/task.bash" => "task" zsh_completion.install "completion/zsh/_task" => "_task" fish_completion.install "completion/fish/task.fish" + (share/"nushell/vendor/autoload").install "completion/nu/task-completions.nu" commit_author: name: task-bot email: 106601941+task-bot@users.noreply.github.com diff --git a/CHANGELOG.md b/CHANGELOG.md index 43e4df6f..9902a8b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,9 @@ stopping without a trace (#2240 by @Drino). - Fixed pressing `Esc` at an interactive variable prompt not cancelling the run (#2942 by @anilnatha). +- Added Nushell completions, available via `task --completion nu`. They complete + task names and aliases, every flag with its description, and the values of + `--completion`, `--output` and `--sort` (#2966 by @vmaerten). ## v3.52.0 - 2026-07-02 diff --git a/completion.go b/completion.go index 1ab08e76..ab333b7a 100644 --- a/completion.go +++ b/completion.go @@ -11,6 +11,9 @@ var completionBash string //go:embed completion/fish/task.fish var completionFish string +//go:embed completion/nu/task-completions.nu +var completionNu string + //go:embed completion/ps/task.ps1 var completionPowershell string @@ -24,6 +27,8 @@ func Completion(completion string) (string, error) { return completionBash, nil case "fish": return completionFish, nil + case "nu", "nushell": + return completionNu, nil case "powershell": return completionPowershell, nil case "zsh": diff --git a/completion/fish/task.fish b/completion/fish/task.fish index d4629000..6b3c4e6c 100644 --- a/completion/fish/task.fish +++ b/completion/fish/task.fish @@ -69,7 +69,7 @@ complete -c $GO_TASK_PROGNAME \ 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" +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' diff --git a/completion/nu/task-completions.nu b/completion/nu/task-completions.nu new file mode 100644 index 00000000..29e6ab7d --- /dev/null +++ b/completion/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 flags at the bottom are +# always offered; Task rejects them when the experiment is off. Run +# `task --experiments` to see which ones 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 + --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 + --download # [REMOTE_TASKFILES] download a cached version of a remote Taskfile + --offline # [REMOTE_TASKFILES] only use local or cached Taskfiles + --clear-cache # [REMOTE_TASKFILES] clear the remote Taskfile cache + --trusted-hosts: string # [REMOTE_TASKFILES] trusted hosts for remote Taskfiles (comma-separated) + --timeout: string # [REMOTE_TASKFILES] timeout for downloading remote Taskfiles + --expiry: string # [REMOTE_TASKFILES] expiry duration for cached remote Taskfiles + --remote-cache-dir: directory # [REMOTE_TASKFILES] directory used to cache remote Taskfiles + --cacert: path # [REMOTE_TASKFILES] custom CA certificate for HTTPS connections + --cert: path # [REMOTE_TASKFILES] client certificate for HTTPS connections + --cert-key: path # [REMOTE_TASKFILES] client certificate key for HTTPS connections +] diff --git a/completion/zsh/_task b/completion/zsh/_task index ba163f45..7e3082e7 100755 --- a/completion/zsh/_task +++ b/completion/zsh/_task @@ -94,7 +94,7 @@ _task() { '(-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)' + '(--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]' diff --git a/website/src/docs/installation.md b/website/src/docs/installation.md index a983dc0e..3d3f660c 100644 --- a/website/src/docs/installation.md +++ b/website/src/docs/installation.md @@ -407,6 +407,16 @@ task --completion fish | source Invoke-Expression (&task --completion powershell | Out-String) ``` +```nu [nushell] +# ~/.config/nushell/config.nu +# +# Nushell cannot source a script from stdin, so the script is saved where +# Nushell auto-loads it at startup. Autoload directories are read after +# config.nu, so the completions become available in the next shell. +mkdir ($nu.data-dir | path join "vendor/autoload") +task --completion nu | save --force ($nu.data-dir | path join "vendor/autoload/task-completions.nu") +``` + ::: ### Option 2. Copy the script to your shell's completions directory @@ -428,6 +438,10 @@ task --completion zsh > /usr/local/share/zsh/site-functions/_task task --completion fish > ~/.config/fish/completions/task.fish ``` +```nu [nushell] +task --completion nu | save --force ($nu.data-dir | path join "vendor/autoload/task-completions.nu") +``` + ::: ### Zsh customization @@ -446,3 +460,29 @@ canonical task names, add the `show-aliases` zstyle: ```shell zstyle ':completion:*:*:task:*' show-aliases false ``` + +### Nushell caveats + +Nushell cannot source a script from stdin, so both options above write the script +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 +```