// ZSH-CPAN-COMPLETION — ENGINEERING REPORT

Live remote CPAN completion · perl -MCPAN -e 'CPAN::Shell->m(...)' dispatch · _describe-driven menu · _retrieve_cache / _store_cache per-prefix cache · ZPWR_CPAN_MIN_PREFIX overload guard

>_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).

202
Total LOC (src + plugin)
2
#compdef Completion Files
62
zunit @test Cases
21
Structural Gates (.sh)
6
zunit Files
40
Git Commits

~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.zsh79Defines __cpan_single_module, __cpan_multiple_modules, __cpan_modules; sets ZPWR_CPAN_MIN_PREFIX=2; self-locates via ZSH_ARGZERO; prepends src/ to fpath
src/_cpan60#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/_cpanm63#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 source202Three 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.

  • -a autobundle · -c clean · -f force · -F no-lock
  • -i install · -r recompile · -u upgrade-all
  • -l list-installed · -O show-outdated · -J dump-config
  • -p ping-mirrors · -P find-best-mirrors
  • -s shell · -v/-V version · -h help

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.zsh37End-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.zsh7Plugin-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.zsh6Self-locate idiom pin — 0="${${0:#$ZSH_ARGZERO}:-${(%):-%N}}" + 0="${${(M)0:#/*}:-$PWD/$0}" + fpath=("${0:h}/src" $fpath) all present
tests/t-contract3.zsh5Completion-function shape — __cpan_modules takes a prefix arg, __cpan_single_module + __cpan_multiple_modules render via _describe
tests/t-contract4.zsh5Cache invariants — _retrieve_cache / _store_cache are called with prefix-keyed names (cpan_${PREFIX}_cache) and never with bare $PREFIX
tests/t-syntax.zsh2zsh -n parse-cleanness on entrypoint + every src/_* file
Total626 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:


?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.