./run in the repo root dispatches to the command scripts in this directory.
Run ./run (or ./run help) for the full command list, and
./run <command> help for details on one command.
- macOS / Linux:
./run <command> - Windows: use Git Bash (installed with Git for Windows):
./run <command>, orbash run <command>from any shell (e.g., PowerShell) that can invoke bash.
./run setup checks that everything below is installed and reports all
problems in one pass. It installs nothing.
Most common commands:
- once:
./run setup - when editing (in two terminals):
./run serve: http://localhost:8080 with rendered docs./run watch: regenerate docs whenever a file changes (manually refresh in browser to see new docs rendered)
| Tool | Needed by | Notes |
|---|---|---|
| bash | everything | 3.2+; Git Bash on Windows; included on most other platforms |
| Python | build, serve, watch, check-links, doctest |
3.11 or newer |
| dotnet | docfx install and hosting | .NET SDK 8.0 or newer |
| docfx | build, serve, watch |
local dotnet tool or global |
| te | doctest (all but validate) |
Tabular Editor CLI |
Working on these scripts (python and shell scripts) needs a few more tools; see Run script development under Contributing below.
We provide the most common and script-friendly installation methods per platform in each section below. If you prefer, though, you can follow these links to each tool's official installation docs
- Bash (Git for Windows): https://git-scm.com/install/windows
- Bash (non-windows): https://www.gnu.org/software/bash/
- Python: https://www.python.org/
- docfx: https://dotnet.github.io/docfx/
- te (Tabular Editor CLI): https://tabulareditor.com/download-tabular-editor-cli
# Python from your distro, e.g. Debian/Ubuntu:
sudo apt-get install python3
# .NET SDK (for docfx): see https://learn.microsoft.com/dotnet/core/install/linuxbrew install python3
brew install --cask dotnet-sdk # .NET SDK (for docfx)Install Git for Windows for Git Bash, then:
winget install Python.PythonInstallManager
winget install Microsoft.DotNet.SDK.10Note: winget has no unversioned .NET SDK id; docfx needs SDK 8.0 or newer.
With the .NET SDK installed (see your platform above), install docfx as a repo-local dotnet tool. From the repo root:
dotnet tool install docfx
dotnet tool restoreIf you are running a dotnet older than 10.0, run dotnet new tool-manifest first.
The te CLI is installed the same way on every platform:
sign in at tabulareditor.com,
download the archive for your platform and architecture, extract it,
and put te on your PATH (or point RUN_TE at it).
Full instructions, including verification, live in this repo's own docs:
te CLI install guide.
The most common commands are provided below for reference. All sub-commands provide help text at the command line.
- Check all dependencies are installed:
./run setup: run only to start and ensure you have the correct toolsRUN_TE=/path/to/te RUN_PYTHON=/path/to/python ./run setup: (advanced usage) confirm that tools at custom paths are resolved correctly
- Hot reloading iteration on markdown content (run in two separate terminals); likely all you need for most content contribution:
./run serve: builds docs site and launches a localhost server at http://localhost:8080 where you can browse rendered docs./run watch: watches for changes in template files and markdown files, runs a fast rebuild upon detecting changes
- One-off build (pick one):
./run build: required after a./run cleanor a fresh clone of the repo./run build fast: skip re-generating the API docs; what you probably want most of the time and saves 1/3-1/2 build time
- Check the built site for broken links (needs a built site: run
./run buildfirst):./run check-links: full check; fetches every unique external URL once, so it needs network access and can take a while./run check-links local: offline; on-disk files and anchors only
- Test the annotated C# code blocks in the docs (needs the te CLI); without file args, automatically discovers all markdown files with annotated C# code blocks:
./run doctest: compile and run all annotated code blocks; compare results to**Output**blocks, but do not update files./run doctest file1.md file2.md: same, for exactly the named files./run doctest update: compile, run, and replace**Output**blocks in all markdown files with annotated code blocks./run doctest update file1.md path/to/file2.md: same, but only for the named files
- Clean build artifacts:
./run clean - Get help:
./run help: print top-level help for therundispatcher and list all subcommands./run <subcommand> help: print full help for<subcommand>
Every external tool can be pinned to a specific executable with a RUN_<TOOL> environment variable:
RUN_PYTHON, RUN_TE (and, for the tooling developers' tools, RUN_UVX, RUN_SHELLCHECK, RUN_SHFMT).
When set, that path is the only candidate checked; there is no fallback to your PATH environment variable.
Example:
RUN_PYTHON=/opt/python3.12/bin/python3 ./run build
RUN_PYTHON=/opt/python3.12/bin/python3 RUN_SHELLCHECK=/path/to/shellcheck ./run buildThese environment variables are intended for testing specific versions or for users with these binaries not available in their PATH.
docfx is resolved by build-docs.py itself:
a DOCFX environment variable, then a repo-local dotnet tool manifest, then a global docfx on PATH.
This section is specifically about contributing to the run scripts themselves. For guidance on contributing documentation content, see the project README and the authoring guidance below that section.
./run scripts and its nested commands are the gateway to run scripts about run scripts.
- Check the script-development tools are installed:
./run scripts setup - Lint and verify formatting, without modifying anything (the check a PR should pass):
./run scripts check: everything (all shell tooling, the qualified Python sources)./run scripts check file1.sh file2.py ...: specific files, routed by kind
- Apply the formatters (writes files):
./run scripts format [file ...] - Scaffold a new run script:
./run scripts new <name>(createsbuild_scripts/run_scripts/<name>.shfrom the anatomy below, ready to edit)
Additional requirements beyond the doc-contributor table above:
| Tool | Needed by | Notes |
|---|---|---|
| uvx | scripts (Python sources) |
part of uv |
| shellcheck | scripts (shell sources) |
|
| shfmt | scripts (shell formatting) |
mvdan/sh |
The first ./run scripts check needs network access: uvx downloads ruff and mypy into its cache on first use.
Installation:
# Linux (Debian/Ubuntu):
sudo apt-get install shellcheck shfmt
curl -LsSf https://astral.sh/uv/install.sh | sh # uv provides uvx
# macOS:
brew install uv shellcheck shfmt# Windows:
winget install astral-sh.uv
winget install koalaman.shellcheck
winget install mvdan.shfmt- bash 3.2 compatible (macOS default bash), ASCII only, long-form CLI flags where a long form exists
- Shell formatted by
./run scripts format(shfmt -s: tab indentation,name() {function style, simplifications applied) - Python tooling formatted with ruff format (also via
./run scripts format); lint rules, line length, strict mode, and the qualified-file lists live in pyproject.toml at the repo root (the ruffincludeand mypyfileslists sit adjacent there and must stay in sync) - Each script: banner help block, per-function comment docs, awk-extracted help,
dispatch <default_fn> "$@"at the bottom - Shared helpers live in
lib.sh; command-specific state lives in the command's script - All of this tooling must pass
./run scripts check(shellcheck + shfmt diff for the shell sources; ruff, ruff format diff, and mypy for the Python sources) - Any internal function not intended for re-use is prefixed with an underscore
Everything flows through the run command,
which is a dispatcher to various special-purpose scripts, exposed as subcommands to run.
Every subcommand is a script in this directory (run_scripts/).
The script must be named name.sh and name becomes the name of the sub-command;
there is no other registration for sub-commands, if the script exists, it's a subcommand.
run is the script in the repo root, through which we invoke all run scripts.
It is intentionally minimal, and does only these things:
Happy path:
cdto the repo root, so every run script starts from the same working directory no matter where you invoke it from- resolve
<subcommand>tobuild_scripts/run_scripts/<subcommand>.shand invoke it withbash, passing all remaining arguments through verbatim
help: Print help (./run, ./run help) and the command list (./run commands, ./run --list)
Error path: Print an error plus the command list when the first argument does not resolve to a script
The command list is discovered by scanning this directory for *.sh files (excluding lib.sh);
the one-line description for each command is pulled from its banner comment with banner_title.
Anything beyond the above belongs in a run script, not in run.
Shared helpers live in lib.sh; these are intended to be used for boilerplate and common operations across sub-commands:
info: print informational messages onto stderr for the usererror: print error messages prefixed with "ERROR: "log_and_run: prints the command being run in bold on stderr; use for all commands that actually get run for a scripts worklog_cmd: the printing half oflog_and_runon its own; use when the command's execution is indirected (e.g. parallel workers whose output is captured, as indoctest.sh)require: automatically handle the env var for tool overrides and fail with info if the tool can't be foundrun_var_name: print theRUN_<TOOL>env var name for a tool name (the naming convention, defined in one place)print_resolved: print each given tool with the executable path itsRUN_<TOOL>resolved to (for setup-style reports; callrequirefirst)dispatch: provide a way to take a default function with args (see usage in commands for examples)banner_title: print the first one-line summary of a comment banner at the top of a script file (for command lists)banner_help: print the whole comment banner at the top of a script file (for help and usage)command_help: print doc comments for each function in a script (for help output)
If you are writing or editing a run script, you should use these helpers.
If there's some new generic requirement, that's a decent candidate to be added to lib.sh.
All run scripts must:
- be named
<subcommand>.shin this directory; they are automatically discovered based on that naming convention. - have a
#!/usr/bin/env bashshebang, and are marked executable withchmod +x. - have a comment banner with a one-line summary, help text, and usage examples
- contain the header below
- use shell functions for all functionality
- contain a standard
helpfunction - use the
dispatchpattern
A banner comment is of the form below. This becomes the help text for this script.
###################################### (at least 10 octothorpes starting at character 0)
# One line command summary
#
# all banner comment lines must start with an octothorpe.
# provide descriptive help text
# explain what this run script does
# give any relevant details for a user running this command
# not information about the development of the command
#
# Usage:
# ./run command # what this invocation does
# ./run command arg # what *this* one does
###################################### (at least 10 octothorpes starting at character 0)This:
- sets up some safety defaults for Bash
- makes sure that all scripts are executing in the same working directory context
- sources
lib.shso we have the shared helpers
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=build_scripts/run_scripts/lib.sh
source "$SCRIPT_DIR/lib.sh"
cd "$SCRIPT_DIR/../.."Anything that looks like an argument or a sub-subcommand is implemented as a Bash function. The very first thing in the function definition must be comments that become the help text for this function.
Example:
fn() {
# the help text for this function
# can span many lines
require ...
<implementation>These functions must declare their required tools upfront with require, e.g., require uvx;
do not use a path here, but only the exact binary name.
The sole exception is Python, which must always be written as "python" verbatim (require handles platform specifics for this).
Examples:
require uvxrequire pythonrequire python shellcheck uvx
Using require means we get automatic environment variable handling for these things, and helpful error messages when the binaries are not available.
require does two jobs.
- It checks that each named tool is available,
honoring the user's
RUN_<TOOL>override as described in Dependency overrides, - It exports a
RUN_<TOOL>environment variable holding the winner. The variable name is the tool name uppercased, with dashes turned into underscores:require uvxexportsRUN_UVX,require shellcheckexportsRUN_SHELLCHECK,require pythonexportsRUN_PYTHON.
Always invoke a required tool through its variable, never by its bare name:
python () {
# ruff on the Python tooling
require uvx
log_and_run "$RUN_UVX" ruff check --quiet "$@"
}Invoking "$RUN_UVX" (quotes included; the path may contain spaces) is what makes a user's override actually take effect.
A bare uvx on that line would search the user's PATH, but if they provided RUN_UVX=/other/path/to/uvx ./run scripts check,
then they are explicitly telling us, "Use the uvx at the provided path, not from PATH,"
so we honor that.
These exports also flow into any script the current one delegates to (e.g., watch.sh runs build.sh fast, which sees the same RUN_PYTHON),
so an override applies consistently across a whole command.
On failure, require prints what is missing and a pointer to this README.
When there are multiple arguments to require, e.g., require python uvx, it resolves and sets all variables.
If one or more tools are not found, then the error printed to the terminal includes details about all missing tools.
When it doesn't find all required tools then it returns nonzero; it never exits the script itself.
In the normal placement, at the top of a function that the user invoked directly, that is all you need:
the script header's set -euo pipefail turns the non-0 return into a clean stop.
The exception is a function that gets called inside a conditional (if, !, &&, ||).
Bash suspends set -e for everything running inside a condition, including called functions, so a failing require there does not stop anything;
execution falls through to the next line as if nothing happened.
In that placement, you must propagate the failure explicitly:
require dotnet || return 1setup.sh is the resident example: its check function probes helpers inside if ! conditionals, so those helpers use the explicit form.
This uses lib.sh helpers to give a standardized help text.
These pull help text out of the banner comment and all leading comments in functions.
They are automatically formatted as you see when you run ./run help or ./run subcommand help.
help() {
# show this help
banner_help "$0"
command_help "$0"
}The last line of every run script is a call to dispatch:
dispatch <default_fn> "$@"Read it as: "look at what the user typed after ./run <subcommand>, and call the function with that name".
Take build.sh, which ends with dispatch full "$@", as a worked example.
"$@" stands for "everything the user typed after ./run build", so:
- When a user submits
./run build, there is nothing after the subcommand, so we end up withdispatch fulland nothing in"$@". With no user input to look at,dispatchfalls back to the default it was given and calls thefullfunction. - When a user submits
./run build fast, we end up withdispatch full fast.dispatchsees the user'sfast, finds a function with that name in the script, and callsfast; thefulldefault is ignored. - When a user submits
./run build nonsense, we end up withdispatch full nonsense. There is nononsensefunction in the script, so dispatch prints an error, then the script'shelp, and exits with a usage error. - Anything after the function name is handed to that function as its arguments.
In
scripts.sh(defaultcheck),./run scripts format a.py b.shends up asdispatch check format a.py b.sh; dispatch calls theformatfunction witha.py b.shas its arguments.
Name the default function after what it does (full, all, check), not a generic main;
function names are user interface here, since they are exactly what a user types after ./run <subcommand>.
There are exactly two correct ways to write the dispatch line:
- The standard form shown above:
dispatch <default_fn> "$@". The default function name is required;dispatchalways needs to know what to run when the user types nothing. - A wrapper around form 1, for a script that wants to handle unrecognized arguments itself instead of treating them as an error.
For example,
scripts.shtreats an unrecognized first argument as a list of files to check:
if declare -F "${1:-check}" >/dev/null; then
dispatch check "$@"
else
check "$@"
fiTwo mistakes to avoid:
dispatch "$@"(leaving out the default) is broken, not just incomplete:dispatchwould mistake the user's second argument for the name of the function to call, or crash the script when there are no arguments at all. If a script has no obvious default action, makehelpits default."$@"must always be written with the quotes. Without them, an argument containing a space (such as a file path) gets split into pieces beforedispatchever sees it.
We use shell scripts primarily for orchestration of other tools. If you just want to provide a friendly wrapper for some more complex command, or a bunch of reasonable defaults, then a shell script in this directory is the right choice. If there is significant logic, data processing, or any sort of parsing, then a python script is probably the right choice. If you build a python script that is a reasonable tool for most contributors to use, then you should also give it a friendly run script wrapper here. A good example of a run script wrapper improving contributor convenience is doctest.sh; this wraps a python tool that can only operate on one file, and gives a run script interface that lets you pass many files, calling the python script once for each.
Write everything as a series of small functions,
using dispatch patterns as in these run scripts and demonstrated in check_links.py,
te_script_runner.py, and csharp_doctest.py.
This allows incremental testing and validation:
small pieces are built that do a part of the work and their logic is demonstrated to be sound;
then downstream components compose and orchestrate this functionality.
This style supports incremental validation, as well as ad hoc reuse of components.
We do not follow TDD (there are not test suites for these scripts),
but following this approach still keeps the code testable, which is critical.
General conventions and guidelines.
- Output routing: stdout carries only a command's real output;
all chatter (progress, status, errors) goes to stderr via
info/error, so output stays clean for piping and redirection. - Success is quiet: pick tool flags that silence success output
(e.g. ruff
--quiet, mypy--no-error-summary), or capture a step's report and print it only on failure (asdoctest.shdoes). Anything printed should mean something needs attention. - Ensure failures have non-0 exit stati; for anything in a pipeline or other automation, this is the sole reliable indicator of failure.
log_and_runexecuted commands: every command that performs a script's actual work runs throughlog_and_run, or announces itself withlog_cmdwhen its execution is indirected (see the parallel workers indoctest.sh); silent probes and internal plumbing do not. When delegating to another script, let the delegate echo its own commands: seewatch.shandbuild.sh- Report everything at once: collect all failures (missing tools, broken files) and report them together, one per line, before failing; this allows a user to remediate as much as possible at once, rather than chasing one error at a time. If there is useful documentation to point to in an error context, do so; this README is a good choice.
- No silent false successes: a checker that found nothing to check must fail or say so loudly,
never pass quietly (see the missing-root and zero-pages guards in
check_links.py). - Explicit user input always overrides defaults: if a user gives us filenames or sets an env var,
we use exactly what they have provided, rather than automatic discovery or default file lists.
See
RUN_<TOOL>handling and all commands that accept file args. - Run scripts should always accept multiple files, and route these files intelligently;
see the mixed .py and .sh handling in
scripts.sh. - A wrapper is a chance to fix warts in what it wraps, not to reproduce them; when porting or wrapping existing behavior surfaces a bug, fix it.
- Use constructs portable across the BSD and GNU userlands:
plain
trranges underLC_ALL=Crather than[:class:]-plus-literal sets, and streaming filters rather thansed -i(whose flags differ between the two). - Keep docs, comments, and implementation in sync.
- Ensure python scripts remain cleanly importable by using setup functions guarded in
if __name__ == '__main__':.