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:

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:

  1. Perform an optional base formatting
  2. Perform an optional merging/flattening of attribute paths
  3. Sort expressions according to your config
  4. Perform an optional base formatting
  5. Optionally enforce spacing between attribute paths/sets

Safety

  • pedantix preserves 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

ArgumentDescription
[FILES]...Files to format in place. With no files (or -), reads stdin and writes the result to stdout

Options

OptionDescription
-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-commandExecute 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
--checkDon'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

CodeMeaning
0Success — files were formatted, or nothing needed reformatting when using --check.
1With --check: at least one file would be reformatted.
2Bad 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

KeyTypeDefaultDescription
format-after-sortbooltrueRun the base formatter again after sorting.
format-before-sortbooltrueRun the base formatter before sorting.
formatterFormatterChoice"nixfmt"Base formatter run before and after sorting.
formatter-commandlist of stringArbitrary 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-placementInheritPlacement"front"Where inherit statements land relative to ordinary bindings.
top-level-blank-linesintExact 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-depthint1How 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-modeBlankLinesMode"multiline"Which top-level bindings receive the blank-line spacing.

Construct rules ([args], [attrs], [lets], [inherits], [lists])

The merge, flatten, and blank-lines* keys only apply to [attrs].

KeyTypeDefaultDescription
blank-linesint(attrs only) Number of blank lines between the set's bindings. Unset keeps the existing spacing.
blank-lines-depthint1(attrs only) How deep the spacing reaches; wrappers such as functions and lets do not count as levels.
blank-lines-modeBlankLinesMode(attrs only) Which bindings receive the blank-line spacing.
firstlist 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.
flattenboolfalse(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.
lastlist of string[]Names pinned to the end, in the given order.
mergeboolfalse(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; };.
sortbooltrueWhether to reorder this construct at all.

Override keys ([[overrides]])

KeyTypeDefaultDescription
argspartial rulesFunction-argument rules to apply to matched paths.
attrspartial rulesAttribute-set rules to apply to matched paths.
inheritspartial rulesinherit rules to apply to matched paths.
letspartial ruleslet-binding rules to apply to matched paths.
listspartial rulesList-element rules to apply to matched paths.
pathstringGlob over dot-separated attribute paths. * matches exactly one component, ** any number (including zero).

FormatterChoice values

ValueDescription
"nixfmt"
"alejandra"
"nixpkgs-fmt"
"off"Disable the base formatter; only reorder.

BlankLinesMode values

ValueDescription
"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

ValueDescription
"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-lines does not descend into function-call sets, and treats a flake.nix inputs/outputs body as its own top-level set.

[attrs] blank-lines applies 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:

AttributeWhat it is
packages.${system}.pedantixThe bare binary. You must provide the base formatter (nixfmt/alejandra/nixpkgs-fmt) on PATH yourself.
packages.${system}.pedantix-wrappedA 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:

OverlayEffect
overlays.defaultAdds pkgs.pedantix and pkgs.pedantix-wrapped.
overlays.emacsAdds 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:.

Purposeflake-parts moduleStandalone modulePage
treefmtflakeModules.default / flakeModules.pedantixtreefmtModules.defaulttreefmt
git-hooks / pre-commitflakeModules.git-hooksgitHooksModules.defaultPre-commit
home-managerhomeModules.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

CommandDescription
M-x pedantix-format-bufferFormat the current buffer with pedantix.
M-x pedantix-format-on-save-modeRun ‘pedantix-format-buffer’ before saving the buffer.
M-x pedantix-format-regionFormat the region from BEG to END with pedantix.

Customization

VariableDefaultDescription
pedantix-argumentsnilExtra command line arguments passed to pedantix.
pedantix-program"pedantix"Name or path of the pedantix executable.
pedantix-show-errorstWhether to pop up a buffer with stderr output when formatting fails.

Why the region must be self-contained. pedantix and the base formatters parse complete expressions. A bare run of bindings like b = 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):