Intro

Elixir is easy to learn and use and combined with Mix it is a great tool for automating tedious or repetitive tasks. Follow along as we discuss the steps you would take to automate a task, we will look at examples as well as alternatives.

What and why

Have you ever had a tedious or repetitive task that you wished you could automate? Maybe it’s a task at work, or maybe it’s something you do in your personal life. Whatever it is, automating it could save you a lot of time and hassle.

In this blog post, we’ll discuss practical steps for writing our very own Mix task. Mix tasks are a great way to automate common tasks. They can be used to:

First, this post is not about creating a standalone command line application. Instead, we are taking a much simpler approach by looking at ways to use Mix tasks to automate parts of our job or personal admin tasks.

This post also assumes you have Erlang and Elixir already installed on your development computer.

With that out of the way, let us get started.

How to get started

Create a new app

Mix is a build tool that provides tasks for creating, compiling, testing Elixir projects, and much more. To create a new Elixir app, follow these steps:

  1. Open a terminal window and navigate to the directory where you want to create your app.
  2. Run the following command:
Mix new <app-name>

Replace <app-name> with the name of your app. For example, to create an app named “my_app”, you would run the following command:

$ Mix new my_app

Mix will create a new directory for your app, with the following files:

Mix new allows for more options to customize exactly how the project should be created. To see these options you can run Mix help new.

Once done, remember to navigate into your project directory, for the above example we would run the following command:

cd my_app

Fetch dependencies

Elixir apps can depend on other Elixir libraries, called “dependencies”. To fetch the dependencies for your app, run the following command:

Mix deps.get

This will download the latest versions of your dependencies and add them to your project.

Add a task

To add a new task, follow these steps:

  1. Create a new file in the lib/Mix/tasks directory.
  2. Name the file after the task you want to create. For example, to create a task to parse a CSV file, you would name the file parse_csv.ex.
  3. In the file, define a module that starts with Mix.Tasks. For example:
defmodule Mix.Tasks.ParseCsv do
  use Mix.Task
end
  1. Define a run/1 function in the module. This function will be called when the task is executed. For example:
defmodule Mix.Tasks.ParseCsv do
  use Mix.Task

  def run(args) do
    # Do something to parse the CSV file.
  end
end
  1. You can now run the task by typing Mix parse_csv in the terminal. Mix will compile the project before it runs the task.

Some things that you might note is:

Inside of the file we would create a module that contains the code required to complete the task we are automating. If you need some inspiration you can go visit the tasks defined in the phoenix project we might even want to look at Mix new task we used to create our project here.

Add help text

Help text is a great way to document your Mix tasks and make them easier to use. You can add help text to your tasks using the @shortdoc and @moduledoc attributes.

The @shortdoc attribute is used to add a short description of the task. This description will be displayed when the user runs the Mix help command. For example:

@shortdoc "Creates a new Elixir project"

The @moduledoc attribute is used to add a longer description of the task. This description will be displayed when the user runs the Mix help <task-name> command. For example:

@moduledoc """
Creates a new Elixir project.

Elaborates a bit on what it mean to create a new project.

## Examples

	$ Mix new hello_world
 
## Command line options

	* `-u`, `--umbrella` - generate an umbrella application
"""

I recommend that you always add help text to your Mix tasks. This will make your tasks easier to use and will help other people understand what your tasks do.

Here are some additional tips for writing good help text:

Take a look at the @shortdoc and @moduledoc attributes from these repos. Notice that they contain clear explanations, usage examples and insight into the available command line options.

Use OptionParser

OptionParser is a built-in Elixir module that provides a simple way to parse command line arguments. Command line arguments are often referred to as “switches” or “flags”. An “argument” refers to the value that is assigned to the switch.

OptionParser provides some conveniences out of the box, such as aliases and automatic handling of negation switches and you can read more about them in the documentation.

The parse/2 function accepts command line options and returns a keyword list of switches and arguments. Below is an example showing how we could use parse/2 to accept a debug flag:

OptionParser.parse(["--debug"], strict: [debug: :boolean])
# ```
{[debug: true], [], []}

This will return a tuple of the form {parsed, args, invalid}. The parsed value is a keyword list of parsed switches with {switch_name, value} tuples in it. The switch_name is the atom representing the switch name while the value is the value for that switch.

The args value is a list of arguments that were not parsed as switches. The invalid value is a list of switches that were not recognized.

Here is an example of how we might use OptionParser in our parse_csv task:

defmodule Mix.Tasks.ParseCsv do
  use Mix.Task

  @shortdoc "A short description of the parse_csv task."
  
  @moduledoc """
    Help text with examples and command line options.
  """

  @switches [
    file: :string,
    debug: :boolean
  ]

  def run(args) do
    {parsed, _, _} = OptionParser.parse(args, switches: @switches)

    # Do something with the parsed switches and arguments.
  end
end

Testing

Testing a Mix task with ExUnit is no different from any other testing that we might do, but there are 3 things we would need to do to write our tests:

# Get Mix output sent to the current
# process to avoid polluting tests.
Mix.shell(Mix.Shell.Process)

defmodule Mix.Tasks.ParseCsvTest do
  use ExUnit.Case, async: true

  describe "run/1" do
    test "outputs parsed csv data" do
      # Run the task.
      Mix.Tasks.ParseCsv.run(["--file", "tmp/my_file.csv"])

      # Capture the output.
	  assert_received {:Mix_shell, :info, [output]}

      # Make assertions against the output.
      assert output == """
               Acc Name, 2023/05/02, Food, , 280.00
               """
    end
  end
end

To prevent polluting test output we swap the default shell with a test shell that sends the output to our current test process.

# Get Mix output sent to the current
# process to avoid polluting tests.
Mix.shell(Mix.Shell.Process)

Notice how we include command line options, --file tmp/my_file.csv in the run step of our test.

The assert_receive assertions is particularly well suited for capturing messages sent to the shell. It has the added benefit of allowing us to pattern match on the message, command line output in this case, that we expect to receive.

Elixir for CLI applications

Are you interested in creating a command line application? If so, you might want to explore ElixirScript. ExlixirScript files have the .exs file extension. However, it’s important to note that ElixirScript has certain limitations, such as the lack of straightforward inclusion of other modules.

To overcome some of these limitations, you can consider investigating Escript. Escript enables the distribution of your entire Elixir project as a standalone executable, which can be beneficial for command line applications.

Another tool worth exploring is Burrito. It allows you to build an Elixir command line application that can be distributed across various operating systems like Linux, macOS, or Windows.

One drawback of Elixir command line applications is their relatively long startup time. Consequently, they may not be the most ideal choice for applications where startup time is a critical concern. However, in cases where startup time is not a significant factor, delving into Elixir can be highly rewarding.

Conclusion

Elixir is a powerful programming language that can be used to create a variety of applications. In this post post, we have discussed how to create a new Elixir project, add a task, use OptionParser and then how to test a task.

We briefly looked at ElixirScript, Escript and Burrito to create command-line applications.

I hope this blog post has been helpful in guiding you to use Elixir and Mix to automate tasks that you previously performed manually.

References