Executable Elixir: Working with Escripts

In my previous post, we built an Elixir OTP application that implemented a supervisor tree to clone down repositories and check them for code quality with the help of Credo.

Now, we'll build a command line interface for our application and make it executable with the help of Elixir's escripts.

Our aim is to be able to run our application from the command line like this:

$ ./elixir_linter --lint SophieDeBenedetto/my_great_elixir_repo

What Is Escript?

The Mix docs lay it out pretty clearly:

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.

Main Module

Configuring our project for escript is fairly simple. First off, we need to tell our Mixfile what module escript should run when it executes. We do this by defining a main_module option under a key of escript in our Mixfile.

defmodule ElixirLinter.Mixfile do
  use Mix.Project

  def project do
    [app: :elixir_linter,
     version: "0.1.0",
     elixir: "~> 1.3",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     deps: deps(),
     escript: escript]
  end

  ...
  def escript do
    [main_module: ElixirLinter.Cli]
  end
end

Our main module, the module we want to start up via escript, is our Cli module, which we'll define to capture input from the command line and start up our supervisor tree with that input.

The CLI Module

Our main module, ElixirLinter.Cli, has a few rules in order for it to be escript compatible.

It must implement a funtion, main/1, which should expect to receive and argument of argv––the input from the command line. This is the function escript will execute when it runs your program.

defmodule ElixirLinter.Cli do
  def main(argv) do
    argv
    |> parse_args
    |> run
  end

  defp parse_args(args) do
    parsed_args = OptionParser.parse(args, switches: [help: :boolean],
                                     aliases: [h: :help])
    case parsed_args do
      {[help: true], _, _} -> :help
      {[lint: repo_name], _, _} -> {:start, repo_name}
      _ -> :help
    end
  end

  defp run(:help) do
    Bunt.puts [:steelblue, """
      Run the Elixir Linter engine from the command line by typing elixir_linter --lint 
      where the repo name is formatted like this: 'owner/repo_name`.
    """]
  end

  defp run({:start, repo_name}) do
    ElixirLinter.start("whatever", repo_name)
  end
  ...
end

Our main function receives input from the command line and pipes that input into a helper function, parse_args. Then, we execute a final function, run.

The parse_args helper function uses Elixir's OptionParser module.

The OptionParser.parse function returns
a three-element tuple:

{parsed, args, invalid}

Let's break this down:

  • parsed is a keyword list of parsed switches with {switch_name, value} tuples in it.
  • args is a list of the remaining arguments in argv as strings
  • invalid is a list of invalid options as tuples.

A "switch" and its corresponding value constitute this portion of the command line input

--im-a-switch! im-the-value

So our switch-value pair would be this part of the command line input:

--lint repo-owner/repo-name

which would be parsed into a tuple:

[lint: repo-name]

Lastly, let's take a look at our run function.

The run function is another requirement for our escript to work. Our run function uses pattern matching to either output a nicely colorized (with the help of Bunt) help message. Or, to start up our supervisor tree by manually calling ElixirLinter.start with an argument of the repo name passed in from the command line.

It's important to note that our Mixfile does not automatically start up our application.

Normally, your Mixfile would specify a key of mod, pointing to the module to run when the application is compiled and automatically started up. Something like this:

# mix.exs

...
def application do
  [applications: [:logger],
   mod: {ElixirLinter, []}]
end

Since we only want our supervisor tree to start up once we capture command line input, we wait to start up the tree manually once we've done so.

In order to tell our Mixfile not to automatically start up the app when it compiles, we simply remove the mod key from our application function:

# mix.exs

...
def application do
  [applications: [:logger]]
end

Building Our Executable

Last but not least, we will run the Mix task that builds our executable:

$ mix escripts.build

And that's it!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus