TL;DR
elixir --erl '-user user_drv' -S mix run --no-start --no-halt
Long Version
I'm going to assume that you want an Erlang shell, but you don't mind having the Elixir stuff there. Mix
really loads the modules based on the project in mix.exs
. Since Mix
is written in Elixir, you're going to have to have it in there somewhere.
Given that caveat, you can do this one of three ways:
- Locally In Shell Break Mode
- Locally From The Command Line
- As A Remote Shell
I'll provide examples of each of these below. I'm presenting them in a sort of "tutorial" fashion with some supporting context. This is actually a good way to learn some neat things about Erlang and Elixir and I'll try to slide some of them in.
Examples
In these examples, some steps are interactive. In this case, I'll use a code span
to mark the keys you'll type. Special keys will be specified inside of angle brackets (e.g. <enter>
or <insert>
). Modifiers will be specified with a plus between the key and the modifier (e.g. ctrl+alt+<delete>
).
Locally In Shell Break Mode
If you are just doing stuff manually, you can load up IEx as normal and use the "Shell Break Mode" key to break out of the IEx shell. By default, this is ctrl+G
.
This will pop up a prompt that says "User switch command". In this mode you can control the different "jobs". There is very sparse help to be had in there by just typing h<enter>
.
By default, this is just the spawned shell (which is IEx in this case). If you type j<enter>
, it will list the current "jobs". With IEx, this should display something like:
1* {'Elixir.IEx',start,[[{dot_iex_path,nil},{on_eof,halt}],{elixir,start_cli,[]}]}
To start a new Erlang shell, just type s<enter>
. This will initially be rather anticlimactic, because you won't see anything resembling the shell. That's because it is now started in the background and you haven't connected to it yet.
Let's list the jobs again by typing j<enter>
. You should see something like:
1 {'Elixir.IEx',start,[[{dot_iex_path,nil},{on_eof,halt}],{elixir,start_cli,[]}]}
2* {shell,start,[]}
From here you can see that you can actually run multiple shells at the same time! This is really pretty cool in my opinion. As an aside, this comes in handy even with just IEx because it's sometimes useful to have different shells simultaneously. And this also isn't an Elixir feature, you can do this in pure Erlang as well.
Note that *
and the number in front of these jobs. The start indicates the "active" job and is the one that will be connected to by default. The number is used to specify which job you want to "connect" to. The active job will be used if you don't specify one.
Let's connect to our Erlang shell by typing c 2<enter>
. We could also type c<enter>
since it is the active job (because of that *
). Now you have an Erlang shell!
You may have noticed that it created an Erlang shell by default, not an Elixir one. This is because this functionality is actually implemented in Erlang. To start an Elixir shell, you need to tell it what shell to start. You can do this by typing s 'Elixir.IEx'
.
As an aside, you may have noted in the help text that you can launch remote shells. You can do this from the command-line using the -remsh
option as well. We'll discuss this a bit in the next example.
You can find some documentation on this in both the Elixir docs here and in the Erlang docs here.
Locally From The Command Line
This gets a bit complicated. You can try a few things here.
Just Start The Shell Directly
In IEx, you can just try :shell.start()
. This fails because both shells (the IEx one and the Erlang one) are trying to do IO simultaneously.
Try To Start The Shell From Mix Eval
You can also try mix eval :shell.start
. That presumably starts the shell but immediately exits because it doesn't know to wait on it.
You can try to hack a wait in with something gnarly like mix eval ':shell.start() ; receive do ; :wait -> :forever ; end
. This works, but you'll immediately notice that you don't have the same command editing functionality. So the shell starts, but something about the IO is wrong.
Try to Start the "User Driver" To Get A Shell
One might ask, how does Erlang start its shell? When Erlang first starts, it runs a sort of "main" module. For reasons this is called the "user" module and the default is called user_drv
.
If you try to run it from IEx, it fails spectacularly because something else is already registered as the user
process. You can see yourself by trying :user_drv.start
.
You can run it from mix eval
. You need to use the same trick we did so it doesn't exit. So mix eval ':user_drv.start() ; receive do ; :wait -> :forever ; end'
works but you still don't get the IO right. As it turns out, mix eval
just doesn't initialize the IO right.
We can also try it under mix run
with the --no-start
and --no-halt
options. If we try `mix run --no-start --no-halt -e ':user_drv.start()', we get a shell with the editing capabilities you'd expect. However, it does print a very antisocial error message about "stealing control from the input driver".
So this works, but it's maybe not quite what we want.
Use The Elixir CLI But Pass Some Options To Erlang Under The Hood
So, if Erlang normally runs :user_drv
to make things go, what does IEx do? As it turns out, we can find out! Elixir looks for an environmental variable ELIXIR_CLI_DRY_RUN
. If this is set, it doesn't execute any underlying commands, but does print out what arguments it would use if it did.
To see what's going on, let's run IEx as follows:
ELIXIR_CLI_DRY_RUN=1 iex -S mix
This gets interesting. It outputs:
erl -pa path_to_elixir_install/bin/../lib/eex/ebin path_to_elixir_install/bin/../lib/elixir/ebin path_to_elixir_install/bin/../lib/ex_unit/ebin path_to_elixir_install/bin/../lib/iex/ebin path_to_elixir_install/bin/../lib/logger/ebin path_to_elixir_install/bin/../lib/mix/ebin -elixir ansi_enabled true -noshell -user Elixir.IEx.CLI -extra --no-halt +iex -S mix
So, there's a lot going on there, but it does shed some light on how all of this fits together. All of those paths are how Elixir itself gets loaded into Erlang. Once it loads, it then loads your Elixir code. Two other things stand out, though. Firstly, it includes -noshell
, which seems to suppress loading the shell. Secondly, it passes -user Elixir.Iex.CLI
, which seems to be the incantation to actually start IEx.
Unfortunately, with the iex
command, there doesn't seem to be an easy way to get that argument in there. There is an environmental variable you can set, but I'll skip this because there's a more direct and better way.
As it turns out, the elixir
command has an --erl
option to allow giving Erlang options under the hood. Maybe we can use one of the other options that sort of worked above to get an Erlang shell. Putting this together, we can run:
elixir --erl '-user user_drv' -S mix run --no-start --no-halt
This works! No errors. Full editing support. You can even validate that your applications are loaded (but not running) with application:loaded_applications().
and application:which_applications().
.
Th
As A Remote Shell
The other option that works for accessing a running system is to just spawn a remote shell. I didn't start there because it requires Distributed Erlang to be set up and working correctly. That's really a series of other StackOverflow questions, so I'm not going to cover it here.
You can, however, use erl -sname ... -remsh ...
to connect to another node (running Elixir) and it will start an Erlang shell. Since the code runs on the other node, you don't even need to load the other bits of Elixir locally. So this isn't necessarily the easiest or simplest way to do it, but it works well enough when you want to connect to an already-running node.