>_EXECUTIVE SUMMARY
zsh-dotnet-completion is a 15-line zsh plugin that wires the official dotnet complete self-hosted completer into zsh's legacy compctl completion system. Microsoft ships completion logic inside the dotnet SDK itself — dotnet complete "$words" returns the candidate list given the current command line, so the plugin's only job is to bridge that output into zsh's reply array. Because the SDK owns the completion table, every new dotnet release (new subcommands, new template names, new package sources) shows up the moment dotnet --version bumps — no plugin update needed. The plugin also conditionally appends $HOME/.dotnet/tools to $PATH (guarded by [[ -d ... ]] so it's a no-op on hosts without the dotnet SDK installed), and registers three aliases (dt=test, dr=run, dcl=clean) that match Microsoft's documented developer-workflow shortcuts. Smallest plugin in the family by source size, largest test-to-source ratio: 39 zunit @test cases / 15 LOC = 2.6 tests per line.
dt/dr/dcl)~ARCHITECTURE · FILE STRUCTURE
One file. Four distinct surfaces in 15 lines: PATH augmentation (guarded), callback function, alias registration, compctl binding.
| Path | Lines | Role |
|---|---|---|
| zsh-dotnet-completion.plugin.zsh | 15 | The whole plugin — [[ -d $HOME/.dotnet/tools ]] guard + export PATH=...; _dotnet_zsh_complete() function calling dotnet complete "$words"; aliases dt / dcl / dr; compctl -K _dotnet_zsh_complete dotnet binding |
| src/ | 0 | No #compdef file — this plugin uses zsh's legacy compctl system, not the modern compsys _arguments framework. Microsoft's official completer drives candidate selection, so a static _arguments table would only duplicate (and drift from) the SDK |
| Total source | 15 | One file, no completion-system #compdef file |
@COMPLETION COVERAGE
Microsoft owns the completion table; the plugin is the bridge. Whatever dotnet complete returns shows up in the menu, line-split on \n.
Subcommand completion (live)
Top-level subcommands — new, build, run, test, restore, add, remove, publish, pack, clean, nuget, workload, tool, etc. — come straight from dotnet complete "$words". New SDK adds a subcommand: it works immediately, no plugin bump.
Template completion
dotnet new <TAB> lists every template the SDK knows about (console, classlib, web, webapi, blazor, maui, custom templates installed via dotnet new install, etc.). The SDK queries its template engine; this plugin just forwards the result.
Package completion
dotnet add package <TAB> — the SDK's completer queries configured NuGet sources. Plugin doesn't talk to NuGet directly, so private feeds (configured via ~/.nuget/NuGet/NuGet.Config) Just Work without per-feed plumbing.
Flag & configuration completion
dotnet build -c <TAB> → Debug / Release. dotnet test --framework <TAB> → net6.0 / net8.0 / etc. — the SDK's completer reads the actual .csproj / global.json in the current directory, so completion is project-aware, not generic.
Aliases (3)
Workflow shortcuts matching Microsoft's documented developer flow:
dt→dotnet testdr→dotnet rundcl→dotnet clean
PATH augmentation
$HOME/.dotnet/tools is where dotnet tool install --global ... drops binaries. The plugin appends (not prepends) it to $PATH only if the directory exists — silent no-op on hosts without dotnet, never shadows /usr/local/bin on hosts with it.
#TEST COVERAGE
39 zunit @test cases across 7 files, plus 21 structural shell gates from the umbrella. Test-to-source ratio is the highest in the family (2.6 tests per LOC) precisely because the plugin's surface is small enough to enumerate every contract.
| Test file | @test count | What it pins |
|---|---|---|
| tests/t-plugin.zsh | 16 | Full plugin contract — $HOME/.dotnet/tools guard, PATH append (NOT prepend) order, callback fn defined, callback delegates to dotnet complete "$words" (the official tool), reply assigned via parameter-expansion split, compctl -K _dotnet_zsh_complete dotnet binding |
| tests/t-contract.zsh | 3 | Entrypoint stem matches plugin-dir basename (Zinit / OMZ source-line copy-paste pin) |
| tests/t-contract2.zsh | 5 | Idempotency under repeated source (alias count stays at 3) |
| tests/t-contract3.zsh | 5 | Each alias body matches exactly its documented expansion |
| tests/t-contract4.zsh | 5 | No accidental global-scope side effects beyond PATH + aliases + compctl |
| tests/t-aliases.zsh | 4 | 3 aliases register, each resolves to the right dotnet subcommand |
| tests/t-syntax.zsh | 1 | zsh -n parse-cleanness on entrypoint |
| Total | 39 | 7 zunit files |
// STRUCTURAL GATES (21 shell scripts)
Same 21-script gate-set the rest of the meta family enforces. Categories:
- docs/*.html gates (9): h1 present, body+html closing, final newline, no deprecated tags, https-only, no inline event handlers (Tauri-CSP-safe), no placeholder hash hrefs,
target=_blank rel=noopener - README.md gates (4): final newline, badges present, h2 sections, https links
- man-page gates (3): final newline, no trailing whitespace, synopsis section
- tests/ gates (2): shell scripts executable, shebang present
- .github/workflows/ gates (2): final newline, no tabs
- cargo gate (1): vacuously passes — no Cargo.toml
?KEY DESIGN DECISIONS
Five tradeoffs in 15 lines of code.
Delegate to dotnet complete
The alternative is hand-authoring an _arguments spec table covering every dotnet subcommand, flag, template, and framework target — thousands of entries that rot the moment Microsoft ships a new SDK. dotnet complete "$words" is the SDK's own completer; the plugin gets free maintenance on every dotnet update.
compctl, not compdef
Most modern zsh plugins use the compsys framework (#compdef + _arguments). This plugin uses the legacy compctl -K API instead because dotnet complete returns a raw newline-separated candidate list — perfectly shaped for compctl's reply-array contract. Wiring that through _describe would add complexity for no behavioral gain.
PATH append, not prepend
$HOME/.dotnet/tools gets appended ($PATH:$HOME/.dotnet/tools), not prepended. Means a stale dotnet-tool can never shadow /usr/local/bin/foo on hosts where both exist. If the user explicitly wants tool precedence, they can re-order in their ~/.zshrc after sourcing.
Guarded PATH
[[ -d "$HOME/.dotnet/tools" ]] wraps the export. On hosts without dotnet installed, the plugin is silent — no phantom PATH entry that slows down every command lookup. The guard is pinned by t-plugin.zsh so a refactor can't remove it accidentally.
Three aliases, not thirty
Only the three most-typed shortcuts get aliases (dt test, dr run, dcl clean). The plugin doesn't try to alias every dotnet subcommand — that would collide with users' own aliases and the user can always type the full subcommand with completion firing.