efmt
An Erlang code formatter.
Features
- An opinionated formatter
- No configuration options
- If items (e.g.,
caseblocks, lists, records) contain newlines in the original code, those are processed in multi-line mode
- Emacs Erlang Mode friendly indentation with some exceptions
- Preserves non-whitespace tokens of the original text as-is
- Ensures the code after formatting keeps the same semantic meaning
- Thorough macro support (MACRO_AND_DIRECTIVE.md)
An Formatting Example
Before
After
Installation
Pre-built binaries
Pre-built binaries for Linux and macOS are available from the releases page:
x86_64-unknown-linux-musl(Linux x86_64, fully static)aarch64-unknown-linux-musl(Linux arm64, fully static)aarch64-apple-darwin(macOS Apple Silicon)
For example, download the Linux x86_64 binary from the latest release with the GitHub CLI:
$ gh release download --repo sile/efmt \
--pattern "efmt-*.x86_64-unknown-linux-musl"
$ chmod +x efmt-*.x86_64-unknown-linux-musl
$ ./efmt-*.x86_64-unknown-linux-musl --version
The asset name is efmt-<version>.<target>, where <target> is one of the
triples above and <version> is the release version without the leading v.
To download a specific version, pass its tag name (for example v0.21.0)
instead of the latest release:
$ gh release download v0.21.0 --repo sile/efmt \
--pattern "efmt-0.21.0.x86_64-unknown-linux-musl"
With Cargo
If you have installed cargo (the package manager for Rust), you can install efmt with the following command:
$ cargo install efmt
$ efmt
Usage
Formats an Erlang file (assuming example.erl in the above example is located in the current directory):
$ efmt example.erl
// You can specify multiple files.
$ efmt example.erl rebar.config ...
Checks diff between the original text and the formatted one:
$ efmt -c example.erl
-module(example).
--export(
- [fac/1]
-).
+-export([fac/1]).
-fac(1)->
-1;fac(N )
--> N*fac(
-N-1).
+
+fac(1) ->
+ 1;
+fac(N) ->
+ N * fac(N - 1).
// If you omit the filename, all the Erlang-like files (i.e., `*.{erl, hrl, app.src}` and `rebar.config`)
// are included in the target (if you're in a git repository the files specified by `.gitignore` are excluded).
$ efmt -c
Overwrites the original file with the formatted one:
$ efmt -w example.erl
// As with `-c` option, you can omit the filename arg.
$ emf -w
For the other command-line options, please see the help document:
// Short doc.
$ efmt -h
// Long doc.
$ efmt --help
How to keep some areas from being formatted
If you want to keep the style of some areas in your input text,
please use @efmt:off and @efmt:on comments as follows:
..,
998,999,1000],
%% @efmt:on
Editor Integrations
- Emacs: emacs-format-all-the-code
- Sublime Text: Formatter
Differences with other Erlang formatters
Since I'm not familiar with other Erlang formatters, and the README.md of erlfmt already provides a good comparison table among various formatters, I only describe the differences between efmt and erlfmt here.
Note that in the following examples, I used efmt-v0.11.0 and erlfmt-v1.0.0.
Formatting style
I think the formatting style of efmt is much different from erlfmt.
IMO, this is a major point when you decide which one you should choose.
If you like the erlfmt style. It's okay. I recommend using erlfmt.
But, if you like the efmt style. It's welcomed. Please use efmt.
It's hard work to pick up all difference points here. So I just give you some formatted code examples and hope they give you a sense.
Original code
Let's see how erlfmt and efmt format the above code.
erlfmt formatted code
$ erlfmt foo.erl
efmt formatted code
$ efmt foo.erl
No line width limit
Unlike erlfmt, efmt doesn't provide a feature to ensure each line of the formatted code is within a specified line width (columns).
Error handling
erlfmt seems to try formatting the remaining part of code even if it detected a syntax error.
In contrast, efmt aborts once it detects an error.
For instance, let's format the following code.
->
ok.
Using erlfmt:
$ erlfmt bar.erl
-module(bar).
invalid_fun() ->
: foo,
ok.
valid_fun() ->
ok.
bar.erl:4:5: syntax error before: ':'
// `valid_fun/0` was formatted and the program exited with 0 (success)
Using efmt:
$ efmt bar.erl
[2021-11-28T11:30:06Z ERROR efmt] Failed to format "bar.erl"
Parse failed:
--> bar.erl:4:5
4 | : foo,
| ^ unexpected token
Error: Failed to format the following files:
- bar.erl
// The program exited with 1 (error)
Macro handling
efmt, as much as possible, processes macros as the Erlang preprocessor does.
Thus, it can cover a wide range of tricky cases. Let's format the following code which is based on a macro usage in sile/jsone/src/jsone.erl:
%% The 'OTP_RELEASE' macro introduced at OTP-21,
%% so we can use it for detecting whether the Erlang compiler supports new try/catch syntax or not.
Using efmt:
$ efmt baz.erl
-module(baz).
-ifdef('OTP_RELEASE').
%% The 'OTP_RELEASE' macro introduced at OTP-21,
%% so we can use it for detecting whether the Erlang compiler supports new try/catch syntax or not.
-define(CAPTURE_STACKTRACE, :__StackTrace).
-define(GET_STACKTRACE, __StackTrace).
-else.
-define(CAPTURE_STACKTRACE, ).
-define(GET_STACKTRACE, erlang:get_stacktrace()).
-endif.
decode(Json, Options) ->
try
{ok, Value, Remainings} = try_decode(Json, Options),
check_decode_remainings(Remainings),
Value
catch
error:{badmatch, {error, {Reason, [StackItem]}}} ?CAPTURE_STACKTRACE->
erlang:raise(error, Reason, [StackItem])
end.
Using erlfmt:
$ erlfmt baz.erl
baz.erl:6:29: syntax error before: ':'
-module(baz).
-ifdef('OTP_RELEASE').
%% The 'OTP_RELEASE' macro introduced at OTP-21,
%% so we can use it for detecting whether the Erlang compiler supports new try/catch syntax or not.
-define(CAPTURE_STACKTRACE, :__StackTrace).
-define(GET_STACKTRACE, __StackTrace).
-else.
-define(CAPTURE_STACKTRACE,).
-define(GET_STACKTRACE, erlang:get_stacktrace()).
-endif.
decode(Json, Options) ->
try
{ok, Value, Remainings} = try_decode(Json, Options),
check_decode_remainings(Remainings),
Value
catch
error:{badmatch, {error, {Reason, [StackItem]}}} ?CAPTURE_STACKTRACE ->
erlang:raise(error, Reason, [StackItem])
end.
baz.erl:19:50: syntax error before: '?'
Formatting speed
The following benchmark compares the time to format all "*.erl" files contained in the OTP-24 source distribution.
// OS and CPU spec.
$ uname -a
Linux TABLET-GC0A6KVD 5.10.16.3-microsoft-standard-WSL2 #1 SMP Fri Apr 2 22:23:49 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux
$ cat /proc/cpuinfo | grep 'model name' | head -1
model name : 11th Gen Intel(R) Core(TM) i7-1185G7 @ 3.00GHz
// Downloads OTP source code. There are 3,737 "*.erl" files.
$ wget https://erlang.org/download/otp_src_24.1.tar.gz
$ tar zxvf otp_src_24.1.tar.gz
$ cd otp_src_24.1/
$ find . -name '*.erl' | wc -l
3737
// Erlang version: Erlang/OTP 24 [erts-12.1] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit]
// erlfmt: 17.30s
$ time erlfmt (find . -name '*.erl') > /dev/null 2> /dev/null
________________________________________________________
Executed in 17.30 secs
usr time 97.73 secs
sys time 10.20 secs
// efmt: 5.84s
$ time efmt --parallel $(find . -name '*.erl') > /dev/null 2> /dev/null
________________________________________________________
Executed in 5.84 secs
usr time 43.88 secs
sys time 1.28 secs
Development phase
erlfmt has released the stable version (v1), but efmt hasn't.
Perhaps some parts of the efmt style will change in future releases until it releases v1.
Limitations
There are some limitations that are not planned to be addressed in the future:
- Only supports UTF-8 files
- Doesn't process parse transforms
- That is, if a parse transform has introduced custom syntaxes in your Erlang code,
efmtcould fail
- That is, if a parse transform has introduced custom syntaxes in your Erlang code,
- Doesn't process
-include().and-include_lib().directives- Macros defined in those include files are expanded to (dummy) atoms.