Changing the Erlang shell prompt
Did you know that you can change the Erlang shell prompt?
I previously wrote about colouring the ‘iex’ prompt. Can we do the same for the Erlang shell prompt?
We need a module containing our new prompt function; we’ll start with something basic, as follows:
-module(shell_prompt).
-export([prompt_func/1]).
prompt_func(_Opts) ->
"erl> ".
Compile it, and put the result in $HOME/ebin
:
mkdir -p "$HOME/ebin"
erlc -o "$HOME/ebin" shell_prompt.erl
We need a $HOME/.erlang
file:
code:load_abs(filename:join([os:getenv("HOME"), "ebin", "shell_prompt"])).
shell:prompt_func({shell_prompt, prompt_func}).
The first line loads $HOME/ebin/shell_prompt.beam
; the second line sets a function from that module as the prompt
function.
.erlang
file is loaded on every Erlang instantiation, including things like running Elixir’s mix
and so on.
Bear that in mind before you do anything too involved.
Embedded mode
Why don’t we allow loading arbitrary modules from $HOME/ebin
? In that case, the first line would look like this
instead:
% Doesn't work in "embedded" mode.
code:add_patha(filename:join([os:getenv("HOME"), "ebin"])).
Unfortunately, if you run an Erlang release (in “embedded” mode) – _build/prod/rel/foo/bin/foo console
, for example
– it doesn’t allow implicitly loading arbitrary modules, and you’ll get ** exception error: undefined function
shell_prompt:prompt_func/1
every time it attempts to display a prompt.
This BEAM file was compiled for a later version
If you compile shell_prompt.erl
with, say, Erlang 26.x, and then attempt to load it into an Erlang 23.x session,
you’ll get the following error message:
=ERROR REPORT==== 21-Sep-2023::14:30:11.433270 ===
Loading of .../ebin/shell_prompt.beam failed: badfile
=ERROR REPORT==== 21-Sep-2023::14:30:11.706057 ===
beam/beam_load.c(1886): Error loading module shell_prompt:
This BEAM file was compiled for a later version of the run-time system than 23.
To fix this, please recompile this module with an 23 compiler.
(Use of opcode 172; this emulator supports only up to 170.)
** exception error: undefined function shell_prompt:prompt_func/1
* Bad prompt function: {shell_prompt,prompt_func}
You can fix the first problem by compiling the file with the oldest version of Erlang you’re likely to use. It will usually load into newer versions of Erlang without problem. I’ve tested with OTP-23.x and OTP-26.x.
If you’re using kerl
, that’s something like this (depending on where you keep your Erlang installations):
~/.kerl/erlangs/OTP-23.3.4.19/bin/erlc -o "$HOME/ebin" shell_prompt.erl
You can partially fix the first problem by checking the result of code:load_abs
, but you still get the error report
the first time you attempt to load the file.
You can fix the second problem by checking that the function is exported. The following snippet does both:
% Load the module explicity, otherwise it doesn't work in 'embedded' mode.
case code:load_abs(filename:join([os:getenv("HOME"), "ebin", "shell_prompt"])) of
{module, _} ->
case erlang:function_exported(shell_prompt, prompt_func, 1) of
true ->
shell:prompt_func({shell_prompt, prompt_func});
_ ->
skip
end;
_ ->
skip
end.
Shell Prompt
That results in a shell prompt that looks like this:
Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Eshell V13.0.4 (abort with ^G)
erl> █
We can make our prompt look identical to the default Erlang prompt with the following:
prompt_func(Opts) ->
prompt_func(erlang:is_alive(), lists:keyfind(history, 1, Opts)).
prompt_func(false, {history, N}) ->
io_lib:format("~B> ", [N]);
prompt_func(true, {history, N}) ->
io_lib:format("(~s)~B> ", [node(), N]).
From the documentation:
The function is called as
Mod:Func(L)
, whereL
is a list of key-value pairs created by the shell. Currently there is only one pair:{history, N}
, whereN
is the current command number.
I decided to allow for the case where future Erlang adds more things to the list.
Aside: the default prompt implementation uses ~w
(format as an Erlang term) to display the history number. I’m not
sure why. I suspect it’s just in case it’s not a number.
I like to also display the current process ID in the prompt:
prompt_func(Opts) ->
prompt_func(erlang:is_alive(), lists:keyfind(history, 1, Opts)).
prompt_func(false, {history, N}) ->
io_lib:format("~w ~B> ", [self(), N]);
prompt_func(true, {history, N}) ->
io_lib:format("~w (~s)~B> ", [self(), node(), N]).
That looks like this:
$ erl -sname foo
Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Eshell V13.0.4 (abort with ^G)
<0.86.0> (foo@ROGER-SURFACEBOOK3)1> █
Colour
Let’s add some colour:
-define(Reset, "\e[0m").
-define(IRed, "\e[0;91m").
-define(IGreen, "\e[0;92m").
-define(IYellow, "\e[0;93m").
-define(IWhite, "\e[0;97m").
prompt_func(Opts) ->
prompt_func(erlang:is_alive(), get_colour(), lists:keyfind(history, 1, Opts)).
prompt_func(false, _Colour, {history, N}) ->
io_lib:format("~w ~B> ", [self(), N]);
prompt_func(true, Colour, {history, N}) ->
io_lib:format("~w (" ++ Colour ++ "~s" ?Reset ")~B> ", [self(), node(), N]).
get_colour() ->
get_colour(os:getenv("WHICH_ENVIRONMENT")).
get_colour(false) -> ?IWhite;
get_colour("dev") -> ?IGreen;
get_colour("test") -> ?IYellow;
get_colour(_) -> ?IRed.
That comes out looking like this:
$ WHICH_ENVIRONMENT=dev erl -sname foo
Erlang/OTP 25 [erts-13.0.4] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]
Eshell V13.0.4 (abort with ^G)
<0.86.0> (foo@ROGER-SURFACEBOOK3)1> █
I had problems in the distant past (Erlang R16 or so) which made me go back to a monochrome prompt. Maybe I’ll have better luck this time.
File location
Unlike Elixir and its .iex.exs
file, Erlang doesn’t look in the current directory for the .erlang
file; per the
documentation:
When Erlang/OTP is started, the system searches for a file named
.erlang
in the user’s home directory and thenfilename:basedir(user_config, "erlang")
.
On Linux, that usually resolves to $HOME/.config/erlang
.
It would occasionally be nice if it also looked in the current directory; I’ll try to write another blog post about that at some point.