I’ve taken a break from Dagger to build a library for writing CLI apps in Elixir. When I started building Dagger I knew I would eventually need a CLI interface. I hoped someone had already built the Elixir equivalent of Click, a full featured CLI app library written in Python.

Sadly, my search came up empty. My first instinct was to ignore the siren call of Yet Another Problem and build the simplest thing I needed to make progress with Dagger.

I built a minimal app using OptionParser and some scaffolding to support sub-commands. It worked but the code was not pretty. Handling optional and required CLI options and enforcing option types were especially hacky. After a couple of days of work it became clear to me a more principled approach was necessary.

I decided to treat my first attempt as a learning experience and thought about what features I needed.

  1. Typed CLI options w/validation
  2. Support for optional and required options
  3. Sub-command support
  4. Easy generation of pretty terminal output

I gave myself a week to build a library which met these requirements. Clik is the result.

Introducing Clik

Clik is designed to simplify writing CLI apps in Elixir packaged as self-contained escripts. App authors build commands by implementing the Clik.Command behavior. Instances of the Clik.Option struct model CLI options including short names, long names, data type, and optional vs. required.

Clik also includes very basic templating inspired by Elixir’s Inspect.Algebra. App authors can use it to generate lines of text with correct line terminators (think Unix vs. Windows) and tables. I’m especially happy tables are responsive to terminal width and will elide text longer than column width with ellipses.

Hello, Clik

This is the complete source to a self-contained CLI app using Clik. The complete Mix project is available on Github here.


defmodule HelloClik do
  use Clik.Command
  alias Clik.{Command, CommandEnvironment, Configuration, Option, Platform}
  alias Clik.Renderable
  alias Clik.Output.Table

  def help_text(), do: "Say hello to the world"

  def options() do
    %{
      verbose: Option.new!(:verbose, short: :v, type: :boolean, help: "Be verbose"),
      table: Option.new!(:table, short: :t, type: :boolean, help: "Use a table")
    }
  end

  def run(env) do
    use_tables = Keyword.get(env.options, :table, false)

    greeting =
      if Keyword.get(env.options, :verbose, false) == true do
        "Greetings and salutations!"
      else
        "Hello!"
      end

    write_output(use_tables, greeting, env.output)
  end

  defp write_output(false, greeting, out) do
    IO.puts(out, greeting)
  end

  defp write_output(true, greeting, out) do
    t =
      Table.empty(2, ["Type", "Phrase"])
      |> Table.add_row(["Greeting", greeting])

    Renderable.render(t, out)
  end

  def main(args) do
    config =
      Configuration.add_command!(
        Configuration.new(),
        Command.new!(:hello, __MODULE__)
      )

    env = CommandEnvironment.new(Platform.script_name())
    Clik.run(config, args, env)
  end
end

HelloClik exercises all Clik’s major features. Let’s break it down.


defmodule HelloClik do
  use Clik.Command
  .
  .
  .
  def help_text(), do: "Say hello to the world"

  def options() do
    %{
      verbose: Option.new!(:verbose, short: :v, type: :boolean, help: "Be verbose"),
      table: Option.new!(:table, short: :t, type: :boolean, help: "Use a table")
    }
  end

  def run(env) do
    .
    .
    .
  end
end

The Clik.Command behavior defines 3 possible callback functions: help_text/0, options/0, and run/1. Of these only run/1 is required. Clik.Command provides overrideable do-nothing implementations of help_text/0 and options/0. run/1 is where the command implementation executes its main logic. Clik passes a Clik.CommandEnvironment struct which contains parsed options, arguments, input, output, and error IO streams.

If a command has specific options it should implement options/0 and return a map of Clik.Option structs.


def options() do
  %{
    verbose: Option.new!(:verbose, short: :v, type: :boolean, help: "Be verbose"),
    table: Option.new!(:table, short: :t, type: :boolean, help: "Use a table")
  }
end

Clik supports option types of string (default), integer, boolean, and count. Count options increment their value on each occurrence. This is meant for cases where a command wants to specify a level based on the number of times an option occurs. Options can have help text, single-letter short name aliases, indicate when they are mandatory, and provide default values.

The Clik.Output package contains basic output templating. Commands can build formatted output via Clik.Output.Document and Clik.Output.Table. The example command formats its output in a table if the command is called with the -t or --table options. Clik’s tables are automatically responsive to terminal width.


defp write_output(true, greeting, out) do
  t =
    Table.empty(2, ["Type", "Phrase"])
    |> Table.add_row(["Greeting", greeting])

  Renderable.render(t, out)
end

Clik is available on Github on the develop branch. As soon as I’m done writing docs I’ll cut a release and publish a hex package.

The best place to ask questions is the Fediverse since I’m online pretty much all the time.