pedantix
pedantix is the pedantic Nix formatter.
It runs your base formatter of choice (nixfmt, alejandra, nixpkgs-fmt, or an arbitrary command of your choosing) and then applies reordering on top. Every feature is optional.
On by default:
- ordering of function arguments
- ordering of attributes (comment blocks above the moved line stay attached)
Off by default:
- ordering of inherits (a case could be made that this should be on by default)
- ordering of let bindings
- ordering of lists
- merging of repeated keys (this does not fix broken evaluations)
- flattening of attribute sets with a single entry
- enforcing of blank lines in between attribute sets
- ordering overrides for custom attribute paths
Continue to Getting Started.
Getting started
Try it now
The following gives you a preview of your file, formatted using pedantix. You can run this from any machine that has nix installed:
$ nix run github:swarsel/pedantix < your-file.nix
Normally, you will probably want to format a file in place:
$ pedantix file.nix
With no file arguments (or a single -), pedantix reads from stdin and writes the result to stdout.
Installing
Add pedantix as a flake input:
{
inputs.pedantix.url = "github:swarsel/pedantix";
}
From there you can:
- use it with treefmt;
- run it as a pre-commit hook;
- manage the global config with home-manager;
- use it from Emacs.
A first configuration
pedantix works with zero configuration, but the whole idea is that you tune a pedantix.toml to your own pedantic tastes; a minimal one that turns on let sorting and sets a custom argument order could look like this:
formatter = "nixfmt"
[args]
first = ["self", "lib", "config", "pkgs"]
last = ["<defaulted>", "..."]
[lets]
sort = true
The full set of options is documented in Configuration.
How it works
pedantix parses Nix with tree-sitter and rewrites the syntax tree. A run works roughly like this:
- Perform an optional base formatting
- Perform an optional merging/flattening of attribute paths
- Sort expressions according to your config
- Perform an optional base formatting
- Optionally enforce spacing between attribute paths/sets
Safety
pedantixpreserves semantics, which should guarantee that it will not break your config- repeated runs are idempotent (the only exception would be if you enforce a higher line spacing than allowed by your base formatter)
- It stays compliant with your base formatter (unless you do what I described above)
Comments
When attributes are reordered, comments move with the binding they belong to, i.e. the binding below the comments. So in general a comment on its own line will travel along with the correct line of code.
Corollary:
- If you use comments to demarcate "sections" of sorts, that will obviously break when running
pedantix.- "Free-floating" comments for e.g. "archived" code will as a result move somewhat randomly.
The base formatter
There are a number of good Nix formatters out there and I considered it a foolish idea to introduce a new one into the ecosystem that nobody would want to adapt; iInstead, you can bring your own formatter and still use pedantix in any case.
If the default set of base formatters (nixfmt, alejandra, nixpkgs-fmt) is not to your liking, you can supply any command of your choosing that reads from stdin and emits to stdout. For safety reasons, the latter is only accepted when passed directly on the CLI or when passing --allow-formatter-command. See Configuration for details.
Command line
pedantix formats each file in place. With no files — or a single - —
it reads from stdin and writes the formatted result to stdout.
Usage
$ pedantix [OPTIONS] [FILES]...
Arguments
| Argument | Description |
|---|---|
[FILES]... | Files to format in place. With no files (or -), reads stdin and writes the result to stdout |
Options
| Option | Description |
|---|---|
-c, --config <CONFIG> | Path to a config file (default: search for pedantix.toml or .pedantix.toml upwards from each file's directory, stopping at the git repository root — or immediately when there is no repository — then fall back to $XDG_CONFIG_HOME/pedantix/pedantix.toml) |
--config-toml <TOML> | Complete configuration as an inline TOML document; disables config file loading and discovery. Example: --config-toml 'formatter = "off"' |
--set <KEY.PATH=VALUE> | Set a single configuration value on top of the loaded config, using the same keys as pedantix.toml. Repeatable. Values are parsed as TOML. Examples: --set lets.sort=true --set 'args.first=["self"]' |
--allow-formatter-command | Execute a formatter-command found in an auto-discovered config file. Without this flag, only configs you name explicitly (--config, --config-toml, --set, the global XDG config) may specify a command to run; discovered pedantix.toml files are limited to the built-in formatter choices |
--check | Don't write anything; exit 1 if any file would be reformatted |
--formatter <FORMATTER> | Override the configured base formatter Possible values: nixfmt, alejandra, nixpkgs-fmt, off. |
--stdin-filepath <STDIN_FILEPATH> | Path the stdin content notionally comes from; used for config discovery in stdin mode |
Config discovery
When you don't pass --config or --config-toml, pedantix searches for apedantix.toml or .pedantix.toml in the same directory and searching up, stopping at the repo root (or immediately, if not in one). Otherwise it falls back to $XDG_CONFIG_HOME/pedantix/pedantix.toml.
In stdin mode, --stdin-filepath tells pedantix which directory to start that search from — this is how editor integrations get the same config a plain file run would.
formatter-command security
A config that specifies a formatter-command (an arbitrary stdin → stdout program) is only allowed to run when passed --config, --config-toml, --set via the CLI, or through the global XDG config. An auto-discovered pedantix.toml will not run an arbitrary command unless passed --allow-formatter-command.
Exit codes
| Code | Meaning |
|---|---|
0 | Success — files were formatted, or nothing needed reformatting when using --check. |
1 | With --check: at least one file would be reformatted. |
2 | Bad configuration, unreadable file, parse failure, etc. |
Examples
# Format two files in place
$ pedantix flake.nix hosts/foo.nix
# Check if any file is unformatted
$ pedantix --check $(git ls-files '*.nix')
# Additionally sort let bindings for one run
$ pedantix --set lets.sort=true file.nix
# Explicitly specify base formatter for one run
$ pedantix --formatter alejandra file.nix
Configuration
pedantix is configured with a TOML file (see config discovery).
Individual values can also be passed on the command line with --set, e.g. pedantix --set lets.sort=true file.nix, or the whole document inline with --config-toml.
preset
This applies a base layer of configuration which you can layer your own config atop of - see Presets.
preset = "nixos-module"
Reference
Top-level keys
| Key | Type | Default | Description |
|---|---|---|---|
format-after-sort | bool | true | Run the base formatter again after sorting. |
format-before-sort | bool | true | Run the base formatter before sorting. |
formatter | FormatterChoice | "nixfmt" | Base formatter run before and after sorting. |
formatter-command | list of string | — | Arbitrary stdin -> stdout command; overrides formatter. Because it runs an external program, it is only honored from configs named explicitly (--config, --config-toml, --set, the global XDG config); an auto-discovered config requires --allow-formatter-command. |
inherit-placement | InheritPlacement | "front" | Where inherit statements land relative to ordinary bindings. |
top-level-blank-lines | int | — | Exact number of blank lines between the bindings of the file's outermost attribute set. Unset keeps existing blank lines as-is. |
top-level-blank-lines-depth | int | 1 | How deep the top-level spacing reaches: 1 is only the outermost set, 2 also covers the sets its bindings define, and so on. In flake.nix, inputs and outputs always count as top-level sets. |
top-level-blank-lines-mode | BlankLinesMode | "multiline" | Which top-level bindings receive the blank-line spacing. |
Construct rules ([args], [attrs], [lets], [inherits], [lists])
The
merge,flatten, andblank-lines*keys only apply to[attrs].
| Key | Type | Default | Description |
|---|---|---|---|
blank-lines | int | — | (attrs only) Number of blank lines between the set's bindings. Unset keeps the existing spacing. |
blank-lines-depth | int | 1 | (attrs only) How deep the spacing reaches; wrappers such as functions and lets do not count as levels. |
blank-lines-mode | BlankLinesMode | — | (attrs only) Which bindings receive the blank-line spacing. |
first | list of string | [] | Names pinned to the front, in the given order. The sentinel "<defaulted>" stands for all arguments with a default value (name ? value); "..." is the ellipsis. |
flatten | bool | false | (attrs only) Flatten a binding whose value is an attrset holding a single binding into one attrpath: a = { b = 1; }; becomes a.b = 1;. Overrides match the path of the set being flattened. |
last | list of string | [] | Names pinned to the end, in the given order. |
merge | bool | false | (attrs only) Merge bindings sharing their first attrpath component into one nested set: a.b = 1; a.c = 2; becomes a = { b = 1; c = 2; };. |
sort | bool | true | Whether to reorder this construct at all. |
Override keys ([[overrides]])
| Key | Type | Default | Description |
|---|---|---|---|
args | partial rules | — | Function-argument rules to apply to matched paths. |
attrs | partial rules | — | Attribute-set rules to apply to matched paths. |
inherits | partial rules | — | inherit rules to apply to matched paths. |
lets | partial rules | — | let-binding rules to apply to matched paths. |
lists | partial rules | — | List-element rules to apply to matched paths. |
path | string | — | Glob over dot-separated attribute paths. * matches exactly one component, ** any number (including zero). |
FormatterChoice values
| Value | Description |
|---|---|
"nixfmt" | |
"alejandra" | |
"nixpkgs-fmt" | |
"off" | Disable the base formatter; only reorder. |
BlankLinesMode values
| Value | Description |
|---|---|
"multiline" | Space bindings whose text spans several lines; keep consecutive single-line bindings together. |
"all" | Apply spacing to all bindings. |
"off" | Suppress spacing entirely. |
InheritPlacement values
| Value | Description |
|---|---|
"front" | Pin `inherit lines to the top of the set. |
"last" | Pin `inherit lines to the bottom of the set. |
"sorted" | Sort `inherit lines alphabetically among the bindings. |
Notes
The five construct tables share one shape
[args], [attrs], [lets], [inherits], and [lists] all take the same sort rules keys. Names not listed in first / last are sorted alphabetically between them. The merge, flatten, and blank-lines* keys are only meaningful under [attrs].
[args]
first = ["self", "lib", "config", "pkgs"]
last = ["<defaulted>", "..."]
[attrs]
first = ["imports", "enable", "package"]
[lets]
sort = true
[lets], [inherits], and [lists] are off by default. List sorting in particular is best enabled per-path via an override rather than globally, since list order is often significant.
merge shared attribute paths
[attrs]
merge = true
turns a.b = 1; a.c = 2; into a = { b = 1; c = 2; };. This does not fix broken evaluation by e.g. dynamically derived attribute set names.
flatten single-binding attribute sets
[attrs]
flatten = true
turns a = { b = 1; }; into a.b = 1;, provided b is the only binding in the set. This skips on rec, inherit, dynamic keys, and comments around binding.
Top-level blank lines vs. [attrs] blank-lines
The top-level-blank-lines* keys and the [attrs] blank-lines* keys look similar but do slightly different things:
top-level-blank-linesdoes not descend into function-call sets, and treats aflake.nixinputs/outputsbody as its own top-level set.
[attrs] blank-linesapplies to every attrset the[attrs]rules match, at any nesting depth (including sets inside function calls), and can be targeted per path via[[overrides]].
A reordered set by default drops all blank lines, so set one of these to restore spacing.
[inherits] vs. inherit-placement
[inherits] sorts the names inside an inherit. The top-level inherit-placement key is separate: it controls where the whole inherit statement sits relative to other bindings.
Per-path overrides
The [[overrides]] array lets you change any of the rules above for specific attribute paths — see Overrides.
Presets
A preset provides a set of base values that your explicit settings layer on top of. Select one with the top-level preset key:
preset = "nixos-module"
# anything below overrides the preset
[lets]
sort = true
Note:
When you set
[[overrides]]yourself, note that a matching override list replaces the discovered list, while preset-provided overrides still combine!
The following presets are currently in pedantix:
alphabetical
Preset that sorts every expression (except lists) alphabetically.
[lets]
sort = true
[attrs]
sort = true
[args]
sort = true
[inherits]
sort = true
nixos-module
Preset for NixOS / Home Manager modules.
[args]
first = ["config", "lib", "pkgs", "options", "modulesPath", "utils"]
last = ["<defaulted>", "..."]
[attrs]
first = ["imports", "options", "config", "enable", "package"]
last = ["meta"]
nixpkgs-package
Preset for nixpkgs-style package derivations.
[args]
first = ["lib", "stdenv", "fetchurl", "fetchFromGitHub", "fetchFromGitLab"]
last = ["<defaulted>", "..."]
[attrs]
first = [
"pname",
"version",
"src",
"outputs",
"patches",
"postPatch",
"strictDeps",
"nativeBuildInputs",
"buildInputs",
"propagatedBuildInputs",
"cargoLock",
"cargoHash",
"vendorHash",
"npmDepsHash",
"configureFlags",
"cmakeFlags",
"mesonFlags",
"makeFlags",
"buildFlags",
"env",
"preConfigure",
"postConfigure",
"preBuild",
"buildPhase",
"postBuild",
"doCheck",
"nativeCheckInputs",
"checkInputs",
"checkFlags",
"preCheck",
"checkPhase",
"postCheck",
"preInstall",
"installPhase",
"postInstall",
"doInstallCheck",
"nativeInstallCheckInputs",
"installCheckPhase",
"preFixup",
"postFixup",
]
last = ["passthru", "meta"]
[[overrides]]
path = "**.src"
attrs.first = [
"url",
"owner",
"repo",
"rev",
"tag",
"hash",
"sha256",
"fetchSubmodules",
]
[[overrides]]
path = "**.meta"
attrs.first = [
"description",
"longDescription",
"homepage",
"changelog",
"license",
"sourceProvenance",
"maintainers",
"platforms",
"badPlatforms",
"mainProgram",
]
Overrides
Sometimes you might want to supply a custom ordering for a specific attribute path. The [[overrides]] array applies targeted rules to matching paths.
Each override has a path glob and may set the partial rules (sort / first / last / [attrs]-only keys) of any construct: attrs, args, lets, inherits, lists.
Precedence
Overrides layer on top of the global construct rules (and any preset). When multiple overrides match a path, later entries in the array win for the keys they set.
Path globs
path is a glob over dot-separated attribute paths:
*matches exactly one path component;**matches any number of components, including zero.
Examples
Leave a git alias table in its original order:
[[overrides]]
path = "**.programs.git.settings.alias"
attrs.sort = false
Put description and wantedBy first inside each systemd service:
[[overrides]]
path = "**.systemd.services.*"
attrs.first = ["description", "wantedBy"]
Sort the elements of environment.systemPackages:
[[overrides]]
path = "**.environment.systemPackages"
lists.sort = true
Restore one blank line between the inputs of a flake.nix:
[[overrides]]
path = "inputs"
attrs.blank-lines = 1
Nix integration
pedantix provides a flake. To add it as an input:
{
inputs.pedantix.url = "github:swarsel/pedantix";
}
Packages
Per system, the flake exposes:
| Attribute | What it is |
|---|---|
packages.${system}.pedantix | The bare binary. You must provide the base formatter (nixfmt/alejandra/nixpkgs-fmt) on PATH yourself. |
packages.${system}.pedantix-wrapped | A wrapper that bundles nixfmt, alejandra, and nixpkgs-fmt on PATH. This is what the flake app and all the modules use by default. |
To install it system-wide:
environment.systemPackages = [ pedantix.packages.${system}.pedantix-wrapped ];
Or, to install via home-manager for a user:
home.packages = [ pedantix.packages.${system}.pedantix-wrapped ];
App
You can run pedantix directly from any system that has nix installed:
$ nix run github:swarsel/pedantix -- file.nix
Overlays
The following overlays are provided by the flake:
| Overlay | Effect |
|---|---|
overlays.default | Adds pkgs.pedantix and pkgs.pedantix-wrapped. |
overlays.emacs | Adds pedantix to every emacsPackagesFor set (works with e.g. emacs-overlay). See Emacs integration. |
Modules
pedantix ships modules for some common Nix tooling. See the following table to learn which module you have to import for what:.
| Purpose | flake-parts module | Standalone module | Page |
|---|---|---|---|
| treefmt | flakeModules.default / flakeModules.pedantix | treefmtModules.default | treefmt |
| git-hooks / pre-commit | flakeModules.git-hooks | gitHooksModules.default | Pre-commit |
| home-manager | — | homeModules.default (homeManagerModules.default) | Home Manager |
There is also lib.emacsPackage, a function epkgs -> package for installing
the Emacs package without the overlay — see Emacs.
treefmt
pedantix integrates with treefmt-nix as a formatter program.
With flake-parts
Import the flake module and enable the program under perSystem:
{
imports = [
inputs.treefmt-nix.flakeModule
inputs.pedantix.flakeModules.default
];
perSystem = _: {
treefmt.programs.pedantix = {
enable = true;
};
};
}
Without flake-parts
Use the standalone treefmt module with treefmt-nix's evalModule:
let
treefmtEval = inputs.treefmt-nix.lib.evalModule pkgs {
imports = [ inputs.pedantix.treefmtModules.default ];
projectRootFile = "flake.nix";
programs.pedantix.enable = true;
};
in
{
formatter.${system} = treefmtEval.config.build.wrapper;
checks.${system}.formatting = treefmtEval.config.build.check self;
}
Options
settings is translated into --set key=value flags, so each key is merged on top of any pedantix.toml discovered in the project. This yields one quirk: setting overrides in settings replaces a discovered overrides list, whereas preset-provided overrides still combine. If you want to keep a project's pedantix.toml authoritative, prefer configFile (or rely on discovery) over restating everything in settings.
programs.pedantix.enable
Whether to enable pedantix, the pedantic Nix formatter.
Type: boolean
Default:
false
Example:
true
programs.pedantix.package
The pedantix package to run. The default wrapper ships nixfmt, alejandra and nixpkgs-fmt on PATH; use the unwrapped pedantix package if you provide the base formatter yourself.
Type: package
Default:
pedantix.packages.${system}.pedantix-wrapped
programs.pedantix.configFile
A complete pedantix.toml to use instead of config file discovery (settings still applies on top).
Type: null or absolute path
Default:
null
programs.pedantix.excludes
Path / file patterns to exclude.
Type: list of string
Default:
[ ]
Example:
[
"generated/*.nix"
]
programs.pedantix.extraArgs
Extra command line arguments passed to pedantix, appended after the flags generated from settings and configFile.
Type: list of string
Default:
[ ]
Example:
[
"--formatter=nixpkgs-fmt"
]
programs.pedantix.includes
Path / file patterns to include.
Type: list of string
Default:
[
"*.nix"
]
programs.pedantix.priority
treefmt priority, for ordering relative to other formatters on the same files.
Type: null or signed integer
Default:
null
programs.pedantix.settings
pedantix configuration, using the same structure as pedantix.toml.
Passed as --set flags, i.e. each key is merged on top of any pedantix.toml discovered in the project (overrides replaces a discovered override list; preset overrides still combine).
Type: attribute set of anything
Default:
{ }
Example:
{
preset = "nixos-module";
formatter = "alejandra";
args.first = [ "self" "lib" "config" "pkgs" ];
lets.sort = true;
overrides = [
{
path = "**.programs.git.settings.alias";
attrs.sort = false;
}
];
}
Full example
treefmt.programs.pedantix = {
enable = true;
settings = {
preset = "nixos-module";
formatter = "alejandra";
lets.sort = true;
overrides = [
{
path = "**.programs.git.settings.alias";
attrs.sort = false;
}
];
};
};
Pre-commit / git-hooks
pedantix works with git-hooks.nix (formerly known as pre-commit-hooks.nix).
The hooks entry defaults to running pedantix --check on staged *.nix files, so it just fails the commit when something is unformatted, not editing any files.
Using treefmt hook
If you already format with treefmt, you only need to enable the treefmt hook — it runs pedantix as part of the treefmt pass:
{
imports = [
inputs.treefmt-nix.flakeModule
inputs.pedantix.flakeModules.default
inputs.git-hooks-nix.flakeModule
];
perSystem = _: {
treefmt.programs.pedantix.enable = true;
pre-commit.settings.hooks.treefmt.enable = true;
};
}
Without treefmt
Without treefmt, enable the pedantix hook through the git-hooks flake module:
{
imports = [
inputs.git-hooks-nix.flakeModule
inputs.pedantix.flakeModules.git-hooks
];
perSystem = _: {
pre-commit.settings.hooks.pedantix.enable = true;
};
}
Without flake-parts
Use the standalone module directly:
{
outputs = { self, nixpkgs, git-hooks-nix, pedantix, ... }: {
checks.x86_64-linux.pre-commit = git-hooks-nix.lib.x86_64-linux.run {
src = ./.;
imports = [ pedantix.gitHooksModules.default ];
hooks.pedantix.enable = true;
};
devShells.x86_64-linux.default =
nixpkgs.legacyPackages.x86_64-linux.mkShell {
shellHook = self.checks.x86_64-linux.pre-commit.shellHook;
};
};
}
Home Manager
The Home Manager module installs pedantix and, optionally, writes a global pedantix.toml to $XDG_CONFIG_HOME/pedantix/pedantix.toml.
Options
programs.pedantix.enable
Whether to enable pedantix, the pedantic nix formatter.
Type: boolean
Default:
false
Example:
true
programs.pedantix.package
The pedantix package to install. The default wrapper ships nixfmt, alejandra and nixpkgs-fmt on PATH; use the unwrapped pedantix package if you provide the base formatter yourself.
Type: package
Default:
pedantix.packages.${system}.pedantix-wrapped
programs.pedantix.settings
Global fallback configuration, using the same structure as pedantix.toml.
Written to $XDG_CONFIG_HOME/pedantix/pedantix.toml.
Type: TOML value
Default:
{ }
Example:
{
preset = "nixos-module";
lets.sort = true;
}
Full Example
{
imports = [ inputs.pedantix.homeModules.default ];
programs.pedantix = {
enable = true;
settings = {
preset = "nixos-module";
lets.sort = true;
};
};
}
Emacs integration
pedantix ships an Emacs package, pedantix.el, that formats Nix buffers by calling pedantix. It provides two main commands plus a format-on-save minor mode.
The package requires Emacs 27.1 or newer.
Commands and customization
Commands
| Command | Description |
|---|---|
M-x pedantix-format-buffer | Format the current buffer with pedantix. |
M-x pedantix-format-on-save-mode | Run ‘pedantix-format-buffer’ before saving the buffer. |
M-x pedantix-format-region | Format the region from BEG to END with pedantix. |
Customization
| Variable | Default | Description |
|---|---|---|
pedantix-arguments | nil | Extra command line arguments passed to pedantix. |
pedantix-program | "pedantix" | Name or path of the pedantix executable. |
pedantix-show-errors | t | Whether to pop up a buffer with stderr output when formatting fails. |
Why the region must be self-contained.
pedantixand the base formatters parse complete expressions. A bare run of bindings likeb = 1; a = 2;is not a valid expression on its own and cannot be formatted — select the enclosing{ … }instead. If formatting a region fails, the error buffer hints at exactly this.
Usage
(require 'pedantix)
(add-hook 'nix-mode-hook #'pedantix-format-on-save-mode)
(add-hook 'nix-ts-mode-hook #'pedantix-format-on-save-mode)
Or with use-package:
(use-package pedantix
:hook ((nix-mode nix-ts-mode) . pedantix-format-on-save-mode))
Installing with Nix
The cleanest way is the overlays.emacs overlay, which adds pedantix to every emacs package set (this works with e.g. emacs-overlay).
Add the overlay, then:
programs.emacs.extraPackages = epkgs: [ epkgs.pedantix ];
Without the overlay, use lib.emacsPackage:
programs.emacs.extraPackages = epkgs: [ (inputs.pedantix.lib.emacsPackage epkgs) ];
Both install pedantix.el with pedantix-program already patched to the wrapped binary's store path, so the executable and the base formatters are on hand without extra PATH setup.
Installing manually
If you install pedantix.el by any other means, make sure the pedantix binary is on PATH (or set pedantix-program to its path):