# Formatting

> What brine fmt changes and what it leaves alone, brine fmt --check, and checking formatting in CI.

Source: https://docs.brinelang.org/tooling/formatting/

`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.

## Usage

[Section titled “Usage”](#usage)

```sh
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
```

```text
 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.

## Before and after

[Section titled “Before and after”](#before-and-after)

messy.br (before)

```brine
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)
```

```sh
brine fmt messy.br
```

messy.br (after)

```brine
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.

## What it changes

[Section titled “What it changes”](#what-it-changes)

| 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)

```brine
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.

## What it keeps

[Section titled “What it keeps”](#what-it-keeps)

* **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:

  ```brine
  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

```brine
# 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}")
```

## Safety

[Section titled “Safety”](#safety)

`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`:

```text
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.

## brine fmt –check

[Section titled “brine fmt –check”](#brine-fmt-check)

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

```sh
brine fmt --check
```

```text
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:

```text
{"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`       |

## In CI

[Section titled “In CI”](#in-ci)

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

```yaml
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

```sh
#!/bin/sh
exec brine fmt --check
```

## Related

[Section titled “Related”](#related)

* [The brine command](/tooling/cli/#brine-fmt)
* [Projects and brine.toml](/tooling/projects/)
