Do you want to pick up from where you left of?
Take me there

Custom Mix Tasks

Creating custom Mix tasks for your Elixir projects.

Introduction

It’s not uncommon to want to extend your Elixir applications functionality by adding custom Mix tasks. Before we learn about how to create specific Mix tasks for our projects, let’s look at one that already exists:

$ mix phx.new my_phoenix_app

* creating my_phoenix_app/config/config.exs
* creating my_phoenix_app/config/dev.exs
* creating my_phoenix_app/config/prod.exs
* creating my_phoenix_app/config/prod.secret.exs
* creating my_phoenix_app/config/test.exs
* creating my_phoenix_app/lib/my_phoenix_app.ex
* creating my_phoenix_app/lib/my_phoenix_app/endpoint.ex
* creating my_phoenix_app/test/views/error_view_test.exs
...

As we can see from the shell command above, The Phoenix Framework has a custom Mix task to generate a new project. What if we could create something similar for our project? Well, the great news is we can, and Elixir makes this easy for us to do.

Setup

Let’s set up a basic Mix application.

$ mix new hello

* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/hello.ex
* creating test
* creating test/test_helper.exs
* creating test/hello_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

cd hello
mix test

Run "mix help" for more commands.

Now, in our lib/hello.ex file that Mix generated for us, let’s create a simple function that will output “Hello, World!”

defmodule Hello do
  @doc """
  Outputs `Hello, World!` every time.
  """
  def say do
    IO.puts("Hello, World!")
  end
end

Custom Mix Task

Let’s create our custom Mix task. Create a new directory and file hello/lib/mix/tasks/hello.ex. Within this file, let’s insert these 7 lines of Elixir.

defmodule Mix.Tasks.Hello do
  @moduledoc "The hello mix task: `mix help hello`"
  use Mix.Task

  @shortdoc "Simply calls the Hello.say/0 function."
  def run(_) do
    # calling our Hello.say() function from earlier
    Hello.say()
  end
end

Notice how we start the defmodule statement with Mix.Tasks and the name we want to call from the command line. On the second line, we introduce the use Mix.Task which brings the Mix.Task behaviour into the namespace. We then declare a run function which ignores any arguments for now. Within this function, we call our Hello module and the say function.

Loading your application

Mix does not automatically start our application or any of its dependencies which is fine for many Mix task use-cases but what if we need to use Ecto and interact with a database? In that case we need to make sure the app behind Ecto.Repo has started. There are 2 ways for us to handle this: explicitly starting an app or we can start our application which in turn will start the others.

Let’s look at how we can update our Mix task to start our application and dependencies:

defmodule Mix.Tasks.Hello do
  @moduledoc "The hello mix task: `mix help hello`"
  use Mix.Task

  @shortdoc "Simply calls the Hello.say/0 function."
  def run(_) do
    # This will start our application
    Mix.Task.run("app.start")

    Hello.say()
  end
end

Mix Tasks in Action

Let’s checkout our mix task. As long as we are in the directory it should work. From the command line, run mix hello, and we should see the following:

$ mix hello
Hello, World!

Mix is quite friendly by default. It knows that everyone can make a spelling error now and then, so it uses a technique called fuzzy string matching to make recommendations:

$ mix hell
** (Mix) The task "hell" could not be found. Did you mean "hello"?

Did you also notice that we introduced a new module attribute, @shortdoc? This comes in handy when shipping our application, such as when a user runs the mix help command from the terminal.

$ mix help

mix app.start         # Starts all registered apps
...
mix hello             # Simply calls the Hello.say/0 function.
...

Note: Our code must be compiled before new tasks will appear in the mix help output. We can do this either by running mix compile directly or by running our task as we did with mix hello, which will trigger the compilation for us.

It’s important to note that task names are derived from the module name, so Mix.Tasks.MyHelper.Utility will become my_helper.utility.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!