Your own Elixir shell
2017-06-24
A question I see often in the Elixir community is "how do I do the equivalent of gem install
so it's available globally?"
Folks typically do this because they want to be able to boot up their REPL (previously irb
, now iex
) and pull in an HTTP client, JSON parser, etc., and fire off some quick commands without having to got through the hassle of making a new project every time.
The usual answer I've seen has been to create a dummy project that you then load with your chosen dependencies and iex
preloads and call it a day. This sounds tedious, but it's not all bad. It localizes your "global" dependencies to a specific place, and, even better, it's just a normal project that you can check into git
and push somewhere. git clone
and you have it on your new machine.
Setting up the dummy project
Create your project using Mix, like this:
$ cd ~/code/dotfiles
$ mix new exshell --sup
I use --sup
to make this project a supervision tree, because my past Clojure experience leads me to leave REPLs open for long periods of time. And when you leave things running for a while, you usually need to restart them or reload them after putting your machine to sleep, having a job time out, etc. This is much easier to do it Elixir/Erlang than Clojure, so why not?
I added these dependencies to my project's mix.exs
:
defp deps do
[{:httpoison, "~> 0.11.1"},
{:poison, "~> 3.1"},
{:strand, "~> 0.5"},
{:array_vector, "~> 0.2"},
{:prolly, "~> 0.2"}]
end
This is basically all you have to do.
After running a quick mix deps.get
, you can now just cd ~/code/dotfiles/exshell
and iex -S mix
, and you're cooking. Granted, you can't open iex
from anywhere and magically have your dependencies available.
But what if you could?
As with most things, our global Elixir REPL project can be improved with a sprinkle of UNIX.
I added this function to my .zshrc
:
function siex() {
pushd $HOME/code/dotfiles/exshell \
&& iex "$@" -S mix \
&& popd
}
pushd
with a directory as an argument changes to that directory, but not before first putting your current working directory on the directory stack.
popd
changes to the top item on the directory stack.
So, when I start off in ~
, and run siex
, and Ctrl-C
the REPL, this is what happens:
[~]
clark$> siex
~/code/dotfiles/exshell ~ ~/code/dotfiles
Erlang/OTP 20 [erts-9.0] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]
Interactive Elixir (1.4.5) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> pwd()
/Users/clark/code/dotfiles/exshell
iex(2)>
BREAK: (a)bort (c)ontinue (p)roc info (i)nfo (l)oaded
(v)ersion (k)ill (D)b-tables (d)istribution
\^C~ ~/code/dotfiles
[~]
clark$>
We went from ~
to /Users/clark/code/dotfiles/exshell
in order to run the project and back to ~
without having to manage that ourselves. Nice!
Now we're free to just start using those "global" dependencies we pulled in earlier, Ruby-style:
$ siex
iex(1)> HTTPoison.get("yahoo.com")
{:ok,
%HTTPoison.Response{body: "<HTML>\n<HEAD>\n<TITLE>Document Has Moved</TITLE>\n</HEAD>\n\n<BODY BGCOLOR=\"white\" FGCOLOR=\"black\">\n<H1>Document Has Moved</H1>\n<HR>\n\n<FONT FACE=\"Helvetica,Arial\"><B>\nDescription: The document you requested has moved to a new location. The new location is \"https://www.yahoo.com/\".\n</B></FONT>\n<HR>\n</BODY>\n",
headers: [{"Date", "Sat, 24 Jun 2017 05:38:38 GMT"},
{"Via", "https/1.1 ir15.fp.gq1.yahoo.com (ApacheTrafficServer)"},
{"Server", "ATS"}, {"Location", "https://www.yahoo.com/"},
{"Content-Type", "text/html"}, {"Content-Language", "en"},
{"Cache-Control", "no-store, no-cache"}, {"Connection", "keep-alive"},
{"Content-Length", "304"}], status_code: 301}}
iex(2)> Poison.encode!(["isn't", "this", "great?"])
"[\"isn't\",\"this\",\"great?\"]"
Also, notice that the actual iex
invocation looks like iex "$@" -S mix
. The "$@"
lets you pass your own arguments in to the VM, so you can even run a few shells locally and communicate between them, like this:
# in shell 1
$ siex --sname joe
iex(joe@clark)1>
# in shell 2
$ siex --sname mike
iex(mike@clark)1> Node.connect(:"joe@clark")
true
iex(mike@clark)2> Node.spawn_link(:"joe@clark", fn -> IO.puts("Hello, Joe"); IO.puts(Node.self) end)
Hello, Joe
#PID<15384.205.0>
joe@clark
# back in shell 1
iex(joe@clark)1> Node.spawn_link(:"mike@clark", fn -> IO.puts("Hello, Mike"); IO.puts(Node.self) end)
Hello, Mike
#PID<15429.205.0>
mike@clark
Fun, huh?