Changing the Erlang shell prompt

4 Mar 2023 16:38 erlang

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.

Alternatively, we could allow loading arbitrary modules from $HOME/ebin. In that case, the first line would look like this instead:

code:add_patha(filename:join([os:getenv("HOME"), "ebin"])).
The .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.

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), where L is a list of key-value pairs created by the shell. Currently there is only one pair: {history, N}, where N 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 shell 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> █

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.

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 then filename: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.