Writing command line apps with Elixir

Posted by Ilija Eftimov on November 27, 2015

Elixir is a really cool language. Although I do not have much experience with it (yet), I am always trying to build interesting stuff with it and learn the built-in tools. In this blog post I decided to show you how to build a self-contained command line application with Elixir, with some help from escript.

Escript

Erlang and Elixir have this cool thing called escript. It’s basically a tool that compiles an Elixir app that you have as a command line application.

From Elixir’s documentation on escript:

An escript is an executable that can be invoked from the command line. An escript can run on any machine that has Erlang installed and by default does not require Elixir to be installed, as Elixir is embedded as part of the escript.

This task guarantees the project and its dependencies are compiled and packages them inside an escript.

What is really interesting is that escript will build your Elixir CLI application and create an executable. The executable will only require Erlang to be able to run on any machine. It will not need Elixir because it will be contained in the executable it builds.

eight_ball wrapped in escript

Now, instead of going at length of building a tiny example Elixir application, I will use the eight_ball application that we built in the Write and publish your first Elixir library post. If you haven’t read it yet you can click on the link above, read the step-by-step tutorial and come back to this post once you are done.

The application is quite simple. It’s a module which has only one function EightBall.ask/1. It takes a question as an argument and returns the answer. Let’s use escript to build a command line application from this Elixir application.

Adding escript to the application

If you want to follow along, you can find the repo here.

In the mix.exs file, we need to make an addition:

defmodule EightBall.Mixfile do
  use Mix.Project

  def project do
    [app: :eight_ball,
     version: "0.0.1",
     elixir: "~> 1.0",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     escript: [main_module: EightBall.CLI], # <- this line
     deps: deps,
     package: package ]
  end
  # ...
end

The line we added will tell escript that the main module where the main/1 function will be placed is EightBall.CLI. If you are wondering what main/1 function, think of it in this way: the main/1 function is the function that will be an entry point for the command line application.

EightBall.CLI

Since the EightBall.CLI module doesn’t exist, let’s create it:

defmodule EightBall.CLI do
  def main(args) do
  end
end

As you can see, the module contains the main/1 function wich will take the command line arguments map as an argument. Now, to make any sense of the arguments, we need to use the OptionParser module which comes with Elixir. The OptionParser (docs) module contains functions which can parse the command line arguments.

If I were to wish the syntax of the arguments of the command line application, I’d like to see something like:

eight_ball --question "Is Elixir great?"

or

eight_ball -q "Is Elixir great?"

So, let’s use OptionParser and make -q/--question an argument:

defmodule EightBall.CLI do
  def main(argv) do
    {options, _, _} = OptionParser.parse(argv, 
      switches: [question: :string],
    )

    IO.inspect options
  end
end

The main/1 function will parse arguments and build meaningful tagged lists that we can use. At this time, the main/1 function will only show the options that have been parsed. We will add some meaningful logic later.

First, let’s build the executable with escript. To build the executable, in the project root we need to run:

mix escript.build

This will compile the Elixir application and build an escript executable.

➜  eight_ball git:(master) ✗ mix escript.build
Compiled lib/eight_ball/cli.ex
Generated eight_ball app
Generated escript eight_ball with MIX_ENV=dev

If you check the contents of the root path of the eight_ball project, you will see an eight_ball executable. To use it, type:

./eight_ball --question "Is Elixir great?"

And you will get the following output:

➜  eight_ball git:(master) ✗ ./eight_ball -q "Is Elixir great?"
[question: "Is Elixir great?"]

Voila! We can see the parsed command line arguments.

Wiring it up

Now, let’s use the EightBall.ask/1 function in the CLI app. In the EightBall::CLI.main/1 function, add the following code:

defmodule EightBall.CLI do
  def main(opts) do
    {options, _, _} = OptionParser.parse(opts, 
      switches: [question: :string],
      aliases: [q: :question] # makes '-q' an alias of '--question'
    )

    try do
      IO.puts EightBall.ask(options[:question]) 
    rescue 
      e in RuntimeError -> e
        IO.puts e.message
    end
  end
end

In the try/rescue block we send the question string to the ask/1 function and rescue from a RuntimeError. This error can be triggered by EightBall::QuestionValidator which validates the input string. If the input is not a question, it will throw an error:

Question must be a string, ending with a question mark.

If our command line application rescues this error, it will output the error message.

Building the CLI application

Now, the last step of building the command line application is invoking escript. With Elixir, as we saw earlier, it’s super easy to invoke esciprt via mix:

mix escript.build

If you are following along, the output of the command should be similar to this:

➜  eight_ball git:(master) ✗ mix escript.build
Compiled lib/eight_ball.ex
Generated eight_ball app
Generated escript eight_ball with MIX_ENV=dev

This will create a command line application, with the same filename as the main module. In our case, this will be just eight_ball. Now, if one would open the executable, there will be a ton of non-readable code. This is due to the fact that the code you will see is Erlang VM bytecode.

While the code is unreadable, the awesome thing is that you can send this command line application to anyone that has just Erlang installed on her/his machine. Elixir itself is embedded in the file, so the only dependency is Erlang. Isn’t that cool?

Now, if we run the app:

➜  eight_ball git:(master) ✗ ./eight_ball --question "Is Elixir awesome?"
Outlook good

Or

➜  eight_ball git:(master) ✗ ./eight_ball --question "Is Elixir awesome"
Question must be a string, ending with a question mark.

Outro

Thanks for following along this post. I hope you learn something and you consider it a time well spent. Have you built anything interesting with Elixir? Share it with me in the comments, I would love to check it out!


comments powered by Disqus