Skip to content

brine fmt rewrites source files in the one canonical Brine layout: 4-space indentation, one space around binary operators and after commas, at most one blank line in a row, and lines of at most 100 characters. It only moves whitespace: it never adds, removes or reorders a token, so a formatted program means exactly what it meant before. Run it before every commit; brine fmt --check fails a CI job when a file isn’t formatted.

Terminal window
brine fmt # every .br file in the project
brine fmt src/main.br lib/ # these files, and every .br file in these folders
brine fmt --check # change nothing; list unformatted files, exit 1 if any
Formatted messy.br
1 file checked, 1 reformatted

Without arguments, brine fmt formats every .br file under the project root (the folder with brine.toml), or under the current directory outside a project. It skips target/ and every folder whose name starts with ..

There are no options to configure. Every Brine file in every project looks the same, which is one less thing for people and agents to decide.

messy.br (before)
fn area(w:float,h :float)->float :
return w*h # width times height
struct Point :
x:int
y:int
fn main():
let xs=[1,2,3,]
var total=0
for i in 0..len(xs):
total+=xs[i]
if total>5 and not(total==7):
print( "big",total )
let p=Point(x=1,y=2)
print(area(2.0,3.0),p)
Terminal window
brine fmt messy.br
messy.br (after)
fn area(w: float, h: float) -> float:
return w * h # width times height
struct Point:
x: int
y: int
fn main():
let xs = [1, 2, 3,]
var total = 0
for i in 0..len(xs):
total += xs[i]
if total > 5 and not (total == 7):
print("big", total)
let p = Point(x=1, y=2)
print(area(2.0, 3.0), p)

Both versions compile and print the same thing. Note the trailing comma in [1, 2, 3,]: it’s a token, so brine fmt keeps it.

Rule Before After
Indentation is 4 spaces per block, whatever the input used return w*h return w * h
Tabs in indentation become 4 spaces a tab 4 spaces
One space around binary and assignment operators total+=xs[i] total += xs[i]
One space after , and after : in annotations; none before fn area(w:float,h :float) fn area(w: float, h: float)
-> has a space on each side; no space before a block’s : ->float : -> float:
No space inside brackets, or between a name and its call or index print( "big",total ) print("big", total)
No spaces around = in keyword arguments Point(x = 1) Point(x=1)
No spaces around : and .. in slices and ranges xs[0 : 2], 0 .. n xs[0:2], 0..n
Keywords are separated from what follows not(total==7) not (total == 7)
At most one blank line in a row three blank lines one
No blank line right after a line ending in :
One blank line before each top-level fn, struct, enum and test, and after a top-level block ends struct Point: right before fn main(): a blank line between them
Two spaces before a trailing comment w*h # width w * h # width
No trailing whitespace; the file ends with exactly one newline; \r\n becomes \n
An expression spread over several lines inside brackets is joined into one line sum( / x * x / for x in xs / ) sum(x * x for x in xs)
A line longer than 100 characters is split at its first bracket, one element per line see below

A long call:

long.br (after)
fn describe(name: str, age: int, city: str, country: str, email: str, phone: str) -> str:
return f"{name} {age} {city} {country} {email} {phone}"
fn main():
print(
describe("Ada Lovelace", 36, "London", "United Kingdom", "ada@example.com", "+44 20 7946 0000")
)

The line was split at the ( of print; each element of that bracket goes on its own line, indented one level, and the closing bracket returns to the original indentation. Only the first bracket is split, so a very long single element (here the describe(...) call) can still be over 100 characters.

  • Comments, exactly as written (only their indentation and the spacing before a trailing comment change). A comment on its own line takes the indentation of the code below it, unless it is indented deeper (then it stays with the end of the block above).

  • Literal spellings: 0xFF, 1_000_000, 1e-9, string escapes, f-string contents and format specs.

  • Every token: trailing commas, redundant parentheses and the order of everything stay.

  • Lines that have a comment in the middle: a bracketed list spread over lines with a comment after an element keeps its layout; only its indentation changes. This is the way to keep a hand-made layout:

    let ys = [
    1, # one
    2,
    ]

This file came out of brine fmt with its comment, hex literal, underscores and f-string spec untouched:

Kept as written
# Tools for points.
const LIMIT = 0xFF
fn main():
let big = 1_000_000 # keep the underscores
let xs = [1, 2, 3]
let ys = [x * 2 for x in xs if x > 1]
print(xs[0:2], -big, ys, LIMIT)
print(f"{big:>12}")

brine fmt works on tokens, not on the syntax tree, so comments and spellings survive. After formatting, it lexes the result again and compares the tokens with the input’s. If they differ in any way (which would be a bug in the formatter), it leaves the file as it was, apart from replacing tabs in indentation.

A file with a syntax error isn’t changed. The error is reported and the command exits with 1:

error[E0110]: expected `)`, found end of line
--> broken.br:3:1
|
3 |
| ^ expected `)`
= run `brine explain E0110` for more about this error
error: could not compile due to 1 error
1 file checked, 0 reformatted

Only syntax is checked: brine fmt doesn’t resolve imports or check types, so it formats files that brine check would still reject.

--check writes nothing. It prints each file that would change and exits with 1 if there is one:

Terminal window
brine fmt --check
would reformat messy.br
1 file checked, 1 need formatting

With --json, each file is a line of its own, and the summary isn’t printed:

{"unformatted":"messy.br"}
Result Exit code
everything formatted 0
a file would change (--check) 1
a file has a syntax error 1
a file can’t be read or written 2

Add a step that runs brine fmt --check after installing Brine. It needs no C compiler, so it can run before anything is built:

.github/workflows/ci.yml
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install brine
run: |
curl -fsSL https://raw.githubusercontent.com/brinelang/brine/main/install.sh | sh
echo "$HOME/.brine/bin" >> "$GITHUB_PATH"
- run: brine fmt --check
- run: brine check
- run: brine test

As a Git pre-commit hook:

.git/hooks/pre-commit
#!/bin/sh
exec brine fmt --check