|
spudplate
Template scaffolding compiler for spudlang .spud files
|
This page covers patterns that recur in the templates that work well, alongside the reasoning that motivates each one. Spudlang is small enough that there are not many ways to do most things, but the choices that exist matter for readability and for keeping templates from breaking when they grow.
The advice here is descriptive first: it explains why most useful templates settle on certain idioms, not laws you must obey. Where there really is one right answer (the no-shadowing rule, for example), that comes from the language and is documented in the reference; this page rarely repeats it.
Spudlang identifiers are case-sensitive, but the test suite, the standard library of examples, and every existing real template uses snake_case. Mixing styles within one template is jarring, and camelCase reads oddly next to keywords like mkdir and as. Pick snake_case and stay there.
The variable holds the answer, not the prompt. Aim for names that describe what the value is, not what was asked.
Names that read as a question (should_we_include_tests, how_many_weeks) age badly: they make if use_tests read fine but if should_we_include_tests clunky.
Most boolean asks are gates on optional sections. The conventional prefixes make the intent obvious at a glance:
A direct feature name (tests, docs) also works for the boolean form. Pick one form and stay consistent.
A question with no default is required. The user cannot skip it. That is exactly right for the project name, but wrong for the test runner: a user who does not care should be able to press enter.
A useful rule of thumb: if you can describe a sensible default in one sentence, the question deserves a default.
options turns an answer into a numbered menu. The user can type the literal value or the menu number. Two reasons this is worth using even when typing the value is easy:
default matches the options at parse time, catching typos in the template.Anywhere you would write a comment like "use 'pdf', 'html', or 'latex'", options does the same job in a way the user will see.
Because ask is a regular statement, later questions can use the answers of earlier ones in their default and when clauses. Order them so that prerequisites come first:
A well-ordered question sequence reads like a guided conversation.
A when-gated question is only asked if its condition is true. The default is bound when the gate is false, so subsequent code always sees a real value. This is cleaner than asking a question the user has already implicitly declined.
The when-gated question must always have a default; the validator enforces it.
If the same expression appears in two places, name it with let. It gives you one place to change, and a name that documents what the value means.
When the same path appears twice in a row, an as alias is shorter and conveys "this is the same path, not a coincidentally-similar one":
versus
Both work; the alias form scales better as the path gets deeper or more complex.
A README built from optional sections is the canonical use of file ... as:
This is the lightest-weight way to build a sectioned file with optional parts. The same pattern works for .gitignore, package.json scripts blocks, etc.
A let derived from a let derived from a let works, but each link in the chain hides the original input. Three or four steps is fine; a long chain usually means the original ask should be reshaped.
A bare path segment is a variable reference. A quoted segment is a literal. When in doubt, quote.
A common mistake is to write mkdir templates from base_templates and expect both names as literals. The parser interprets each as a variable reference, and the validator rejects them as undeclared. Quote them: mkdir "templates" from "base_templates".
Interpolation in a path expression works only inside a quoted segment:
If you want to combine an alias with an interpolation, quote the part that needs braces:
These two answer different questions:
| Goal | Statement |
|---|---|
| Create a new directory and populate it from one source | mkdir <path> from <source> |
| Add content to a directory you have already created | copy <source> into <path> |
A common idiom uses both. Create the base from one tree, then merge optional add-ons:
A single conditional statement reads fine with a when clause:
Three or more statements with the same when start to repeat. An if block is clearer:
The threshold is usually two or three statements. Below that, when is fine. Above it, the repeated condition obscures what is shared.
Spudlang has no else. For "do A or B but not both", two if blocks work:
For more than two branches, a chain of if blocks with mutually-exclusive == conditions is the cleanest form.
A repeat introduces its own scope and prompts inside it run once per iteration. Both are useful, but a long loop body with several lets and nested ifs gets hard to follow. If you find yourself writing more than ten or so lines inside a repeat, consider whether each iteration should be its own included template.
file from is for files where most of the content is fixed and you want to ship them as-is, with light {ident} substitution.
file content is for short dynamic strings: a one-line header, a generated config blob.
The line is roughly: if you would think of opening the file in a text editor, it belongs in from. If it is a single string the template assembles, content is right.
The interpreter auto-detects binary content (anything that is not valid UTF-8) and copies it verbatim regardless of the verbatim keyword. So you do not need verbatim on PNGs, favicons, etc.
You do need verbatim on text files that legitimately contain {, because the substitution scan would otherwise misinterpret them. LaTeX templates and shell scripts are common cases.
Most files want the system default. The two cases worth setting mode for are:
mode 0755.mode 0600 or mode 0700.Setting mode 0644 everywhere is noise; that is what your umask gives you anyway.
Without in <path>, a run inherits the cwd of spudplate, which is rarely the project subdirectory the template just created. Pin every command:
A user's string answer can be anything, including a shell injection payload. Two ways to defend:
options.Most templates do not need user input inside a run. The trust prompt and the user reading their own commands are a good safety net, but the safest design avoids the problem.
The default timeout is 60 seconds, which is right for git init but wrong for npm install or a multi-minute build. Set timeout explicitly when you know the command will take longer.
run is the most powerful and the most dangerous statement. Use it for things that cannot be expressed structurally:
git init, git remote add origin <url>npm install, cargo new, python -m venvchmod +x (although mode 0755 on file from does the same thing without the shell)If a run is doing what file content, mkdir from, or copy into could do, prefer the structural form. It is faster, fails earlier, and does not need the trust prompt.
If two templates set up Claude Code config, do not copy the lines into both. Make a claude_setup template, install it, and include it from each:
The included template runs inline at the include point, asks its own questions in source order, and stays maintainable independently. Its bytes are bundled into the parent at install time, so the recipient does not need the dependency installed separately.
An include without a when runs unconditionally, asking its own questions every time. Most include candidates are optional, so a when paired with a bool ask is the conventional shape.
Most spudlang errors surface before any prompt runs. The validator checks types, scopes, alias conditions, and condition normalisation. A template that fails validation is one the user never has to interact with.
A few categories of error are inherently run-time:
from source that is missing at install time (validate with spudplate validate).copy into whose destination does not exist (often a logic error in the template).run command that returns non-zero (the user's environment is the variable).Lean on the parse-time guarantees: a template that survives spudplate validate is much closer to working than one that has not been validated.
spudplate validate <file.spud> runs the lexer, parser, and validator without prompting or installing. Run it after each substantive edit. The errors it produces are precise about line and column.