Mastering Compilation Mode
I’ve been using Emacs for over 20 years. I’ve always used M-x compile and
next-error without thinking much about them – you run a build, you jump to
errors, life is good. But recently, while working on
neocaml (a Tree-sitter-based OCaml major
mode), I had to write a custom compilation error regexp and learned that
compile.el is far more sophisticated and extensible than I ever appreciated.
This post is a deep dive into compilation mode – how it works, how to customize it, and how to build on top of it.
The Basics
If you’re not already using M-x compile, start today. It runs a shell command,
captures the output in a *compilation* buffer, and parses error messages so you
can jump directly to the offending source locations.
The essential keybindings in a compilation buffer:
| Keybinding | Command | What it does |
|---|---|---|
g |
recompile |
Re-run the last compilation command |
M-n |
compilation-next-error |
Move to the next error message |
M-p |
compilation-previous-error |
Move to the previous error message |
RET |
compile-goto-error |
Jump to the source location of the error at point |
C-c C-f |
next-error-follow-minor-mode |
Auto-display source as you move through errors |
But the real power move is using next-error and previous-error (M-g n and
M-g p) from any buffer. You don’t need to be in the compilation buffer –
Emacs tracks the last buffer that produced errors and jumps you there. This works
across compile, grep, occur, and any other mode that produces error-like output.
Pro tip: M-g M-n and M-g M-p do the same thing as M-g n / M-g p but
are easier to type since you can hold Meta throughout.
How Error Parsing Actually Works
Here’s the part that surprised me. Compilation mode doesn’t have a single regexp that it tries to match against output. Instead, it has a list of regexp entries, and it tries all of them against every line. The list lives in two variables:
compilation-error-regexp-alist– a list of symbols naming active entriescompilation-error-regexp-alist-alist– an alist mapping those symbols to their actual regexp definitions
Emacs ships with dozens of entries out of the box – for GCC, Java, Ruby, Python, Perl, Gradle, Maven, and many more. You can see all of them with:
(mapcar #'car compilation-error-regexp-alist-alist)
Each entry in the alist has this shape:
(SYMBOL REGEXP FILE LINE COLUMN TYPE HYPERLINK HIGHLIGHT...)
Where:
- REGEXP – the regular expression to match
- FILE – group number (or function) for the filename
- LINE – group number (or cons of start/end groups) for the line
- COLUMN – group number (or cons of start/end groups) for the column
- TYPE – severity: 2 = error, 1 = warning, 0 = info (can also be a cons for conditional severity)
- HYPERLINK – group number for the clickable portion
- HIGHLIGHT – additional faces to apply
The TYPE field is particularly interesting. It can be a cons cell (WARNING-GROUP
. INFO-GROUP), meaning “if group N matched, it’s a warning; if group M matched,
it’s info; otherwise it’s an error.” This is how a single regexp can handle
errors, warnings, and informational messages.
A Real-World Example: OCaml Errors
Let me show you what I built for neocaml. OCaml compiler output looks like this:
File "foo.ml", line 10, characters 5-12:
10 | let x = bad_value
^^^^^^^
Error: Unbound value bad_value
Warnings:
File "foo.ml", line 3, characters 6-7:
3 | let _ x = ()
^
Warning 27 [unused-var-strict]: unused variable x.
And ancillary locations (indented 7 spaces):
File "foo.ml", line 5, characters 0-20:
5 | let f (x : int) = x
^^^^^^^^^^^^^^^^^^^^
File "foo.ml", line 10, characters 6-7:
10 | f "hello"
^
Error: This expression has type string but ...
One regexp needs to handle all of this. Here’s the (slightly simplified) entry:
(push `(ocaml
,neocaml--compilation-error-regexp
3 ; FILE = group 3
(4 . 5) ; LINE = groups 4-5
(6 . neocaml--compilation-end-column) ; COLUMN = group 6, end via function
(8 . 9) ; TYPE = warning if group 8, info if group 9
1 ; HYPERLINK = group 1
(8 font-lock-function-name-face)) ; HIGHLIGHT group 8
compilation-error-regexp-alist-alist)
A few things worth noting:
- The COLUMN end position uses a function instead of a group number.
OCaml’s end column is exclusive, but Emacs expects inclusive, so
neocaml--compilation-end-columnsubtracts 1. - The TYPE cons
(8 . 9)means: if group 8 matched (Warning/Alert text), it’s a warning; if group 9 matched (7-space indent), it’s info; otherwise it’s an error. Three severity levels from one regexp. - The entry is registered globally in
compilation-error-regexp-alist-alistbecause*compilation*buffers aren’t in any language-specific mode. Every active entry is tried against every line.
Adding Your Own Error Regexp
You don’t need to be writing a major mode to add your own entry. Say you’re working with a custom linter that outputs:
[ERROR] src/app.js:42:10 - Unused import 'foo'
[WARN] src/app.js:15:3 - Missing return type
You can teach compilation mode about it:
(with-eval-after-load 'compile
(push '(my-linter
"^\\[\\(ERROR\\|WARN\\)\\] \\([^:]+\\):\\([0-9]+\\):\\([0-9]+\\)"
2 3 4 (1 . nil))
compilation-error-regexp-alist-alist)
(push 'my-linter compilation-error-regexp-alist))
The TYPE field (1 . nil) means: “if group 1 matches, it’s a warning” – but
wait, group 1 always matches. The trick is that compilation mode checks the
content of the match. Actually, let me correct myself. The TYPE field
should be a number or expression. A cleaner approach:
(with-eval-after-load 'compile
(push '(my-linter
"^\\[\\(?:ERROR\\|\\(WARN\\)\\)\\] \\([^:]+\\):\\([0-9]+\\):\\([0-9]+\\)"
2 3 4 (1))
compilation-error-regexp-alist-alist)
(push 'my-linter compilation-error-regexp-alist))
Here group 1 only matches for WARN lines (it’s inside a non-capturing group
with an alternative). TYPE is (1) meaning “if group 1 matched, it’s a
warning; otherwise it’s an error.”
Now M-x compile with your linter command will highlight errors and warnings
differently, and next-error will jump right to them.
Useful Variables You Might Not Know
A few compilation variables that are worth knowing:
;; OCaml (and some other languages) use 0-indexed columns
(setq-local compilation-first-column 0)
;; Scroll the compilation buffer to follow output
(setq compilation-scroll-output t)
;; ... or scroll until the first error appears
(setq compilation-scroll-output 'first-error)
;; Skip warnings and info when navigating with next-error
(setq compilation-skip-threshold 2)
;; Auto-close the compilation window on success
(setq compilation-finish-functions
(list (lambda (buf status)
(when (string-match-p "finished" status)
(run-at-time 1 nil #'delete-windows-on buf)))))
The compilation-skip-threshold is particularly useful. Set it to 2 and
next-error will only stop at actual errors, skipping warnings and info
messages. Set it to 1 to also stop at warnings but skip info. Set it to 0 to
stop at everything.
The Compilation Mode Family
Compilation mode isn’t just for compilers. Several built-in modes derive from it:
grep-mode–M-x grep,M-x rgrep,M-x lgrepall produce output in a compilation-derived buffer. Samenext-errornavigation, same keybindings.occur-mode–M-x occurisn’t technically derived from compilation mode, but it participates in the samenext-errorinfrastructure.flymake/flycheck– uses compilation-style error navigation under the hood.
The grep family deserves special mention. M-x rgrep is recursive grep with
file-type filtering, and it’s surprisingly powerful for a built-in tool. The
results buffer supports all the same navigation, and you can even edit results
and write changes back to the original files. M-x occur has had this built-in
for a long time via occur-edit-mode (just press e in the *Occur* buffer).
For grep, the wgrep package has
been the go-to solution, but starting with Emacs 31 there will be a built-in
grep-edit-mode as well. That’s a multi-file search-and-replace workflow that
rivals any modern IDE, no external tools required.
Building a Derived Mode
The real fun begins when you create your own compilation-derived mode. Let’s
build one for running RuboCop (a Ruby
linter and formatter). RuboCop’s emacs output format looks like this:
app/models/user.rb:10:5: C: Style/StringLiterals: Prefer single-quoted strings
app/models/user.rb:25:3: W: Lint/UselessAssignment: Useless assignment to variable - x
app/models/user.rb:42:1: E: Naming/MethodName: Use snake_case for method names
The format is FILE:LINE:COLUMN: SEVERITY: CopName: Message where severity is
C (convention), W (warning), E (error), or F (fatal).
Here’s a complete derived mode:
(require 'compile)
(defvar rubocop-error-regexp-alist
`((rubocop-offense
;; file:line:col: S: Cop/Name: message
"^\\([^:]+\\):\\([0-9]+\\):\\([0-9]+\\): \\(\\([EWFC]\\)\\): "
1 2 3 (5 . nil)
nil (4 compilation-warning-face)))
"Error regexp alist for RuboCop output.
Group 5 captures the severity letter: E/F = error, W/C = warning.")
(define-compilation-mode rubocop-mode "RuboCop"
"Major mode for RuboCop output."
(setq-local compilation-error-regexp-alist
(mapcar #'car rubocop-error-regexp-alist))
(setq-local compilation-error-regexp-alist-alist
rubocop-error-regexp-alist))
(defun rubocop-run (&optional directory)
"Run RuboCop on DIRECTORY (defaults to project root)."
(interactive)
(let ((default-directory (or directory (project-root (project-current t)))))
(compilation-start "rubocop --format emacs" #'rubocop-mode)))
A few things to note:
define-compilation-modecreates a major mode derived fromcompilation-mode. It inherits all the navigation, font-locking, andnext-errorintegration for free.- We set
compilation-error-regexp-alistandcompilation-error-regexp-alist-alistas buffer-local. This means our mode only uses its own regexps, not the global ones. No interference with other tools. compilation-startis the workhorse – it runs the command and displays output in a buffer using our mode.- The TYPE field
(5 . nil)means: if group 5 matched, check its content – but actually, here all lines match group 5. The subtlety is that compilation mode treats a non-nil TYPE group as a warning. To distinguish E/F from W/C, you’d need a predicate or two separate regexp entries. For simplicity, this version treats everything as an error, which is usually fine for a linter.
You could extend this with auto-fix support (rubocop -A), or a sentinel
function that sends a notification when the run finishes:
(defun rubocop-run (&optional directory)
"Run RuboCop on DIRECTORY (defaults to project root)."
(interactive)
(let ((default-directory (or directory (project-root (project-current t))))
(compilation-finish-functions
(cons (lambda (_buf status)
(message "RuboCop %s" (string-trim status)))
compilation-finish-functions)))
(compilation-start "rubocop --format emacs" #'rubocop-mode)))
Side note: RuboCop actually ships with a built-in emacs output formatter
(that’s what --format emacs uses above), so its output already matches
Emacs’s default compilation regexps out of the box – no custom mode needed. I
used it here purely to illustrate how define-compilation-mode works. In
practice you’d just M-x compile RET rubocop --format emacs and everything
would Just Work.1
If you want a real, battle-tested rubocop-mode rather than rolling your own,
check out rubocop-emacs. It
provides commands for running RuboCop on the current file, project, or
directory, with proper compilation mode integration. Beyond compilation mode,
RuboCop is also supported out of the box by both
Flymake (via
ruby-flymake-rubocop in Emacs 29+) and
Flycheck (via the ruby-rubocop checker), giving
you real-time feedback as you edit without needing to run a manual compilation
at all.
In practice, most popular development tools already have excellent Emacs integration, so you’re unlikely to need to write your own compilation-derived mode any time soon. The last ones I incorporated into my workflow were ag.el and deadgrep.el – both compilation-derived modes for search tools – and even those have been around for years. Still, understanding how compilation mode works under the hood is valuable for the occasional edge case and for appreciating just how much the ecosystem gives you for free.
next-error is not really an error
There is no spoon.
– The Matrix
The most powerful insight about compilation mode is that it’s not really about
compilation. It’s about structured output with source locations. Any tool
that produces file/line references can plug into this infrastructure, and once it
does, you get next-error navigation for free. The name compilation-mode is a
bit of a misnomer – something like structured-output-mode would be more
accurate. But then again, naming is hard, and this one has 30+ years of momentum
behind it.
This is one of Emacs’s great architectural wins. Whether you’re navigating
compiler errors, grep results, test failures, or linter output, the workflow is
the same: M-g n to jump to the next problem. Once your fingers learn that
pattern, it works everywhere.
I used M-x compile for two decades before I really understood the machinery
underneath. Sometimes the tools you use every day are the ones most worth
revisiting.
That’s all I have for you today. In Emacs we trust!
-
Full disclosure: I may know a thing or two about RuboCop’s Emacs formatter. ↩