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