>_EXECUTIVE SUMMARY
zsh-cpan-completion is a zsh completion plugin for the cpan and cpanm Perl module installers. Both completion files (src/_cpan, src/_cpanm) declare full _arguments-driven flag tables, then fall through to a shared module-search dispatcher (__cpan_modules) that shells out to perl -MCPAN -e 'CPAN::Shell->m("/$PREFIX/")' for live remote name resolution. Results pass through zsh's _describe for menu rendering, and through _retrieve_cache / _store_cache for prefix-keyed disk caching — the live MetaCPAN round-trip happens at most once per prefix per shell session. A configurable prefix-length floor (ZPWR_CPAN_MIN_PREFIX, default 2) gates network calls so a stray TAB on an empty prefix doesn't pull the whole CPAN index. The plugin entrypoint uses the standard ZSH_ARGZERO self-locate idiom to find its own src/ directory and prepend it to fpath, so the plugin is install-location-agnostic (Zinit, Oh My Zsh custom, antibody, manual clone — all work).
~ARCHITECTURE · FILE STRUCTURE
Three source files do the entire job. The plugin entrypoint registers the helpers and patches fpath; _cpan and _cpanm declare the #compdef contract that zsh's completion system discovers from fpath.
| Path | Lines | Role |
|---|---|---|
| zsh-cpan-completion.plugin.zsh | 79 | Defines __cpan_single_module, __cpan_multiple_modules, __cpan_modules; sets ZPWR_CPAN_MIN_PREFIX=2; self-locates via ZSH_ARGZERO; prepends src/ to fpath |
| src/_cpan | 60 | #compdef cpan — 30+ short-flag spec lines (-a autobundle, -i install, -u upgrade-all, -l list, -p mirror ping, etc.); fallback to __cpan_modules $PREFIX + .tar.gz/.tgz/.tar.bz2/.zip file completion |
| src/_cpanm | 63 | #compdef cpanm — long+short flag pairs (--install/-i, --force/-f, --sudo/-S, --local-lib/-l, --mirror with _urls binding, --format with literal tree/json/yaml/dists set); same module + tarball fallback |
| Total source | 202 | Three files, single completion namespace |
@COMPLETION COVERAGE
Two binaries, full flag surface, live remote module corpus. Both completion files share the same __cpan_modules dispatcher so a fix in one helper propagates to both call sites.
cpan flag surface
Every short flag from the cpan(1) client. Hand-curated descriptions match the man-page text. Flags requiring module-list args (-A, -C, -D, -g, -G, -i, -t, -x) fall through to ->args state for module-name completion.
-aautobundle ·-cclean ·-fforce ·-Fno-lock-iinstall ·-rrecompile ·-uupgrade-all-llist-installed ·-Oshow-outdated ·-Jdump-config-pping-mirrors ·-Pfind-best-mirrors-sshell ·-v/-Vversion ·-hhelp
cpanm flag surface
Long+short pairs via zsh's {--long,-s} brace expansion, with full mutex declarations: (-v --verbose --quiet -q) blocks --quiet when --verbose is already on. --mirror binds to _urls for URL completion; --format binds to literal set tree/json/yaml/dists.
--install/-i,--force/-f,--notest/-n,--sudo/-S--verbose/-v↔--quiet/-q(mutex pair)--local-lib/-l,--local-lib-contained/-L--mirror:_urls,--format:(tree json yaml dists)--self-upgrade,--info,--installdeps,--look,--reinstall
Module name corpus (live)
perl -MCPAN -e 'CPAN::Shell->m("/'$PREFIX'/")' 2>/dev/null — runs the CPAN.pm shell's m ("module") command with a regex anchored on the user's typed prefix. Output is parsed by line, matching two shapes: single-module verbose output (Module id = ... + CPAN_FILE ...) and multi-module listing (Module < name tarball). Colons in names are backslash-escaped so _describe doesn't split them as separators.
Tarball file completion
Both completion files also offer .tar.gz / .tgz / .tar.bz2 / .zip file matches via _files -/ -g "*.(tar.gz|tgz|tar.bz2|zip)(-.)" — the -/ flag also offers directories (so you can TAB into nested paths) and the (-.) glob qualifier restricts matches to regular files (no symlinks dangling at deleted tarballs).
#TEST COVERAGE
62 zunit @test cases across 6 files, plus 21 structural shell gates that the MenkeTechnologiesMeta umbrella enforces on every submodule's docs/, README.md, tests/, and .github/workflows/.
| Test file | @test count | What it pins |
|---|---|---|
| tests/t-unit.zsh | 37 | End-to-end unit tests — ZPWR_CPAN_MIN_PREFIX=2 contract, fpath contains src/, _cpan and _cpanm files exist + start with #compdef, helper functions defined, parse cleanness under zsh -n |
| tests/t-contract.zsh | 7 | Plugin-contract pins: entrypoint stem matches plugin-dir basename (so zsh-cpan-completion/zsh-cpan-completion.plugin.zsh stays copy-pasteable), every _* file under src/ starts with #compdef |
| tests/t-contract2.zsh | 6 | Self-locate idiom pin — 0="${${0:#$ZSH_ARGZERO}:-${(%):-%N}}" + 0="${${(M)0:#/*}:-$PWD/$0}" + fpath=("${0:h}/src" $fpath) all present |
| tests/t-contract3.zsh | 5 | Completion-function shape — __cpan_modules takes a prefix arg, __cpan_single_module + __cpan_multiple_modules render via _describe |
| tests/t-contract4.zsh | 5 | Cache invariants — _retrieve_cache / _store_cache are called with prefix-keyed names (cpan_${PREFIX}_cache) and never with bare $PREFIX |
| tests/t-syntax.zsh | 2 | zsh -n parse-cleanness on entrypoint + every src/_* file |
| Total | 62 | 6 zunit files |
// STRUCTURAL GATES (21 shell scripts)
The shared umbrella gate-set runs against every submodule. They aren't unit tests — they are contract pins that the repo's static-content surface (docs, README, workflow YAML, test shell shebangs) stays consistent with the meta-repo conventions. Categories:
- docs/*.html gates (9):
docs-has-h1,docs-has-body-tag,docs-has-html-closing,docs-final-newline,docs-no-deprecated-tags,docs-no-http-links(https-only),docs-no-inline-handlers(Tauri-CSP-safe),docs-no-placeholder-href(no placeholder hash-only hrefs),docs-target-blank-rel-noopener(tab-nabbing-safe) - README.md gates (4):
readme-final-newline,readme-has-badges,readme-has-h2-section,readme-has-https-link - man-page gates (3):
man-page-final-newline,man-page-no-trailing-whitespace,man-page-synopsis-section - tests/ gates (2):
tests-shell-executable(mode +x),tests-shell-shebang - .github/workflows/ gates (2):
workflow-final-newline,workflow-no-tabs - cargo gate (1):
cargo-final-newline(vacuously passes — no Cargo.toml in this repo)
?KEY DESIGN DECISIONS
Five places this plugin could have gone the other way, and why it didn't.
Live remote, not static list
Most OMZ-style completions ship a frozen module list inlined as a zsh array literal — cheap, but stale within a week of release. __cpan_modules instead shells out to perl -MCPAN -e 'CPAN::Shell->m("/'$PREFIX'/")' at completion time, so newly-published modules show up the moment CPAN's index sees them. Cost: one Perl interpreter spawn + a network call per prefix per shell session. _retrieve_cache absorbs the cost on subsequent TABs.
ZPWR_CPAN_MIN_PREFIX floor
A TAB on an empty prefix would ask CPAN for the entire module index — tens of thousands of names, several seconds of network, and zsh's menu rendering choking on the result. The plugin requires $#PREFIX >= ZPWR_CPAN_MIN_PREFIX (default 2) before issuing the remote query; below that, only tarball file-completion runs. The env var is user-tunable for slow networks or fast hardware.
Prefix-keyed cache, not session-global
Cache file is cpan_${PREFIX}_cache — one cache entry per typed prefix. Means typing Mo then Moo then Moose hits three separate caches but each is correctly scoped (you don't get Mo* results bleeding into Moose's menu). zsh's _retrieve_cache / _store_cache handle the actual on-disk layout (~/.zcompcache/ by default).
ZSH_ARGZERO self-locate
The entrypoint uses the canonical two-line idiom 0="${${0:#$ZSH_ARGZERO}:-${(%):-%N}}" + 0="${${(M)0:#/*}:-$PWD/$0}" to resolve its own absolute path regardless of how it was loaded (sourced, autoloaded, Zinit-installed under a hashed path). That lets fpath=("${0:h}/src" $fpath) work without any user-supplied $ZSH_CUSTOM-style env var.
Two binaries, one helper namespace
cpan and cpanm have wildly different flag surfaces but identical module-name semantics: type a prefix, get a list of CPAN packages. Both _cpan and _cpanm delegate to the same __cpan_modules/__cpan_single_module/__cpan_multiple_modules trio defined in the plugin entrypoint — one bug fix, two call sites updated.