diff --git a/README.md b/README.md
index 0f86468..43a4ef2 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,7 @@ shims, all work from your `PATH` — including full MSBuild projects and, with
- [Commands](#commands)
- [Building drivers (WDK)](#building-drivers-wdk)
- [Language](#language)
+- [Shell completion](#shell-completion)
- [Using clang-cl/lld-link instead of Wine](#using-clang-cllld-link-instead-of-wine)
- [Building from source](#building-from-source)
- [How the pieces fit together](#how-the-pieces-fit-together)
@@ -103,6 +104,7 @@ vintner install (i) [dir] wire up wrappe
vintner env (e) --bin
/bin/ print INCLUDE/LIB for native clang-cl/lld-link use
vintner version (v) print the version
vintner help (h) print usage
+vintner completion bash|zsh print a shell completion script
```
`--dest`/`[dir]` both default to `~/.vintner` when omitted.
@@ -150,6 +152,16 @@ VINTNER_LANG=ru vintner help
Deeper error text bubbled up from internal packages stays in English.
+## Shell completion
+
+```bash
+source <(vintner completion bash) # or add to ~/.bashrc
+source <(vintner completion zsh) # or add to ~/.zshrc
+```
+
+Completes subcommands (including the short aliases), `download`'s flags,
+and directory arguments for `install`/`env --bin`.
+
## Using clang-cl/lld-link instead of Wine
You don't need Wine at all if you drive the (nonredistributable) MSVC/WinSDK
diff --git a/cmd/vintner/completion.go b/cmd/vintner/completion.go
new file mode 100644
index 0000000..13a38b8
--- /dev/null
+++ b/cmd/vintner/completion.go
@@ -0,0 +1,140 @@
+package main
+
+import "fmt"
+
+// runCompletion prints a shell completion script for shell ("bash" or
+// "zsh") to stdout, meant to be sourced directly:
+//
+// source <(vintner completion bash) # or add to ~/.bashrc
+// source <(vintner completion zsh) # or add to ~/.zshrc
+//
+// The flag lists below are hand-maintained alongside download.go/env.go's
+// flag.FlagSet definitions rather than generated from them - there's no
+// reflection-friendly registry to walk, and the flag set rarely changes.
+func runCompletion(args []string) int {
+ if len(args) != 1 {
+ fmt.Println("usage: vintner completion bash|zsh")
+ return 1
+ }
+ switch args[0] {
+ case "bash":
+ fmt.Print(bashCompletionScript)
+ return 0
+ case "zsh":
+ fmt.Print(zshCompletionScript)
+ return 0
+ default:
+ fmt.Printf("vintner completion: unsupported shell %q (want bash or zsh)\n", args[0])
+ return 1
+ }
+}
+
+const downloadFlags = "--dest --cache --major --preview --manifest --accept-license " +
+ "--msvc-version --sdk-version --host-arch --only-host --language " +
+ "--include-optional --skip-recommended --only-download --only-unpack " +
+ "--keep-unpack --skip-patch --list-workloads --list-components " +
+ "--print-deps-tree --with-wdk --architecture --ignore -h --help"
+
+var bashCompletionScript = `# vintner bash completion - eval "$(vintner completion bash)"
+_vintner_complete() {
+ local cur cmd
+ COMPREPLY=()
+ cur="${COMP_WORDS[COMP_CWORD]}"
+ cmd="${COMP_WORDS[1]}"
+
+ if [ "$COMP_CWORD" -eq 1 ]; then
+ COMPREPLY=($(compgen -W "download dl install i env e version v help h completion" -- "$cur"))
+ return 0
+ fi
+
+ case "$cmd" in
+ download|dl)
+ COMPREPLY=($(compgen -W "` + downloadFlags + `" -- "$cur"))
+ ;;
+ install|i)
+ COMPREPLY=($(compgen -d -- "$cur"))
+ ;;
+ env|e)
+ COMPREPLY=($(compgen -W "--bin -h --help" -- "$cur"))
+ ;;
+ completion)
+ COMPREPLY=($(compgen -W "bash zsh" -- "$cur"))
+ ;;
+ esac
+ return 0
+}
+complete -F _vintner_complete vintner
+`
+
+var zshCompletionScript = `#compdef vintner
+# vintner zsh completion - source <(vintner completion zsh)
+
+_vintner() {
+ local -a subcommands
+ subcommands=(
+ 'download:fetch and unpack MSVC/WinSDK/WDK'
+ 'dl:alias for download'
+ 'install:wire up wrappers for a downloaded MSVC'
+ 'i:alias for install'
+ 'env:print INCLUDE/LIB for native clang-cl/lld-link use'
+ 'e:alias for env'
+ 'version:print the version'
+ 'v:alias for version'
+ 'help:print usage'
+ 'h:alias for help'
+ 'completion:print a shell completion script'
+ )
+
+ if (( CURRENT == 2 )); then
+ _describe 'command' subcommands
+ return
+ fi
+
+ case "${words[2]}" in
+ download|dl)
+ local -a flags
+ flags=(
+ '--dest[directory to install into]:directory:_files -/'
+ '--cache[persistent download cache directory]:directory:_files -/'
+ '--major[major VS version]:version:'
+ '--preview[use the preview/insiders channel]'
+ '--manifest[use a predownloaded installer manifest file]:file:_files'
+ '--accept-license[do not prompt for accepting the license]'
+ '--msvc-version[install a specific MSVC toolchain version]:version:'
+ '--sdk-version[install a specific Windows SDK version]:version:'
+ '--host-arch[host architecture]:arch:(x86 x64 arm64)'
+ '--only-host[only download packages matching the host architecture]'
+ '--language[preferred package language]:language:'
+ '--include-optional[include all optional dependencies]'
+ '--skip-recommended[skip recommended dependencies]'
+ '--only-download[stop after downloading package files]'
+ '--only-unpack[unpack without pruning to just the CLI tools]'
+ '--keep-unpack[keep the scratch unpack dir]'
+ '--skip-patch[do not apply the Wine compatibility patches]'
+ '--list-workloads[list available workloads and exit]'
+ '--list-components[list available components and exit]'
+ '--print-deps-tree[print the dependency tree and exit]'
+ '--with-wdk[also fetch the Windows Driver Kit]'
+ '--architecture[target architecture]:arch:(x86 x64 arm arm64 host)'
+ '--ignore[package id to skip]:package id:'
+ '-h[show help]'
+ '--help[show help]'
+ )
+ _arguments $flags
+ ;;
+ install|i)
+ _files -/
+ ;;
+ env|e)
+ _arguments \
+ '--bin[bin/ directory produced by install]:directory:_files -/' \
+ '-h[show help]' '--help[show help]'
+ ;;
+ completion)
+ _values 'shell' bash zsh
+ ;;
+ esac
+}
+
+_vintner "$@"
+`
diff --git a/cmd/vintner/completion_test.go b/cmd/vintner/completion_test.go
new file mode 100644
index 0000000..5bf17fa
--- /dev/null
+++ b/cmd/vintner/completion_test.go
@@ -0,0 +1,44 @@
+package main
+
+import (
+ "os/exec"
+ "strings"
+ "testing"
+)
+
+// TestCompletionScriptsAreSyntacticallyValid catches the easy way to break
+// these: a typo in the hand-maintained flag lists that produces invalid
+// shell syntax. It shells out to bash/zsh -n rather than parsing the script
+// itself, so it's testing exactly what a user's shell would see.
+func TestCompletionScriptsAreSyntacticallyValid(t *testing.T) {
+ for _, tc := range []struct {
+ shell string
+ script string
+ }{
+ {"bash", bashCompletionScript},
+ {"zsh", zshCompletionScript},
+ } {
+ t.Run(tc.shell, func(t *testing.T) {
+ if _, err := exec.LookPath(tc.shell); err != nil {
+ t.Skipf("%s not installed", tc.shell)
+ }
+ cmd := exec.Command(tc.shell, "-n", "/dev/stdin")
+ cmd.Stdin = strings.NewReader(tc.script)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ t.Fatalf("%s -n rejected the completion script: %v\n%s", tc.shell, err, out)
+ }
+ })
+ }
+}
+
+func TestRunCompletionUnknownShell(t *testing.T) {
+ if code := runCompletion([]string{"fish"}); code != 1 {
+ t.Errorf("runCompletion([\"fish\"]) = %d, want 1", code)
+ }
+ if code := runCompletion(nil); code != 1 {
+ t.Errorf("runCompletion(nil) = %d, want 1", code)
+ }
+ if code := runCompletion([]string{"bash", "extra"}); code != 1 {
+ t.Errorf("runCompletion with extra arg = %d, want 1", code)
+ }
+}
diff --git a/cmd/vintner/main.go b/cmd/vintner/main.go
index 5882eef..1541eae 100644
--- a/cmd/vintner/main.go
+++ b/cmd/vintner/main.go
@@ -47,6 +47,8 @@ func runCLI(args []string) int {
return runInstall(args[1:])
case "env", "e":
return runEnv(args[1:])
+ case "completion":
+ return runCompletion(args[1:])
case "version", "v", "--version":
fmt.Println("vintner " + version)
return 0
diff --git a/internal/i18n/i18n.go b/internal/i18n/i18n.go
index 892fc1c..48dd277 100644
--- a/internal/i18n/i18n.go
+++ b/internal/i18n/i18n.go
@@ -74,6 +74,7 @@ Usage:
vintner env (e) --bin print INCLUDE/LIB for native clang-cl/lld-link use
vintner version (v) print the version
vintner help (h) show this message
+ vintner completion bash|zsh print a shell completion script
Run "vintner --help" for that command's own options - download
has many, including --with-wdk, --list-workloads, --list-components and
@@ -81,6 +82,7 @@ has many, including --with-wdk, --list-workloads, --list-components and
--dest/[dir] default to ~/.vintner if omitted.
Language: set VINTNER_LANG=ru (or LANG=ru_RU...) for Russian output.
+Completion: source <(vintner completion bash) # or zsh
Once installed, add /bin/ to PATH and invoke the tools directly:
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr
@@ -94,6 +96,7 @@ Once installed, add /bin/ to PATH and invoke the tools directly:
vintner env (e) --bin вывести INCLUDE/LIB для clang-cl/lld-link напрямую
vintner version (v) показать версию
vintner help (h) показать эту справку
+ vintner completion bash|zsh вывести скрипт автодополнения для оболочки
Запустите «vintner <команда> --help» для параметров конкретной команды —
у download их много, включая --with-wdk, --list-workloads, --list-components
@@ -101,6 +104,7 @@ Once installed, add /bin/ to PATH and invoke the tools directly:
--dest/[каталог] по умолчанию — ~/.vintner.
Язык: установите VINTNER_LANG=en (или LANG=en_US...) для вывода на английском.
+Автодополнение: source <(vintner completion bash) # или zsh
После установки добавьте /bin/ в PATH и вызывайте инструменты напрямую:
cl, link, lib, ml, ml64, mc, midl, mt, rc, dumpbin, msbuild, nmake, armasm, armasm64, cmd, findstr