Defining Commands

Commands, arguments, and options with Terminalwire.CLI

Terminalwire.CLI turns a module into a command router. Public functions become commands, their parameters become arguments, and @desc becomes the help text. It covers the common case (named commands with positional arguments) without a parser library. When you need flags or subcommands, drop down to a plain handler.

Commands

A public function is a command. Its name is what the user types.

defmodule MyApp.CLI do
  use Terminalwire.CLI, name: "my-app"

  @desc "Say hello"
  def hello do
    puts("Hello, World!")
  end
end
my-app hello
Hello, World!

Running my-app with no command (or my-app help) prints the generated list of commands and their @desc text.

Arguments

Function parameters are positional arguments, parsed left to right.

@desc "Greet NAME with GREETING"
def greet(greeting, name) do
  puts("#{greeting}, #{name}!")
end
my-app greet "Good morning" Ada
Good morning, Ada!

A default value makes an argument optional.

@desc "Say hello to NAME, or the world"
def hello(name \\ "World") do
  puts("Hello, #{name}!")
end
my-app hello
Hello, World!
my-app hello Ada
Hello, Ada!

Flags and subcommands

Terminalwire.CLI deliberately stops at positional arguments. For --flags, -o options, or nested subcommands, write a handler (a run/1 function that takes a Terminalwire.Server.Context) and parse the args yourself. Optimus is a good fit.

defmodule MyApp.Deploy do
  alias Terminalwire.Server.Context

  @optimus Optimus.new!(
    name: "deploy",
    args: [env: [required: true]],
    flags: [verbose: [short: "-v", long: "--verbose"]]
  )

  def run(ctx) do
    # Don't use Optimus.parse!/2 — it calls System.halt on error, which would
    # take your server down. Parse the result and return an exit code instead.
    case Optimus.parse(@optimus, Context.args(ctx)) do
      {:ok, parsed} ->
        Context.puts(ctx, "Deploying #{parsed.args.env}…")
        if parsed.flags.verbose, do: Context.puts(ctx, "(verbose)")
        0

      {:error, message} ->
        Context.warn(ctx, message)
        1
    end
  end
end

Mount a handler the same way you mount a Terminalwire.CLI module: pass &MyApp.Deploy.run/1 as the :handler. Both forms receive the same Context, so you can mix them: a Terminalwire.CLI router for everyday commands, a handler for the one command that needs rich parsing.

Never call System.halt (or anything that does, like Optimus.parse!). Your command runs inside your server process; halting takes the whole server down. Return an integer exit code instead, or call Context.exit(ctx, status).

Next: Standard I/O covers reading and writing the terminal in more detail.