Introduction
Drut is a linter, formatter, and editor tooling suite for Cube Voyager
control-statement scripts (.s / .block files) — the script language
transportation planners use to write travel-demand model logic.
Editing these scripts without Drut usually means no syntax highlighting, no error checking until the model actually runs, and no consistent formatting across a team. Drut catches structural mistakes as you type, keeps scripts formatted consistently, and adds real editor support — syntax highlighting, hover help, autocomplete — to VS Code and any other editor that speaks the Language Server Protocol.
What Drut does
- Structural diagnostics — unmatched
IF/LOOP/RUN/PROCESSblocks, unclosed comments, and other real mistakes, flagged before you run the model. See the Editor Guide for the full list. - Formatting — consistent indentation and, optionally, keyword casing, operator spacing, and blank-line normalization — on save, on paste, or from the CLI. See the Formatter Guide.
- Hover, autocomplete, and “did you mean” spell-check for control words and keyword names.
- Shared project configuration via a
drut.tomlfile, so a team doesn’t need to agree on CLI flags or editor settings individually. See the Configuration Reference. - Three ways in: a
drutCLI, a VS Code/Open VSX extension (built on the Language Server Protocol, so it works in any LSP-capable editor, not only VS Code), and a Model Context Protocol server for AI coding assistants — all built on one shared engine, so every surface agrees on what’s valid.
What Drut does not do
Drut is a structural and formatting tool, not a full semantic validator. It does not:
- Validate program-box-specific keyword combinations (e.g. whether a particular
PATHLOADparameter combination is meaningful for the model you’re running). - Check cross-file/repo-wide semantics beyond the direct
READ FILEinclusion hover already reaches. - Run or simulate your model in any way.
These are explicit, current scope boundaries — not oversights.
Who this is for
Anyone writing or reviewing Cube Voyager .s/.block scripts: transportation
model developers, analysts maintaining a shared script library, and teams that
want consistent formatting without hand-enforcing a style guide.
Where to go next
- New to Drut? Start with Install, then Getting Started.
- Looking for a specific
drut.tomlfield? Jump straight to the Configuration Reference. - Integrating an AI coding assistant? See the MCP Guide.
Install
Pick one or both — they’re independent, and the extension doesn’t require you to separately install the CLI.
VS Code / any VS Code-compatible editor (recommended for editing)
Install Drut for Cube Voyager from the VS Code Marketplace, or from Open VSX on VS Code-compatible editors that use it instead (Cursor, VSCodium, and similar).
Nothing else to install. On first activation, the extension resolves a
working drut language server binary automatically: it checks PATH first,
then its own persistent extension storage from a prior activation, then — if
neither is present — downloads the correct binary for your platform from the
latest GitHub Release and verifies it against its published SHA-256 checksum
before trusting it. If every option is unavailable (offline, an unsupported
platform, a failed download), the extension degrades to syntax-highlighting-only
rather than failing outright, and tells you why once.
Once installed this way, a throttled (at most once per 24 hours), non-blocking background check offers a dismissible notification when a newer release is available — it never silently replaces a running binary.
Just the CLI (for scripting or CI)
cargo install drut-cli
Or build from source:
cargo build --release -p drut-cli
# binary at target/release/drut(.exe) -- put it on PATH
Confirm it’s working:
drut --help
Continue to Getting Started to run it against a real script.
Getting Started
This walks through the CLI against a small sample script. If you’re only using
the VS Code/Open VSX extension, the same check/format behavior happens
automatically as you type and save — skip ahead to the
Editor Guide.
1. A sample script
Save this as sample.s:
run pgm=matrix
mati=base.mat,mo=out.mat
if(i==1)
ZONES = 100
endif
endrun
It’s structurally valid Cube Voyager — but the indentation is inconsistent (2
spaces under RUN, then 7 under IF) and IF(i==1) has no space before its
condition.
2. Check it
drut check sample.s
Expected output: nothing, and an exit code of 0. check only prints when
it finds a real structural problem (an unmatched IF/LOOP/RUN/PROCESS, an
unclosed comment, and similar) — a clean script produces no output at all, the
same way a passing test suite often does.
3. See what formatting would change
drut format sample.s --diff --isolated
(--isolated skips drut.toml discovery for this one run, so the output below
is reproducible regardless of any config file elsewhere on your machine — see
the Configuration Reference for what --isolated
skips.)
Expected output:
--- sample.s
+++ sample.s
@@ -1,6 +1,6 @@
run pgm=matrix
- mati=base.mat,mo=out.mat
- if(i==1)
- ZONES = 100
- endif
+ mati=base.mat,mo=out.mat
+ if(i==1)
+ ZONES = 100
+ endif
endrun
Every nested line is now indented to a consistent 4 spaces per level (the
built-in default — see indent_width
to change it). Nothing about the script’s meaning changed — no line was
reordered, no keyword was invented or removed.
4. Write the change
drut format sample.s --write --isolated
Reformats sample.s in place. --write, --check (report which files would
change, write nothing), and --diff (shown above) are mutually exclusive — see
the CLI Reference for the full flag list, including
--casing-control-words, --operator-spacing, and every other formatting axis.
Next steps
- Set up your editor for live diagnostics/formatting: Editor Guide.
- Share these settings with your team via
drut.toml: Configuration Reference. - Wire an AI coding assistant to Drut: MCP Guide.
CLI Reference
drut has four subcommands: check, format, server, and mcp.
check
drut check <PATH>
Reports every structural diagnostic found for each .s/.block file under
<PATH> (a single file or a directory, scanned recursively). Prints nothing and
exits 0 for a clean run.
| Flag | Values | Default | Effect |
|---|---|---|---|
--format | text, sarif | text | Output format. sarif emits SARIF 2.1.0, for CI/tooling integration. |
format
drut format <PATH> [flags]
Normalizes whitespace (and, opt-in, keyword casing/operator spacing/blank-line
runs) for each .s/.block file under <PATH>. With none of --write,
--check, or --diff, defaults to printing the reformatted result to stdout.
Disposition flags (mutually exclusive):
| Flag | Effect |
|---|---|
--write | Overwrite each matched file in place. |
--check | Report which files would change; write nothing. |
--diff | Print a unified diff per changed file; write nothing. |
Formatting-axis flags — every one of these mirrors a drut.toml [format]
field one-to-one; see the Configuration Reference
for accepted values, defaults, and what each actually does:
| Flag | drut.toml field |
|---|---|
--casing-control-words | casing_control_words |
--casing-pair-keywords | casing_pair_keywords |
--casing-data-references | casing_data_references |
--casing-function-calls | casing_function_calls |
--indent-top-level | indent_top_level |
--indent-width | indent_width |
--operator-spacing | operator_spacing |
--blank-lines | blank_lines |
--blank-lines-top-cap | blank_lines_top_cap |
--blank-lines-nested-cap | blank_lines_nested_cap |
--line-wrap | line_wrap |
--line-wrap-width | line_wrap_width |
--line-wrap-style | line_wrap_style |
An explicit flag here always wins over drut.toml and any editor setting for
that one run — see the Configuration Reference’s
Precedence section.
Other flags:
| Flag | Effect |
|---|---|
--isolated | Skip drut.toml discovery entirely for this run — built-in defaults plus whatever other flags you passed. Useful for CI reproducibility or a one-off sanity check. |
server
drut server
Speaks the Language Server Protocol over stdio. No flags — launched by an LSP client (like the VS Code extension), not run interactively. See the Editor Guide.
mcp
drut mcp
Speaks the Model Context Protocol over stdio. No flags — launched by an
MCP-capable client (an AI coding assistant), not run interactively. Exposes four
read-only tools, entirely independent of server above (no shared state). See
the MCP Guide.
Editor (LSP) Guide
Everything here comes from drut server, launched automatically by the VS
Code/Open VSX extension — and, since it’s a standard Language Server Protocol
implementation, usable from any LSP-capable editor, not only VS Code.
Diagnostics
Two kinds of diagnostics are published, at different severities.
Structural diagnostics (real problems, Error severity) — seven categories,
covering unmatched blocks and a few other real structural defects:
| Diagnostic | Fires on |
|---|---|
UnmatchedIf | An IF with no matching ENDIF, or a dangling ENDIF/ELSEIF/ELSE. |
UnmatchedLoop | A LOOP with no matching ENDLOOP, or a dangling ENDLOOP. |
UnclosedBlockComment | A block comment with no matching */ before end of file. |
InvalidContinuation | A continuation character with no valid following line. |
UnmatchedRun | A non-disabled RUN with no ENDRUN and no implicit closer (a following RUN or shell-escape statement), a disabled !RUN missing its required explicit ENDRUN, or a dangling ENDRUN. |
UnmatchedProcess | A PROCESS/PHASE= with no matching ENDPROCESS/ENDPHASE and no following PROCESS/PHASE= (the legitimate implicit-close pattern). |
MisplacedBreak | A BREAK with no enclosing block of any kind. |
(An eighth category, InvalidEncoding, exists in voyager-core for raw-byte
input but is unreachable through live editing — the LSP transport only ever
delivers already-decoded text.)
Hint-level diagnostics (best-effort signals, not hard errors, Hint
severity) — three of these, each its own distinct source so they’re visually
and programmatically distinguishable from the structural set above:
| Diagnostic | Source | Fires on |
|---|---|---|
Unclosed ; FMT: OFF | drut-fmt | A ; FMT: OFF marker with no matching ; FMT: ON before end of file — the rest of the file stays unformatted, and this tells you why. |
Malformed drut.toml | drut-config | An unrecognized key or an out-of-range value in the resolved drut.toml — formatting still completes using the built-in default for just that field. |
Undefined @token@ | drut-token | An @token@ reference with no assignment findable in the same file or a directly included one. Never a hard error — a resolver blind spot (a reference on a block-opener line, more than one level of READ FILE inclusion, or a token-built inclusion path) is never itself treated as evidence the token doesn’t exist; it may still be defined somewhere Drut can’t see. |
Hover
Hovering a block keyword (IF, LOOP, RUN, …) shows its kind and where its
matched counterpart is — correctly resolved even through RUN/PROCESS’s
implicit-close quirk. Hovering an @token@ reference shows the value it
currently resolves to and where that value was assigned (the most recent
same-file assignment before the reference, or one found via a directly-included
READ FILE).
Completion and spell-check
Autocomplete for control words and keyword=value pair names is scoped to the
enclosing control word (e.g. completing inside RUN PGM=... only offers
PGM-relevant pair keywords). A misspelled keyword gets a “did you mean”
suggestion riding on the same hover mechanism.
Folding
Every block kind (IF/LOOP/RUN/PROCESS/JLOOP/LINKLOOP/
DISTRIBUTEMULTISTEP) and block comment can be collapsed/expanded like any
other language.
Format-on-save and format-on-paste
Format-on-save is auto-enabled the first time the extension activates in a
workspace (workspace-scoped, one-time — it won’t silently turn itself back on
if you disable it afterward). Saving a .s/.block file reformats it
automatically.
Format-on-paste stays off by default. Turn it on with:
{
"[drut]": {
"editor.formatOnPaste": true
}
}
in your workspace’s .vscode/settings.json. Once enabled, pasting Cube Voyager
script text into a .s/.block file reindents it to match its new surrounding
structure immediately — correctly handling a paste that opens or closes a
block.
Syntax highlighting
Static TextMate-grammar highlighting (works immediately, before the language server even attaches) recognizes these categories:
| Category | Covers | Scope |
|---|---|---|
| Control words | IF, LOOP, RUN, ENDIF, … | keyword.control.drut |
| Statement words | PRINT, FILEI, FILEO, ARRAY, … | support.function.statement.drut |
| Function calls | A recognized Cube Voyager built-in function name immediately followed by ( — REPLACESTR(...), ROUND(...), and 136 others (see the Formatter Guide for the full list) | support.function.builtin.drut |
| Pair-keyword names | A keyword=value pair’s keyword, e.g. PATHLOAD’s PATH | variable.parameter.drut |
| Values | A pair’s bareword value, e.g. PGM=MATRIX’s MATRIX | constant.other.drut |
| Data references | The Matrix/Line/Node/Zone/Database family (MI, MW, DBA, ZONES, …), by name, regardless of position | variable.language.data-reference.drut |
| User variables | Any other bareword identifier not covered by a category above | variable.other.identifier.drut |
@name@ substitution | Variable references | variable.other.readwrite.drut, plus a semantic-token variable override (below) |
| Numbers | Numeric literals | constant.numeric.drut |
| Operators | =, +, -, <>, … | keyword.operator.drut |
| Comments | ; ... and /* ... */ | comment.line.semicolon.drut / comment.block.drut |
| Strings | Quoted string literals | string.quoted.single.drut / string.quoted.double.drut |
Function calls and statement words render in the same color by default (both use the generic “built-in procedure” convention most themes already style), but are independently recognized and independently colorable — see Highlight color customization below.
@name@ references also always get a real color, not just whatever a theme
happens to assign — the extension auto-seeds a #4EC9B0 semantic-token
override the first time it activates in a workspace, since some themes render
that TextMate scope with no color at all. This seed is workspace-scoped and
one-time only: deleting it from .vscode/settings.json by hand keeps it
deleted, forever, for that workspace (the extension never fights that choice
back) — unless you configure drut.highlight.namedVariables (below).
Editor client settings
All 13 [format] fields (see the Configuration Reference)
are also available as personal VS Code settings, not only via a project’s
committed drut.toml:
| Setting | drut.toml field |
|---|---|
drut.format.casingControlWords | casing_control_words |
drut.format.casingPairKeywords | casing_pair_keywords |
drut.format.casingDataReferences | casing_data_references |
drut.format.casingFunctionCalls | casing_function_calls |
drut.format.indentTopLevel | indent_top_level |
drut.format.indentWidth | indent_width |
drut.format.operatorSpacing | operator_spacing |
drut.format.blankLines | blank_lines |
drut.format.blankLinesTopCap | blank_lines_top_cap |
drut.format.blankLinesNestedCap | blank_lines_nested_cap |
drut.format.lineWrap | line_wrap |
drut.format.lineWrapWidth | line_wrap_width |
drut.format.lineWrapStyle | line_wrap_style |
Set these through VS Code’s built-in Settings UI (search for “drut”), or
directly in settings.json. A drut.toml value always wins over a
conflicting client setting for the same field — a client setting is a personal
fallback default, never a way to override a project’s own committed
configuration. See the Configuration Reference’s
Precedence section for the full
four-tier chain. A changed setting takes effect on the very next format request
against an already-open document — no reopen or editor restart needed.
Highlight color customization
Unlike the [format] fields above, drut.highlight.* settings are VS Code
personal settings only — there is no drut.toml equivalent, no CLI flag, no
MCP parameter. Color is a personal/accessibility preference (theme,
colorblindness, monitor), not a shared file-content convention the way casing
or indentation is, so there’s nothing to put in a committed project file.
Eleven settings, one per category from the Syntax highlighting table above
(@name@ excepted — see below), each an optional CSS color
(e.g. #RRGGBB):
| Setting | Colors |
|---|---|
drut.highlight.controlWords | Control words |
drut.highlight.statementWords | Statement words |
drut.highlight.functionCalls | Function calls |
drut.highlight.pairKeywords | Pair-keyword names |
drut.highlight.values | Values |
drut.highlight.dataReferences | Data references |
drut.highlight.userVariables | User variables |
drut.highlight.numbers | Numbers |
drut.highlight.operators | Operators |
drut.highlight.comments | Comments |
drut.highlight.strings | Strings |
Leaving any of these unset keeps your color theme’s own choice for that category — setting one takes effect immediately (no window reload), and clearing it afterward reverts to the theme’s color, not a stuck last value. None of these ever touch a rule they didn’t add themselves — another extension’s customizations, or your own hand-written ones, always survive untouched.
A bareword immediately before = always renders under pairKeywords, even
if it’s also a userVariables-shaped identifier (LINKID in
LINKID = _ANode) — this grammar has no real parse tree to tell a
keyword-pair’s own name apart from an ordinary assignment’s target variable.
The bareword
immediately after = renders under pairValues only when it’s the
entire right-hand side, with nothing else following (X = _ANode alone) —
that shape is genuinely indistinguishable from a keyword-pair’s own value
(PGM=MATRIX’s MATRIX) without a real parse tree. As soon as anything
else follows on the same right-hand side — another operand, an operator, a
string — the whole expression falls to userVariables instead, so
LINKID = _ANode + '_' + _BNode’s _ANode and _BNode render identically
(neither is a real keyword-pair value). dataReferences is the one
exception to the adjacency rule entirely:
a recognized data-reference name always wins that category even when it’s
also pair-keyword-shaped (ZONES in RUN PGM=MATRIX ZONES=5 renders under
dataReferences, not pairKeywords).
drut.highlight.namedVariables (@name@ substitution) works the same way
from a user’s perspective, but is written into the current workspace’s
settings (.vscode/settings.json), not your personal/global settings — VS
Code resolves this particular setting per-scope rather than merging across
scopes, and the auto-seeded default described above already lives at
workspace scope, so a global-scope write would be silently invisible.
Leaving it unset preserves the auto-seed behavior exactly (including “a
manual deletion sticks forever”); setting it takes over live; clearing it
afterward reverts to the #4EC9B0 default specifically, not to no color at
all (a fully theme-driven state would reintroduce the invisibility problem
this default exists to prevent).
MCP Guide
drut mcp speaks the Model Context Protocol
over stdio — a way for an AI coding assistant to query Drut’s understanding of a
Cube Voyager script directly, instead of guessing from raw text. All four tools
are read-only: none of them write to disk.
Launch it the same way any MCP client launches a stdio server: point your
client’s MCP configuration at the drut binary with the mcp argument.
diagnose
Reports every structural diagnostic voyager-core can find for a script (given
as inline text or a file path). Returns an empty list for a structurally valid
script. Same diagnostic categories as the Editor Guide’s
structural set — the Hint-level streams (unclosed ; FMT: OFF, malformed
drut.toml, undefined @token@) are LSP-only and never appear here.
Use it when: an assistant needs to check whether a script (or a change it’s about to propose) is structurally valid before suggesting it.
format
Reformats a script’s whitespace/indentation (and, opt-in via the same
parameters drut format/drut.toml accept — casing_control_words,
operator_spacing, blank_lines, and the rest) and returns the result plus
whether anything changed. Idempotent: formatting an already-formatted script
reports changed=false.
Use it when: an assistant is about to write or hand back Cube Voyager script text and wants it consistently formatted first.
query_structure
Reports which of the seven block kinds (If/Loop/Run/Process/JLoop/
LinkLoop/DistributeMultistep), if any, encloses a given 1-based line/column
position in a script, and where its matched counterpart is — correctly resolved
even through Run/Process’s implicit-close quirk. Reports kind: null, not
an error, when no block encloses the position.
Use it when: an assistant needs to understand the block structure around a
specific position — e.g. “is this line inside an IF, and if so, where does
that IF end?”
lookup_keyword
Looks up real, corpus-evidenced keyword=value pair-name candidates for a given
enclosing control word (e.g. RUN), falling back to the general control-word
list when none is given. Optionally also runs a “did you mean” spell-check
against a supplied token.
Use it when: an assistant is generating or validating a keyword=value pair
and wants to confirm the keyword name is real (or find the likely intended one
for a typo).
Formatter Guide
What formatting guarantees
The formatter is idempotent — running it twice produces the same result as
running it once (format(format(x)) == format(x)). It is strictly
behavior-preserving: it never reorders statements, never changes which lines
are continuations of a prior statement, and never alters program meaning. It
only ever changes whitespace and, opt-in, keyword casing. If a script is
structurally broken (see the Editor Guide’s
diagnostic list), the formatter still does its best on the parts it understands
rather than refusing outright — but a diagnosed/unmatched block’s own children
are left with their original indentation rather than guessed at.
Every field below is documented in full in the Configuration Reference — this page focuses on what changes, with real examples.
Casing
The four independent casing fields —
casing_control_words,
casing_pair_keywords,
casing_data_references,
and casing_function_calls —
only ever touch keyword names — never a value, and never a category they
aren’t scoped to. This example makes several boundaries visible at once, with
casing_control_words = "upper":
-run pgm=matrix
+RUN pgm=matrix
mati=base.mat,mo=out.mat
-endrun
+ENDRUN
run/endrun (control words) are uppercased — but pgm (a pair-keyword name,
casing_pair_keywords’s own scope, left unset here), matrix (a value, never
touched), and mati/mo (data-reference tokens,
casing_data_references’s
own scope, also left unset here) all stay exactly as written. Each field only
ever affects its own category — set the one(s) you want independently.
Function-call casing
casing_function_calls normalizes a recognized Cube Voyager built-in function
name’s casing, but only where it’s immediately followed by ( with no
intervening whitespace — the unambiguous call position, since Voyager has no
user-definable functions:
-RouteName = replacestr(RouteName,'-','',0)
+RouteName = REPLACESTR(RouteName,'-','',0)
The recognized list (138 names) spans the general Control Language core
(ABS, TRIM, REPLACESTR, ROUND, …), Highway/Matrix-program functions
(ROWSUM, PATHTRACE, …), Public Transport skim functions (TIMEA,
BRDINGS, GCOST, …), the CONVERGE-phase iteration-statistics family
(GAPCHANGE, RGAPMIN, …), and CUBE Cluster utility functions — the same
list the VS Code extension’s syntax highlighting uses (see the
Editor Guide).
Two real names exist as more than one thing in Cube Voyager: FORMAT is also
a FILEO pair-keyword, and LOG is also a control word. Each occurrence’s
own position decides which field governs it — never both, never neither:
[format]
casing_pair_keywords = "upper"
casing_function_calls = "lower"
-FILEO format=csv
+FILEO FORMAT=csv
-X = FORMAT(volume,8,2,',')
+X = format(volume,8,2,',')
format on the first line is a pair-keyword name (followed by =), governed
by casing_pair_keywords alone. FORMAT on the second line is a function
call (followed by (), governed by casing_function_calls alone.
Indentation
indent_top_level controls
depth-0 statements; indent_width
controls spacing per nesting level inside a block, relative to the block’s own
opening-statement column. See Getting Started
for a full before/after example.
Operator spacing
preserve (the default) leaves spacing exactly as written. fixed normalizes
every operator to exactly one space on each side and removes interior padding
inside brackets/parens:
-IF(ZONES==1)
- ZONES=1
- CNT=2
- ITER=333
+IF(ZONES == 1)
+ ZONES = 1
+ CNT = 2
+ ITER = 333
ENDIF
auto does everything fixed does, plus vertically aligns the = of
consecutive Assignment statements at the same nesting depth to the longest
left-hand side in the run:
-IF(ZONES==1)
- ZONES=1
- CNT=2
- ITER=333
+IF(ZONES == 1)
+ ZONES = 1
+ CNT = 2
+ ITER = 333
ENDIF
A run resets at a blank line, a comment-only line, a nesting-depth change, or a
non-Assignment statement — so alignment never reaches across unrelated
sections of a script.
Blank-line normalization
preserve (the default) leaves every run of consecutive blank lines exactly as
written, however long. auto contracts a run down to the applicable cap
(blank_lines_top_cap
between top-level statements/blocks, default 2;
blank_lines_nested_cap
inside any block’s body, default 1) — only when a run exceeds the cap, never
padding a shorter run up:
RUN PGM=MATRIX
-
MATI=a.mat
-
MATO=b.mat
-
-
ENDRUN
Line wrapping
preserve (the default) leaves every line exactly as written, however long.
auto wraps an over-width Control statement’s keyword=value pair list
across multiple physical lines once it exceeds
line_wrap_width (default
120) — using Cube Voyager’s own existing line-continuation syntax, the same
trailing comma that already makes the next physical line a continuation of the
same statement. Only Control statements are eligible; an Assignment
statement’s arithmetic/string expression is never touched by this feature.
line_wrap_style decides how
pairs are distributed across the new continuation lines. fill (the default)
packs as many pairs as fit per line:
-RUN PGM=MATRIX, ZONES=5, PRINT=1, COMBINE=T
+RUN PGM=MATRIX, ZONES=5, PRINT=1,
+ COMBINE=T
one_per_line places exactly one pair per continuation line instead, however
much width is left over on any given line:
-RUN PGM=MATRIX, ZONES=5, PRINT=1, COMBINE=T
+RUN PGM=MATRIX,
+ ZONES=5,
+ PRINT=1,
+ COMBINE=T
(Both examples above use a narrowed line_wrap_width = 40 so the wrap is
visible at doc-page width — the real default is 120.)
A statement that already contains a continuation character anywhere — i.e. you already hand-wrapped it — is left completely untouched, regardless of width:
RUN PGM=MATRIX,
ZONES=5, PRINT=1, COMBINE=T
stays exactly as written under line_wrap = "auto", even though the combined
statement is well over 40 characters. This is deliberate, not a missed case:
it’s the safest boundary for an output-modifying transform (no fighting
hand-formatted content) and it’s also what makes the feature idempotent by
construction — once auto wraps a statement, the wrapped result itself
contains a continuation character, so a second format pass sees “already
continued” and leaves it alone. format(format(x)) == format(x) holds without
needing to re-derive it from scratch.
; FMT: OFF / ; FMT: ON regions
Wrap a range in ; FMT: OFF / ; FMT: ON to exclude it from formatting
entirely — useful for a block whose hand-tuned spacing carries meaning to a
reviewer that automatic formatting would otherwise flatten:
-RUN PGM=MATRIX
+RUN PGM = MATRIX
; FMT: OFF
ZONES=1
; FMT: ON
- MATI=a.mat
+ MATI = a.mat
ENDRUN
Everything between the markers (ZONES=1 above) is untouched, while the lines
outside them still get operator_spacing = "fixed" applied. An unclosed
; FMT: OFF (no matching ; FMT: ON before end of file) protects through the
rest of the file and is always surfaced — never silently unbounded with no
indication — as a Hint diagnostic (source drut-fmt, see the
Editor Guide), a CLI stderr notice, or an MCP
format response field, depending on which surface you’re using.
Configuration Reference
Every [format] field Drut currently understands, in one place. All of them are
set the same way, in a drut.toml file at (or above) the file you’re formatting:
[format]
casing_control_words = "lower"
indent_width = 2
Drut discovers the nearest drut.toml by walking up from the file being
processed, stopping at the first drut.toml found, a .git boundary, or the
filesystem root — whichever comes first. A project with no drut.toml anywhere
behaves exactly like a project with an empty one: every field uses its built-in
default. Every field is optional; omitting a key is identical to writing its
default value explicitly.
Starter drut.toml
Every field, commented out at its built-in default. Copy this into a drut.toml
at your project root and uncomment (then change) only the fields you want to
override — a commented-out line changes nothing, so you never need to remember
the full field list or its defaults from scratch:
[format]
# casing_control_words = "preserve" # preserve | upper | lower
# casing_pair_keywords = "preserve" # preserve | upper | lower
# casing_data_references = "preserve" # preserve | upper | lower
# casing_function_calls = "preserve" # preserve | upper | lower
# indent_top_level = "preserve" # preserve | auto
# indent_width = 4 # 1-16
# operator_spacing = "preserve" # preserve | fixed | auto
# blank_lines = "preserve" # preserve | auto
# blank_lines_top_cap = 2 # 1-50, only used when blank_lines = "auto"
# blank_lines_nested_cap = 1 # 1-50, only used when blank_lines = "auto"
# line_wrap = "preserve" # preserve | auto
# line_wrap_width = 120 # 20-500, only used when line_wrap = "auto"
# line_wrap_style = "fill" # fill | one_per_line, only used when line_wrap = "auto"
See Fields below for what each one actually does, and Precedence for how a set value interacts with CLI flags, MCP parameters, and editor settings.
A malformed value never blocks formatting. An unrecognized key or an
out-of-range value only affects that one field — it warns (CLI stderr, an LSP
Hint diagnostic, or the MCP format tool’s config_warnings field) and falls
back to that field’s built-in default. Every other valid setting in the same file
still applies.
Precedence
Every field below resolves the same four-tier way, checked in this order — the first tier that sets a value wins:
- An explicit CLI flag or MCP tool parameter, passed for one specific invocation — always wins when given.
drut.toml— the nearest one found by the discovery walk above.- An editor client setting — for VS Code, one of the
drut.format.*settings under Settings; delivered todrut-lspvia the standard LSPworkspace/configurationmechanism. A personal editor preference never overrides a project’s own committeddrut.toml— it only fills in a fielddrut.tomlleaves unset. - The built-in default — used only if none of the above set the field.
Each tier only fills in a field the tier(s) before it left unset; a field is never assembled from pieces at different tiers. Every field below resolves this same plain four-tier chain — no field has any extra fallback wrinkle.
A flat
casingfield once existed, coveringcontrol_words+pair_keywordstogether — removed once the three granular fields below fully superseded it. Adrut.toml/CLI/MCP/editor-setting still usingcasingno longer does anything; it degrades exactly like any other unrecognized key (a warning, falling back to each field’s own built-in default), never a hard failure.
Fields
casing_control_words
Casing convention for the control_words category (things like IF,
ENDIF, LOOP, ENDLOOP).
Values: preserve ← default, upper, lower.
Default: preserve.
Also known as: CLI flag --casing-control-words; MCP format tool parameter
casing_control_words.
Example:
[format]
casing_control_words = "upper"
Precedence: follows the four-tier chain above.
casing_pair_keywords
Casing convention for the pair_keywords category (keyword names inside
a Control statement’s keyword=value pairs, e.g. PATHLOAD, MATI), same
shape as casing_control_words above.
Values: preserve ← default, upper, lower.
Default: preserve.
Also known as: CLI flag --casing-pair-keywords; MCP format tool parameter
casing_pair_keywords.
Example:
[format]
casing_pair_keywords = "lower"
Precedence: follows the four-tier chain above.
casing_data_references
Casing for the data-reference category: Matrix/Line/Node/Zone/Database
abbreviations (MI/MO/MW, LI/LW, NI/NW, ZI/ZONES/Z,
DBI/DBA), RO, the link-endpoint fields A/B, and the reserved loop-index
identifiers I/J.
Values: preserve ← default, upper, lower.
Default: preserve.
Also known as: CLI flag --casing-data-references; MCP format tool
parameter casing_data_references.
Example:
[format]
casing_data_references = "lower"
Precedence: follows the four-tier chain above.
casing_function_calls
Casing for recognized Cube Voyager built-in function names (e.g. REPLACESTR,
ROUND, TRIM) — only a name immediately followed by ( with no intervening
whitespace counts as a call; the same name elsewhere (a keyword=value pair
name, a plain identifier) is untouched by this field. Covers 138 names spanning
the general Control Language core (Numeric/Trig/Character-String functions),
Highway/Matrix-program functions, Public Transport skim functions, the
CONVERGE-phase iteration-statistics family, and CUBE Cluster utility functions —
see the Formatter Guide for the full
list and how two real names (FORMAT, LOG) that also exist as a pair-keyword/
control word respectively are disambiguated by position.
Values: preserve ← default, upper, lower.
Default: preserve.
Also known as: CLI flag --casing-function-calls; MCP format tool
parameter casing_function_calls.
Example:
[format]
casing_function_calls = "upper"
Precedence: follows the four-tier chain above.
indent_top_level
Whether top-level (depth-0, not inside any block) statement indentation is left exactly as written, or normalized to column 0.
Values:
preserve← default — leave top-level indentation exactly as written.auto— force every top-level line to column 0.
Default: preserve.
Also known as: CLI flag --indent-top-level; MCP format tool parameter
indent_top_level.
Example:
[format]
indent_top_level = "auto"
Precedence: follows the four-tier chain above.
indent_width
Spaces per nesting level of block indentation, relative to the enclosing block’s own opening-statement column.
Values: any integer from 1 to 16 — default 4.
Default: 4.
Also known as: CLI flag --indent-width; MCP format tool parameter
indent_width.
Example:
[format]
indent_width = 2
Precedence: follows the four-tier chain above. An
out-of-range value (0, 500, …) at any tier is
treated as unset for that tier — resolution falls through to the next tier
exactly as if the field had been omitted there.
operator_spacing
Whitespace normalization around =, comparison operators (==, <>, >=,
<=, <, >), binary arithmetic (+, -, *, /), comma spacing between
multiple keyword=value pairs, and interior padding inside [...]/(...).
Values:
preserve← default — leave existing spacing exactly as written.fixed— normalize every occurrence to exactly one space on each side (and zero interior padding inside brackets/parens), independent of neighboring lines.auto— everythingfixeddoes, plus vertically aligns the=of consecutiveAssignmentstatements at the same nesting depth to the column of the longest left-hand side in the run. A run resets at a blank line, a comment-only line, a nesting-depth change, or a non-Assignmentstatement.
Default: preserve.
Also known as: CLI flag --operator-spacing; MCP format tool parameter
operator_spacing.
Example:
[format]
operator_spacing = "auto"
See the Formatter Guide for full
before/after examples of fixed vs. auto.
Precedence: follows the four-tier chain above.
blank_lines
Whether runs of consecutive blank lines (including whitespace-only lines) are left as written or contracted down to a configured cap.
Values:
preserve← default — leave every blank-line run exactly as written, however long.auto— contract a run down to the applicable cap (blank_lines_top_caporblank_lines_nested_cap) only when the run exceeds that cap — never pads a shorter run up.
Default: preserve.
Also known as: CLI flag --blank-lines; MCP format tool parameter
blank_lines.
Example:
[format]
blank_lines = "auto"
Precedence: follows the four-tier chain above.
blank_lines_top_cap
The maximum number of consecutive blank lines blank_lines = "auto" allows
between top-level statements/blocks before contracting the run. Only meaningful
when blank_lines is "auto".
Values: any integer from 1 to 50 — default 2.
Default: 2.
Also known as: CLI flag --blank-lines-top-cap; MCP format tool
parameter blank_lines_top_cap.
Example:
[format]
blank_lines = "auto"
blank_lines_top_cap = 1
Precedence: follows the four-tier chain above. An
out-of-range value at any tier is treated as unset for
that tier, same as indent_width.
blank_lines_nested_cap
The maximum number of consecutive blank lines blank_lines = "auto" allows
inside any block’s own body, uniformly regardless of nesting depth, before
contracting the run. Only meaningful when blank_lines is
"auto".
Values: any integer from 1 to 50 — default 1.
Default: 1.
Also known as: CLI flag --blank-lines-nested-cap; MCP format tool
parameter blank_lines_nested_cap.
Example:
[format]
blank_lines = "auto"
blank_lines_nested_cap = 2
Precedence: follows the four-tier chain above. Same
out-of-range handling as
blank_lines_top_cap.
line_wrap
Whether an over-width Control statement’s keyword=value pair list is wrapped
across multiple physical lines, using Cube Voyager’s own existing
line-continuation syntax (a trailing comma already tells the parser the next
physical line continues the same statement — no new syntax is introduced). Only
Control statements are eligible — an Assignment statement’s arithmetic/string
expression is out of scope. A statement that already contains a continuation
character anywhere is left completely untouched, however long, regardless of
width — never re-flowed, which is also what makes this field idempotent by
construction: once wrapped, a statement is “already continued” on the next pass.
Values:
preserve← default — leave every line exactly as written, however long.auto— wrap once a statement’s line length exceedsline_wrap_width, usingline_wrap_styleto decide how pairs are distributed across the new continuation lines.
Default: preserve.
Also known as: CLI flag --line-wrap; MCP format tool parameter
line_wrap.
Example (width lowered to 40 here only to keep the before/after readable on
this page — 120 is the real default):
[format]
line_wrap = "auto"
line_wrap_width = 40
Before:
RUN PGM=MATRIX, ZONES=5, PRINT=1, COMBINE=T
After, with line_wrap_style = "fill" (the default) — packs as many pairs as
fit per continuation line:
RUN PGM=MATRIX, ZONES=5, PRINT=1,
COMBINE=T
After, with line_wrap_style = "one_per_line" instead — exactly one pair per
continuation line:
RUN PGM=MATRIX,
ZONES=5,
PRINT=1,
COMBINE=T
See the Formatter Guide for this same example plus the already-hand-wrapped case.
Precedence: follows the four-tier chain above.
line_wrap_width
The maximum line width line_wrap = "auto" wraps toward — once
a Control statement’s line exceeds this many characters, it becomes a wrap
candidate. Only meaningful when line_wrap is "auto".
Values: any integer from 20 to 500 — default 120.
Default: 120.
Also known as: CLI flag --line-wrap-width; MCP format tool parameter
line_wrap_width.
Example:
[format]
line_wrap = "auto"
line_wrap_width = 100
Precedence: follows the four-tier chain above. An
out-of-range value at any tier is treated as unset for that tier, same as
indent_width.
line_wrap_style
How a wrapped statement’s keyword=value pairs are distributed across the new
continuation lines once line_wrap wraps it. Only meaningful when
line_wrap is "auto".
Values:
fill← default — greedily packs as many pairs as fit withinline_wrap_widthonto each continuation line before breaking.one_per_line— places exactly one pair per continuation line, regardless of how much width is left over on any given line.
Default: fill — chosen over one_per_line deliberately: a statement’s
wrap style, once applied, is never undone by a later format pass (see
line_wrap above), so fill is the cheaper direction to
manually diverge from afterward if you don’t like it for one specific
statement, versus starting from one_per_line and wanting to hand-compact it
back down.
Also known as: CLI flag --line-wrap-style; MCP format tool parameter
line_wrap_style.
Example:
[format]
line_wrap = "auto"
line_wrap_style = "one_per_line"
See line_wrap above for the before/after example showing both
styles side by side.
Precedence: follows the four-tier chain above.