Conventions¶
House rules for writing a page here. Readers browsing lessons do not need this file; it is for whoever is about to add one.
The rule that comes before the others¶
A page never claims something a program has not printed — and CI prints it twice, on Ubuntu and on macOS, under the same perl. An answer key is only what both machines agree on.
The shape of a lesson¶
01_One_Liners/
n_and_p_are_a_loop/
README.md the lesson
demo/ input files a reader can run the page's one-liners on
orders.txt
examples/
n_and_p_loop_sh.sh a script that runs the one-liners over demo/
n_and_p_loop_sh.out its recorded output (generated — do not hand-edit)
One idea per folder. The folder name is the idea, in lower_snake_case, and it becomes a permanent URL — so name it for what it teaches, not for where it sits in the reading order. A Perl program is examples/<stem>_pl.pl and a script is examples/<stem>_sh.sh: a page names an example by its bare stem, so stems must be unique across languages, and the suffix is how.
The page¶
Open with the title, a **Level:** line (101 / 201 / 301, then ·, then who it is for) and a **One line:** that states the claim rather than the topic. Do not hard-wrap paragraphs — one paragraph, one line.
Output is generated, never typed¶
Mark the spot and let the tool fill it:
tools/run_examples.py runs the example and pastes what it actually printed, with a provenance line above the fence. Inside the markers is generated; outside is yours. A second kind, <!-- source:stem -->, pastes the program itself — use it when the code is the lesson.
python3 tools/run_examples.py # verify + refill
python3 tools/run_examples.py --update --only X # record X's output as its answer key
python3 tools/run_examples.py --check # write nothing, fail on drift (CI)
python3 tools/check_all.py # every gate CI runs, honest exit status
python3 tools/check_all.py --staged # the same gates on the tree your next commit makes
Always pass --only with --update, and read what it recorded before committing: --update accepts whatever the program printed, so it will happily enshrine a bug. Two did on the first day — a helper that copied its argument and so reported pos() as undef, and a yn($s =~ /re/) that died because a failed match in an argument list passes no argument at all.
The programs¶
Perl: use v5.36 first, core modules only. One line turns on strict, warnings, say, signatures and the unicode_strings feature. Core means what perl ships — Encode, Unicode::Normalize, Unicode::Collate, Text::ParseWords, Scalar::Util, File::Temp. A lesson that needs CPAN (Text::CSV) names the module in prose and does not run it.
use utf8 whenever the source holds a non-ASCII character, and binmode STDOUT, ':encoding(UTF-8)' before printing text — except in the lessons whose subject is leaving them out, which say so in their opening comment.
A warning the lesson is about goes to stdout, in order. The runner records stdout only. A program installs a $SIG{__WARN__} handler that prints the message with its at FILE line N. removed, so editing the program does not change the key; a script writes 2>warnings.txt and then cats it. Anything else on stderr is reported as a note, and a non-zero exit fails the run — so an example that shows die catches it with eval.
Shell: bash, from the example's folder, under LC_ALL=C. Print each command before running it — the say helper in the existing scripts does that — so a verified block reads like a terminal. A script that edits files (-i) copies demo/ to a mktemp -d and works there.
Deterministic, on both machines. No clocks, timings or random numbers. No hash order — sort the keys — and no references printed as HASH(0x…). No tool whose formatting differs between BSD and GNU: od -An -tx1 pads its columns on macOS and not on Debian, and wc -l < file right-aligns its number on macOS. Dump bytes with perl instead — perl -0777 -ne 'print join(" ", unpack "(H2)*", $_), "\n"', the hex helper in the Wide character lesson's script.
The fixed environment¶
The runner deletes these before every run, because each changes what perl does before the program's first line, and a reader who has one set would otherwise see a different answer from the page:
| Variable | What it would change |
|---|---|
PERL_UNICODE |
the -C switch from outside — UTF-8 layers on STDIN, STDOUT and files, which hides the "Wide character" and double-encoding lessons |
PERL5OPT |
extra switches, -M included, added to every perl |
PERL5LIB, PERLLIB |
module directories searched before the core ones |
PERLIO |
the default I/O layers |
PERL_HASH_SEED, PERL_PERTURB_KEYS |
hash order |
It also sets LC_ALL=C and LANG=C. Perl ignores the locale unless a program says use locale, so this is for the shell tools around it.
Two machines, one perl¶
CI runs every example on ubuntu-latest and macos-latest, with perl 5.42 installed by shogo82148/actions-setup-perl on both — the macOS image's own /usr/bin/perl is 5.34, too old for use v5.36. Measured differences so far — add a row when you find one, and say where you measured it:
| Linux | macOS | |
|---|---|---|
| perl measured locally | 5.40.1, Debian's gcc:14 image |
5.42.0, Homebrew, macOS 26 |
| Unicode tables in that perl | 15.0 | 16.0 |
| every example's output | identical | identical |
B::Deparse of -n, -p, -l, -a, -F, -i, -0 |
identical | identical |
od -An -tx1 |
single spaces | columns padded |
wc -l < file |
the bare number | right-aligned in 8 columns |
Measured 2026-09-13. The Linux column is the gcc:14 Docker image because it carries Debian's full perl package; ubuntu:24.04, debian:bookworm-slim and python:3.13-slim ship only perl-base, which has no Encode.
Bridges¶
Every lesson ends with If you are coming from another language: the page in the Python ↗, Java text ↗, C ↗ or Rust ↗ library that teaches the same idea, and the encodings library ↗ page for what the bytes mean. Link a sibling page when one exists; do not repeat it. A sibling page links to <site>/<folder>/index.html — every library in the family builds with use_directory_urls: false.
Links¶
- Link a folder by naming its
README.md—[label](some_folder/README.md), never[label](some_folder/). - A link that leaves the library ends its label with
↗; an internal link never does.python3 tools/check_link_style.py --fixadds and removes them; CI runs it without--fix.
Nav order¶
A new lesson folder gets a row in NAV_ORDER in mkdocs_hooks.py ↗. Its sidebar label is its README's # H1 with the backticks dropped. tools/check_nav_chain.py fails on a row naming a folder that does not exist, so commit the folder and its row together.
Committing in a shared checkout¶
Several sessions may work in one checkout. Stage only your own paths, then run python3 tools/check_all.py --staged — it gates the tree your commit will make, not your working directory, so someone else's half-built lesson can neither fail your commit nor ride along in it.