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 --lintwhere 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 inargv
as stringsinvalid
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!