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?