// ZSH-NGINX — ENGINEERING REPORT

zsh plugin · nginx vhost lifecycle · en/dis/vhost · 2 service aliases (ngt/ngr) · sites-available + sites-enabled completion · 2 vhost templates

>_EXECUTIVE SUMMARY

zsh-nginx is the vhost-lifecycle plugin for the Debian/Ubuntu nginx layout: $NGINX_DIR/sites-available/ stores configs, $NGINX_DIR/sites-enabled/ is a symlink farm pointing into the available tree. Three autoloaded zsh functions (en, dis, vhost) plus two service aliases (ngt for config-test, ngr for restart) cover the standard "create vhost → enable it → reload nginx" loop. Each function ships a paired #compdef completion driven by ls over the relevant sites-* directory — completion candidates are always real on-disk files, never a hand-maintained list that can drift. The vhost generator sed-substitutes {vhost}/{user}/{pool_port} into one of two templates (Symfony2 fastcgi or plain PHP-FPM) and optionally writes an /etc/hosts entry — turning a new vhost into a single command.

246
Plugin LOC
3
Autoload Fns
2
Service Aliases
3
Completions
2
Vhost Templates
28
zunit @test Cases
21
Docs/Repo Gates
28
Git Commits

Plugin Source — 246 LOC

147 autoload fns / 54 templates / 24 completions / 21 entrypoint

Functions (autoload/en, dis, vhost) carry 147 LOC of the 246-LOC total. The vhost file is 102 LOC alone — it embeds its own usage banner, template resolution, getopts argument parsing, and the _write_hosts helper that splices a 127.0.0.1 mapping into /etc/hosts.


#FILE STRUCTURE

Three slices: autoload/ for the user-facing fns, src/ for the matching completions (one #compdef per fn), templates/ for the nginx config skeletons. The entrypoint registers both directories on fpath (autoload + completion) so a single source line covers the full surface.

FileLinesRole
autoload/vhost102Vhost generator + _vhost_usage, _vhost_generate, _write_hosts helpers; getopts -l/-u/-t/-n/-w/-h parser; sed-substitutes {vhost}/{user}/{pool_port} into the chosen template; optional en auto-call + /etc/hosts write
autoload/en25Enable site: ln -s $NGINX_DIR/sites-available/$1 $NGINX_DIR/sites-enabled/$1 with pre-check (must exist in available) and post-check (must materialise in enabled); colorized status output
autoload/dis20Disable site: rm -f $NGINX_DIR/sites-enabled/$1 with the same pre/post-condition contract as en
zsh-nginx.plugin.zsh21Entrypoint: $NGINX_DIR/$NGINX_VHOST_TEMPLATE defaults, sudo probe, ngt/ngr aliases, fpath append for both src/ and autoload/, autoload fn registration
templates/symfony227Symfony2 vhost template: web/ root, app_dev.php front controller, fastcgi-pass over unix:/var/run/php5-fpm.{user}.socket, .ht deny
templates/plain_php27Plain PHP vhost template: index.php root, simple try-files, fastcgi-pass over the same per-user PHP-FPM socket, .ht deny
src/_nginx_en8#compdef en: compadd with output of ls -a $NGINX_DIR/sites-available filtered by awk '/^[a-z][a-z.-]+$/'
src/_nginx_dis8#compdef dis: same shape but listing sites-enabled
src/_nginx_vhost8#compdef vhost: lists $HOME/www/ entries as candidates (the “repos that could become vhosts” set)
9 source files2463 fn + 1 entrypoint + 3 completions + 2 templates

$USER SURFACE

Five callable names. The plugin lives in the user's interactive shell; nothing runs at source time except env-var defaults + the sudo probe + 2 alias definitions + fpath append + autoload -Uz. First call to en/dis/vhost triggers the actual fn body via zsh's autoload machinery.

NameKindEffect
ngtalias$sudo nginx -t — config syntax test
ngralias$sudo service nginx restart — service restart
en NAMEfnSymlink $NGINX_DIR/sites-available/NAME into $NGINX_DIR/sites-enabled/
dis NAMEfnRemove $NGINX_DIR/sites-enabled/NAME
vhost [-l] [-u USER] [-t TPL] [-n] [-w] [-h] NAMEfnGenerate a vhost config from $NGINX_VHOST_TEMPLATE (or -t TPL), drop into sites-available/NAME, optionally en it (-n to skip) and optionally splice NAME into /etc/hosts (-w)

// VHOST FLAGS

FlagDefaultEffect
-l(off)List enabled vhosts (ls $NGINX_DIR/sites-enabled); short-circuits before generation
-u USER$USERSet the owning user for fastcgi socket + web root resolution; looked up in /etc/passwd
-t TPL$NGINX_VHOST_TEMPLATETemplate name (looks in $ZSH/plugins/nginx/templates/$TPL first, then accepts a raw path)
-n(enable)Generate-only; skip the trailing en auto-call
-w(off)Append a 127.0.0.1 NAME mapping to /etc/hosts for local-only DNS
-hn/aPrint usage and return

@VHOST PIPELINE

From vhost foo.example to a live nginx site in 5 stages. Each stage gates on the previous — missing user, missing template, or failed enable will short-circuit with a colourised error and leave the system in a consistent state (no half-written config in sites-available, no dangling symlink in sites-enabled).

  vhost [-u USER] [-t TPL] [-n] [-w] NAME
       │
       ▼
  ┌─────────────────────────┐
  │ getopts: parse flags    │
  │   user   ← USER || $USER│
  │   tmpl   ← TPL  || $NGINX_VHOST_TEMPLATE
  │   enable ← (no -n)      │
  │   hosts  ← (-w)         │
  └─────────┬───────────────┘
            │
            ▼
  ┌─────────────────────────┐
  │ resolve template path   │
  │   $ZSH/plugins/nginx/   │
  │     templates/$TPL      │  ──── or ── raw path ── or ── default ─▶
  └─────────┬───────────────┘
            │
            ▼
  ┌─────────────────────────┐
  │ _vhost_generate         │
  │   user lookup (passwd)  │
  │   pool_port = "1" + UID │
  │   sed {vhost}{user}{pool_port}
  │     → $NAME.tmp         │
  │   $sudo mv $NAME.tmp    │
  │     → sites-available/  │
  └─────────┬───────────────┘
            │
            ▼ (if enable)
  ┌─────────────────────────┐
  │ en $NAME                │
  │   ln -s available/$NAME │
  │      enabled/$NAME      │
  │   (sudo)                │
  └─────────┬───────────────┘
            │
            ▼ (if -w)
  ┌─────────────────────────┐
  │ _write_hosts $NAME      │
  │   splice "127.0.0.1     │
  │   $NAME" into first     │
  │   line of /etc/hosts    │
  └─────────────────────────┘
            │
            ▼
  ┌─────────────────────────┐
  │ ngt && ngr              │ ← user-driven, not auto
  │ (verify + reload)       │
  └─────────────────────────┘

The pool-port trick (pool_port = "1" + UID) is the canonical multi-tenant PHP-FPM convention: each user gets a fastcgi socket on their own port, derived from /etc/passwd UID. UID 1000 → pool 11000, UID 1001 → 11001, etc. The template emits the literal unix:/var/run/php5-fpm.{user}.socket string — the pool_port is substituted in for templates that use it.


&COMPLETION

All three completions follow the same 8-line shape: a #compdef directive, an internal helper that runs ls -a against the relevant directory and pipes through awk to filter to lowercase-prefixed names, then compadd `helper` to feed candidates into the completion engine. The point: completion sources are live filesystem state, not a hand-maintained list.

CompletionSource dirawk filter
_nginx_en$NGINX_DIR/sites-available/^[a-z][a-z.-]+$/
_nginx_dis$NGINX_DIR/sites-enabled/^[a-z][a-z.-]+$/
_nginx_vhost$HOME/www/^[^.][a-z0-9._]+$/

The asymmetry is intentional. en's candidates are configs that exist but aren't symlinked (available). dis's candidates are configs that are currently active (enabled). vhost's candidates are project directories that don't yet have a vhost (~/www). Each completion answers exactly the question the user asks; no overlap, no confusion.


~TEMPLATES

Two stock templates, both targeting per-user PHP-FPM with a unix-socket fastcgi backend. Both deny .ht* dotfiles (Apache leftovers blocked by convention). Substitution uses {vhost}, {user}, {pool_port} — a 3-key sed pass. The choice was deliberate: bigger templating systems (mustache, jinja) would force a tool dep on the user's box; sed is in every POSIX install.

templates/symfony2

Web root /home/{user}/www/{vhost}/web. Front controller app_dev.php. try_files $uri $uri/ /app_dev.php$uri /app_dev.php$is_args$args. Fastcgi handler scoped to ~ ^/(app|app_dev|check)\.php(/|$) — only the canonical Symfony2 entry scripts run PHP. client_max_body_size 10M. Per-vhost error + access logs at /var/log/nginx/{vhost}.{error,access}.log.

templates/plain_php

Web root /home/{user}/www/{vhost} (no nested web/). Front controller index.php. try_files $uri $uri/ $uri/index.php. Fastcgi handler scoped to any \.php$ — classic PHP layout with one entry per script. Same per-vhost logs and PHP-FPM socket as the Symfony template.


*TEST COVERAGE

28 zunit @test cases across 5 test files pin the plugin contract: aliases (ngt/ngr) are defined and reference $sudo; entrypoint defaults $NGINX_DIR and $NGINX_VHOST_TEMPLATE only when unset (idempotent re-source); every autoload fn is defined after sourcing the plugin; every completion has a #compdef directive matching its fn name; every source file parses cleanly under zsh -n. 21 additional .sh doc-hygiene gates pin the README, docs, workflow, and test-file structure.

Test file@testPins
tests/t-aliases.zsh6ngt + ngr alias presence; alias values reference $sudo; alias values mention nginx; sudo probe sets $sudo when sudo is on PATH
tests/t-contract2.zsh6Three autoload fns (en, dis, vhost) are defined after sourcing; autoload/ appears on fpath; src/ appears on fpath
tests/t-syntax.zsh6Every file under autoload/ and src/ + entrypoint parses under zsh -n; no syntax drift on push
tests/t-contract3.zsh5$NGINX_DIR defaults to /etc/nginx when unset; $NGINX_VHOST_TEMPLATE default; existing values are not clobbered on re-source
tests/t-contract4.zsh5Each completion file (_nginx_en, _nginx_dis, _nginx_vhost) starts with #compdef followed by the matching fn name; entrypoint stem matches plugin directory name
5 zunit files28+ 21 .sh doc-hygiene gates

?KEY DESIGN DECISIONS

Each call-out is a decision the implementation could have gone either way on, with the rationale for the path taken.

Debian/Ubuntu layout, not Red Hat

The sites-available/ + sites-enabled/ split is a Debian convention — Red Hat puts every config in /etc/nginx/conf.d/. The plugin standardises on the Debian layout because that's where the symlink semantics make sense; en/dis have nothing useful to do on a flat conf.d setup.

Symlink, not move

en creates a ln -s, not a cp or mv. dis is rm -f against the symlink, not the underlying config. This means dis never destroys the actual file; the user can en again to instantly restore.

sed templating

3 placeholders, sed-substituted in a single pass. A real templating language would force a tool dep that may not be on the user's nginx host. sed is POSIX, and the 3-key surface is small enough that escaping concerns don't bite.

UID-derived pool port

pool_port = "1" + UID via /etc/passwd lookup. Avoids hand-assigning per-user PHP-FPM ports; the port is deterministic from the system's user database, so two users on the same box can't collide.

Completion from filesystem

Every #compdef reads ls -a against the relevant directory rather than maintaining a static list. A vhost added by hand outside the plugin still tab-completes; the plugin can never lie about what's actually installed.

Optional sudo

The entrypoint probes for sudo (which -p sudo) and falls back to empty string when missing. Containers, embedded systems, and root-shells get a working plugin without the sudo prefix; default Linux desktops get the elevation they need to touch /etc/nginx.

Two templates

One is the historical Symfony 2 layout (web/ root, fastcgi scoped to entry scripts), one is generic PHP (any .php executes). Together they cover the dominant LAMP / Symfony deployment shapes circa the plugin's origin. A user with another framework can drop a custom template into $ZSH/plugins/nginx/templates/ and select it with -t.

Inline ANSI escapes, not tput

Status messages embed raw \033[31m...\033[0m sequences. The plugin runs in interactive zsh on a terminal that supports them; portability to non-terminal contexts isn't a goal. tput would force a termcap/terminfo lookup that isn't needed.


+COMPAT & DEPS

The plugin assumes Debian-style nginx layout (/etc/nginx/sites-available + /etc/nginx/sites-enabled) and per-user PHP-FPM on unix sockets. Anything else is BYO template. Zsh 5.x compat floor — no zsh 6 features. No build step, no install hook, no compiled output.

DepHardnessUse
zshhardRuntime (5.x+)
nginxhardTarget service — the plugin's reason to exist
sedhardTemplate substitution in _vhost_generate
awkhardCompletion filter (ls -a | awk ...) and /etc/passwd field extraction
sudosoftAuto-detected; falls back to direct exec when missing
service / systemctlsoftngr uses service nginx restart; on systemd-only boxes, customise the alias

// ENV CONTRACT

Env varDefaultPurpose
NGINX_DIR/etc/nginxRoot for sites-available/ + sites-enabled/
NGINX_VHOST_TEMPLATE$ZSH/plugins/nginx/templates/symfony2Default template when vhost is called without -t

!STRATEGIC POSITION

zsh-nginx is the smallest plugin in the MenkeTechnologies stack but covers a real ops loop: "I want a new local vhost on this dev box, now." Without the plugin, the same loop is ~5 hand-typed commands across cd /etc/nginx/sites-available, sudo cp template foo, sudo vim foo, sudo ln -s ../sites-available/foo ../sites-enabled/foo, and sudo service nginx restart. With the plugin it's vhost foo -w && ngt && ngr — same outcome, three commands, no manual sed.

Dev workflow

Spin up a vhost for a project in ~/www/foo: vhost foo -w generates the config, enables it, and writes 127.0.0.1 foo to /etc/hosts. ngt + ngr reload.

Multi-tenant servers

The UID-derived pool port + per-user fastcgi socket convention scales to dozens of users on a shared host without port collisions or hand-maintained pool mappings.

CI / provisioning

The plugin is a thin shim over ln -s + sed — safe to call from provisioning scripts. en/dis are idempotent by construction (the symlink either exists or it doesn't).

Plugin stack hub

One of 5 plugins in the MenkeTechnologies meta repo; the lightest in LOC but the one with the most external system touch (nginx config, /etc/hosts, /etc/passwd, service control).