Mix task: drive mix commands with Cheer

Copy Markdown View Source

Cheer is not only for escripts. The same command definition can power a Mix task, so mix greet world --loud parses, validates, and renders help exactly like a standalone CLI. No framework changes are needed.

Full runnable project: examples/mix_task/.

The task

use Cheer.MixTask combines use Mix.Task and use Cheer.Command and generates the Mix run/1 entry point. Declare the command and implement the leaf run/2:

defmodule Mix.Tasks.Greet do
  use Cheer.MixTask

  command "greet" do
    about "Greet someone with style"

    argument :name, type: :string, required: true, help: "Who to greet"

    option :greeting, type: :string, default: "Hello", env: "GREET_GREETING",
      help: "Greeting word"

    option :loud, type: :boolean, short: :l, help: "SHOUT the greeting"

    option :times, type: :integer, short: :n, default: 1,
      validate: fn n -> if n in 1..10, do: :ok, else: {:error, "times must be 1-10"} end,
      help: "Repeat the greeting"
  end

  @impl Cheer.Command
  def run(%{name: name} = args, _raw) do
    greeting = "#{args[:greeting]}, #{name}!"
    greeting = if args[:loud], do: String.upcase(greeting), else: greeting

    for _ <- 1..args[:times] do
      Mix.shell().info(greeting)
    end

    :ok
  end
end

Then mix greet world --loud parses, validates, and renders help exactly like a standalone CLI.

What the helper generates

use Cheer.MixTask gives you:

  • a run/1 Mix entry point that dispatches argv through the command with Cheer.run/3, using mix greet as the program name in help and usage output;
  • a {:error, :usage} to exit({:shutdown, 2}) translation, the Mix idiom for a nonzero exit;
  • @shortdoc defaulted to the command's about text, so mix help lists the task.

If you prefer to wire it by hand (or need to override run/1 for setup), the manual equivalent is a plain module that does use Mix.Task and use Cheer.Command (they coexist because run/1 and run/2 have different arities) with a run/1 that delegates:

@impl Mix.Task
def run(argv) do
  case Cheer.run(__MODULE__, argv, prog: "mix greet") do
    {:error, :usage} -> exit({:shutdown, 2})
    other -> other
  end
end

Run it

cd examples/mix_task
mix deps.get

mix greet world
# Hello, world!

mix greet world --loud --times 3
# HELLO, WORLD!
# HELLO, WORLD!
# HELLO, WORLD!

GREET_GREETING=Hey mix greet Ada
# Hey, Ada!

mix greet --help
# Usage: mix greet <name> [OPTIONS]
# ...

mix greet
# error: missing required argument(s): <name>
# ... (exits 2)

mix help and mix help greet render from @shortdoc and @moduledoc, the standard Mix mechanism, so the task lists and documents itself like any other.

Signaling failure

The helper translates a {:error, :usage} result into exit({:shutdown, 2}), the Mix idiom: Mix catches this exit and halts with the given code without a crash report.

Do not use Cheer.main/3 inside a Mix task. main/3 calls System.halt, which hard-kills the VM immediately: it skips Mix's own cleanup and is wrong inside mix, CI, and umbrella projects. The helper uses Cheer.run/3 for exactly this reason. main/3 is for escript entry points, where halting the VM is the whole point.

Starting the application

Mix does not start your application before running a task. If the task needs the app running (a repo, a supervision tree, config), start it inside run/2:

@impl Cheer.Command
def run(args, _raw) do
  Mix.Task.run("app.start")
  # ... now the app is running
end

Application.ensure_all_started/1 works too when you only need a specific application. If you would rather start the app before argv is even parsed, override the generated run/1 and call Cheer.run/3 yourself.

What it shows

  • use Cheer.MixTask -- one line that makes a Cheer command a Mix task, generating the run/1 entry point.
  • mix greet program name -- the usage line reads as the Mix invocation, not a bare command name.
  • Exit codes -- a usage failure becomes exit({:shutdown, 2}), a conventional nonzero exit.
  • Mix help integration -- @shortdoc defaults to the command's about.
  • The main/3 caveat -- never System.halt from inside a Mix task.

See also