# CLI apps in Elixir. Part 2

In this post, we'll explore each tool described in [Part 1](https://brewingelixir.com/cli-apps-in-elixir-part-1) to see for ourselves the benefits and limitations of each alternative with the hope we'll end up with enough knowledge to decide which one fits best for each use case.

A nice approach to easily compare alternatives is building the same app with each tool. That way you can easily spot the similarities and differences between them.

For the sake of simplicity, you are going to build a simplified version of the [wc command](https://en.wikipedia.org/wiki/Wc_(Unix)). The `wc` command is short of "word count" and allows counting new lines, words, characters and a few more. But the core features we'll implement are:

* Parse command line arguments.
    
* Read from `stdin` and output to `stdout`.
    
* Support reading a single file when provided as an argument.
    
* Return the stats for newline, word and grapheme (this is not standard but we'll do it this way because it is nicer with UTF-8 files).
    

We'll ignore showing comprehensive help, well-formatted error messages, reading multiple files when provided and any other feature defined in its man page.

Now let's get into the code 🧑‍💻

# Business logic

To make things easier to read and understand let's create a POEM (Plain Old Elixir Module(\*)). We'll be able to use this module across implementations to focus on the differences.

*(\*) I've just made this up but took some inspiration from the Java world where classes holding only business logic are called POJOs (Plain Old Java Objects).*

Here's the definition of `WC`, a module that holds the logic to perform a subset of features offered by `wc`, specifically it counts graphemes, words and lines.

```elixir
defmodule WC do
  def run(args) do
    args
    |> parse_options()
    |> execute()
  end

  def parse_options(args) do
    OptionParser.parse(args,
      aliases: [l: :lines, w: :words, c: :chars],
      switches: [chars: :boolean, words: :boolean, lines: :boolean]
    )
  end

  def execute(options) do
    {file, opts} =
      case options do
        {opts, [], _} ->
          {:stdio, opts}

        {opts, [file | _], _} ->
          {file, opts}
      end

    case read_file(file) do
      {:ok, content} ->
        content
        |> count_content()
        |> print_results(file, opts)

      {:error, :file_not_found} ->
        IO.puts("File not found: #{file}")
        System.halt(1)
    end
  end

  @default_opts [lines: true, words: true, chars: true]

  def print_results(results, file, []) do
    print_results(results, file, @default_opts)
  end

  def print_results(results, file, opts) do
    result =
      Enum.reduce(@default_opts, "", fn {key, _}, acc ->
        if opts[key] do
          acc <> "\t#{results[key]}"
        else
          acc
        end
      end)

    if file == :stdio do
      IO.puts(result <> " " <> "\n")
    else
      IO.puts(result <> " " <> file <> "\n")
    end
  end

  def count_content(content) do
    content
    |> String.graphemes()
    |> Enum.reduce(%{lines: 0, words: 0, chars: 0}, fn char, acc ->
      cond do
        char == "\n" ->
          %{acc | lines: acc.lines + 1, chars: acc.chars + 1, words: acc.words + 1}

        char in [" ", "\t"] ->
          %{acc | words: acc.words + 1, chars: acc.chars + 1}

        true ->
          %{acc | chars: acc.chars + 1}
      end
    end)
  end

  def read_file(:stdio) do
    {:ok, IO.read(:stdio, :all)}
  end

  def read_file(file) do
    if File.exists?(file) do
      File.read(file)
    else
      {:error, :file_not_found}
    end
  end
end
```

Here's a summary of its features:

* Supports `-l` to count lines, `-w` to count words and `-c` to count graphemes.
    
* The first argument after the options should be a path to an existing file.
    
* When no file is provided it reads from `stdin`.
    
* When no option is provided it assumes the caller wants all stats (all switches are on).
    
* Returns an error code of `1` when the file doesn't exist and `0` if the execution was successful.
    

*Note: This is a naive implementation that takes some shortcuts to simplify the code for readability while still having some utility when running some examples.*

# Implementations

For testing purposes let's create a file named `sample.txt` with the following content:

```bash
This is one
simple text
file
1234
end line is this
```

From here on we'll focus only on the differences of each alternative. The full code can be found in [this Github repo](https://github.com/brewingelixir/cli-apps-in-elixir-series).

Also, will be using the `$` character before a shell command to indicate it runs as a non-root user, but most importantly to differentiate a command from its output within the same code block.

## Elixir Scripts

```elixir
# Assume the previous WC module is included here. E.g.
# defmodule WC do
# ...

args = System.argv()
WC.run(args)
```

Let's call this file `wc.exs` and run a few examples:

1. #### Default run
    

```bash
$ elixir wc.exs sample.txt
    5	11	51 sample.txt
```

1. Use a CLI pipe
    

```bash
$ cat sample.txt | elixir wc.exs
    5	11	51 sample.txt
```

1. Pass specific parameters
    

```bash
$ elixir wc.exs -l sample.txt
	5 sample.txt
```

Here you can see how the script gets interpreted by the `elixir` cli app and passes its arguments by taking everything after the `wc.exs` file. Notice how `elixir` needs to be installed as well as having the source code to run the app.

## Mix Run

Once the project starts requiring more structure and code distribution the defacto standard tool to use is Mix. So let's create an app using `mix` and reuse `wc.exs` by promoting to a `.ex` file. Also copy the `sample.txt` file within the project only for convenience.

```bash
mix new app1
cp wc.exs app1/lib/wc.ex
cp sample.txt app1/
cd app1
```

You should edit `app1/lib/wc.ex` by removing the last two lines and placing them in a new file called `run.exs` :

```elixir
args = System.argv()
WC.run(args)
```

Now let's run the application:

```bash
$ mix run run.exs sample.txt
	5	11	51 sample.txt
```

Awesome! You can leverage Mix features to easily organize and improve your projects. You still need the source code to run it this way but this is a quick way to run scripts from a Mix project. Let's improve this by using Mix releases.

## Mix Releases

Create another mix project like you did before but call it `app2` to have a fresh start.

```bash
mix new app2
cp wc.exs app2/lib/wc.ex
cp sample.txt app2/
cd app2
```

Remove the last 2 lines of `wc.ex` as before and create a module under `lib/cli.ex` with the following content:

```elixir
defmodule CLI do
  def run do
    args = System.argv()
    WC.run(args)
  end
end
```

This module will be the starting point for the app.

Next, you need to configure the project. For demo purposes, you'll create a tarball file of the project to be able to distribute it as a single file. So let's edit `mix.exs` and add:

```elixir
def project do
    [
      ...
      releases: releases()
    ]
  end

  def releases do
    [
      app2: [
        include_executables_for: [:unix],
        applications: [runtime_tools: :permanent],
        steps: [:assemble, :tar]
      ]
    ]
  end
```

To build a release run:

```elixir
MIX_ENV=prod mix release
```

We provided `MIX_ENV=prod` to build a release optimized for production use. If you don't pass the environment variable it will use `dev` by default.

The app is ready. Let's use `eval` and pass the `Module.Function` as the first argument and the rest will be provided to the CLI app as its arguments.

```bash
$ _build/prod/rel/app2/bin/app2 eval "CLI.run" -l sample.txt
	5 sample.txt
```

Even `stdin` will work:

```bash
$ cat sample.txt | _build/dev/rel/app2/bin/app2 eval "CLI.run" -lw
	5	11
```

Note: There's no filename in the output because it uses `stdin` as the source of information to parse.

This is all great and you can find the tarball containing the CLI app in `_build/prod/app2-0.1.0.tar.gz`. However, the person who will run this in their host still needs to uncompress and untar it (i.e. `tar xvzf _build/prod/app2-0.1.0.tar.gz`) to use it. In other words it isn't a single executable that you can pass around.

Let's check the next two options to address this final limitation while maintaining all the great features you collected so far.

## Escript

Once again create a new project and reuse `wc.exs` like you did so far:

```bash
mix new app3
cp wc.exs app3/lib/wc.ex
cp sample.txt app3/
cd app3
```

*Note: Remember to remove the last 2 lines used to execute the module's function.*

Next, set up the project to use `escript` and instruct which module should be used to kick off the app. Modify `mix.exs` to include:

```elixir
  def project do
    [
      ...
      escript: escript()
    ]
  end

  def escript do
    [main_module: CLI]
  end
```

Create a file under `lib/cli.ex` with:

```elixir
defmodule CLI do
  def main(args) do
    # No need to call System.argv() as it is provided by escript
    # as an argument to this function
    WC.run(args)
  end
end
```

To build the project use the `escript.build` task:

```bash
$ MIX_ENV=prod mix escript.build
Generated app3 app
Generated escript app3 with MIX_ENV=prod
```

Success! You have a single binary file representing your CLI app. Let's check its type and then test it!

```bash
$ file app3
app3: a /usr/bin/env escript script executable (binary data)
```

```bash
./app3 sample.txt
	5	11	51 sample.txt
```

Very cool! This single file can be easily distributed as long as the limitations described in [Part 1](https://brewingelixir.com/cli-apps-in-elixir-part-1) don't affect your use case. In case some do then prepare your hot sauce because you'll need it for the next tasty solution 🔥🌯.

## Burrito

Until now all alternatives were part of the standard Elixir distribution but thanks to the great work of the community and Burrito maintainers we now have a full-featured solution to build and distribute single binary apps for Elixir: [https://github.com/burrito-elixir/burrito](https://github.com/burrito-elixir/burrito)

Burrito requires [Zig](https://ziglang.org/) to be installed as well as `xz` so make sure you have them installed:

```bash
$ whereis xz
$
```

Let's set up a fresh app and reuse the `WC` module:

```bash
mix new app4
cp wc.exs app4/lib/wc.ex
cp sample.txt app4/
cd app4
```

Burrito is an external dependency so you'll need to add it to `mix.exs` under `deps` :

```elixir
  defp deps do
    [
      {:burrito, github: "burrito-elixir/burrito"}
    ]
  end
```

And then fetch the dependency package using mix:

```bash
mix deps.get
```

Now let's set it up in `mix.exs`

```elixir
def project do
  [
    # ... other project configuration
    releases: releases()
  ]
end

def releases do
  [
    app4: [
      steps: [:assemble, &Burrito.wrap/1],
      burrito: [
        targets: [
          macos: [os: :darwin, cpu: :x86_64],
          linux: [os: :linux, cpu: :x86_64]
        ]
      ]
    ]
  ]
end
```

Sweet! Burrito leverages Mix releases which means you get all their benefits plus the ones from Burrito.

Next You need to define a starting point for the app, so edit `mix.exs` but this time add the following change to it:

```elixir
def application do
  [
    ...
    mod: {CLI, []}
  ]
end
```

`CLI` is just a module name so let's create it under `lib/cli.ex`

```elixir
defmodule CLI do
  use Application

  def start(_type, _args) do
    args = Burrito.Util.Args.get_arguments()
    WC.run(args)

    System.halt(0)
  end
end
```

To build the artifact let's run:

```bash
MIX_ENV=prod mix release
```

The targets can be found under the `burrito_out` directory within the current project. Without specifying a target you end up building all of them listed in your mix configuration file.

To test the app run:

```bash
$ ./burrito_out/app4_macos -l sample.txt
	5 sample.txt
```

Awesome! Let's check the file's type:

```bash
$ file burrito_out/*
burrito_out/app4_linux: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, stripped
burrito_out/app4_macos: Mach-O 64-bit executable x86_64
```

Beautiful! That looks like executables for specific OS and architectures. For more details check out the [Preparation and Requirements section](https://github.com/burrito-elixir/burrito#preparation-and-requirements) of their readme.

🚨 Important: Burrito will install the app based on its mix version. If you perform a change to your code and run `mix release` without uninstalling the app you'll get the previous version executed not the current one. So make sure you either:

a. Bump the version in `mix.exs`

b. Uninstall the current version: `burrito_out/app4_macos maintenance uninstall`

Hope this last tip saves you some time or headaches 😉

# Summary

All options are valid and useful but in general, **Escript** or **Burrito** solutions are what you want to use when building non-trivial single binaries CLI apps in Elixir. But if in doubt then start with a single `.exs` file and see how far you can get until you start needing more sophisticated solutions.

This concludes the second part of this series. Hope you enjoyed it and found it useful! 🍺

%%[convertkit]
