View Source Nexus.CLI behaviour (nexus_cli v0.5.0)

Nexus.CLI provides a macro-based DSL for defining command-line interfaces with commands, flags, and positional arguments using structured ASTs with structs.

Overview

The Nexus.CLI module allows you to build robust command-line applications by defining commands, subcommands, flags, and arguments using a declarative syntax. It handles parsing, validation, and dispatching of commands, so you can focus on implementing your application's logic.

Command Life Cycle

  1. Definition: Use the provided macros (defcommand, subcommand, flag, value, etc.) to define your CLI's structure in a clear and organized way.
  2. Compilation: During compilation, Nexus processes your definitions, builds an abstract syntax tree (AST), and validates your commands and flags.
  3. Parsing: When your application runs, Nexus parses the user input (e.g., command-line arguments) against the defined AST, handling flags, arguments, and subcommands.
  4. Dispatching: After successful parsing, Nexus dispatches the command to your handle_input/2 callback, passing the parsed input.
  5. Execution: You implement the handle_input/2 function to perform the desired actions based on the command and input.

The handle_input/2 Callback

The handle_input/2 function is the core of your command's execution logic. It receives the command path and an Nexus.CLI.Input struct containing parsed flags and arguments.

Signature

@callback handle_input(cmd :: atom | list(atom), input :: Input.t()) :: :ok | {:error, error}
  • cmd: The command or command path (list of atoms) representing the executed command or a single atom if no subcommand is provided.
  • input: An %Nexus.CLI.Input{} struct containing flags, args, and value.

Return Values

  • :ok: Indicates successful execution. The application will exit with a success code (0).
  • {:error, {code :: integer, reason :: String.t()}}: Indicates an error occurred. The application will exit with the provided error code.

Running the CLI Application

You can run your CLI application using different methods:

Using mix run

If you're developing and testing your CLI, you can run it directly with mix run:

mix run -e 'MyCLI.execute("file copy source.txt dest.txt --verbose")'

Compiling with Escript

Escript allows you to compile your application into a single executable script.

Steps:

  1. Add Escript Configuration: In your mix.exs, add the :escript configuration:
def project do
  [
    app: :my_cli,
    version: "0.1.0",
    elixir: "~> 1.12",
    escript: [main_module: MyCLI],
    deps: deps()
  ]
end
  1. Build the Escript:
mix escript.build
  1. Run the Executable:
./my_cli file copy --verbose source.txt dest.txt

Note that in order to use and distribute escript binaries, the host needs to have Erlang runtime available on $PATH

Compiling with Burrito

Burrito allows you to compile your application into a standalone binary.

Steps:

  1. Add Burrito Dependency: Add Burrito to your mix.exs:
defp deps do
  [
    {:burrito, github: "burrito-elixir/burrito"}
  ]
end
  1. Configure Releases: Update your mix.exs with Burrito release configuration:
def project do
  [
    app: :my_cli,
    version: "0.1.0",
    elixir: "~> 1.12",
    releases: releases()
  ]
end

def releases do
  [
    my_cli: [
      steps: [:assemble, &Burrito.wrap/1],
      burrito: [
        targets: [
          macos: [os: :darwin, cpu: :x86_64],
          linux: [os: :linux, cpu: :x86_64],
          windows: [os: :windows, cpu: :x86_64]
        ]
      ]
    ]
  ]
end
  1. Build the Release:
MIX_ENV=prod mix release
  1. Run the Binary:
./burrito_out/my_cli_macos file copy --verbose source.txt dest.txt

Using Mix Tasks

You can also run your CLI as a Mix task.

Steps:

  1. Create a Mix Task Module:
defmodule Mix.Tasks.MyCli do
  use Mix.Task

  @shortdoc "Runs the MyCLI application"

  def run(args) do
    MyCLI.execute(args)
  end
end
  1. Run the Task:
mix my_cli file copy --verbose source.txt dest.txt

Additional Information

  • Version and Description: By default, version/0 and description/0 callbacks fetch information from mix.exs and @moduledoc, respectively. You can override them if needed.
  • Error Handling: Use the {:error, {code, reason}} tuple to return errors from handle_input/2. The application will exit with the specified code, and the reason will be printed.

Summary

Types

Represents the CLI spec, basically a list of Command.t() spec

Represents an final-user error while executing a command

t()

Represent all possible value types of an command argument or flag value

Callbacks

Custom banners can be set

Sets the CLI description

Function that receives the current command being used and its args

Sets the version of the CLI

Functions

Given a the CLI spec and the user input, tries to parse the input against the spec and dispatches the parsed result to the CLI handler module - the one that imeplement Nexus.CLI behaviour.

Defines a top-level command for the CLI application.

Sets the description for a command, subcommand, or flag.

Defines a flag (option) for a command or subcommand.

Defines a short alias for a flag.

Defines a subcommand within the current command.

Defines a positional argument for a command or a flag.

Types

@type ast() :: [Nexus.CLI.Command.t()]

Represents the CLI spec, basically a list of Command.t() spec

@type error() :: {code :: integer(), reason :: String.Chars.t()}

Represents an final-user error while executing a command

Need to inform the return code of the program and a reason of the error

@type t() :: %Nexus.CLI{
  description: String.t(),
  handler: module(),
  name: atom(),
  otp_app: atom(),
  spec: ast(),
  version: String.t()
}
@type value() ::
  :boolean
  | :string
  | :integer
  | :float
  | {:list, value()}
  | {:enum, [atom() | String.t()]}

Represent all possible value types of an command argument or flag value

Callbacks

@callback banner() :: String.t()

Custom banners can be set

@callback description() :: String.t()

Sets the CLI description

Default implementation fetches from @moduledoc, however take in account that if you're compiling your app as an escript or single binary (rg. burrito) the @moduledoc attribute may be not available on runtime

Fetch module documentation on compile-time is marked to Elixir 2.0 check https://github.com/elixir-lang/elixir/issues/8095

Link to this callback

handle_input(cmd, input)

View Source
@callback handle_input(cmd :: atom(), input :: Nexus.CLI.Input.t()) ::
  :ok | {:error, error()}
@callback handle_input(cmd :: [atom()], input :: Nexus.CLI.Input.t()) ::
  :ok | {:error, error()}

Function that receives the current command being used and its args

If a subcommand is being used, then the first argument will be a list of atoms representing the command path

Note that when returning :ok from this function, your program will exit with a success code, generally 0

To inform errors, check the Nexus.CLI.error() type

Examples

@impl Nexus.CLI
def handle_input(:my_cmd, _), do: nil

def handle_inpu([:my, :nested, :cmd], _), do: nil
@callback version() :: String.t()

Sets the version of the CLI

Default implementation fetches from the mix.exs

Functions

@spec __run_cli__(t(), binary()) :: :ok

Given a the CLI spec and the user input, tries to parse the input against the spec and dispatches the parsed result to the CLI handler module - the one that imeplement Nexus.CLI behaviour.

If the dispatchment is successfull and the handle_input/2 return an :ok, then it stops the VM with a success code.

If there is a parsing error it will display the CLI help and stop the VM with error code.

If handle_input/2 returns an error, it stops the VM with the desired code.

Link to this macro

defcommand(name, list)

View Source (macro)

Defines a top-level command for the CLI application.

Use this macro to declare a new command along with its subcommands, arguments, and flags.

Parameters

  • name - The name of the command (an atom).
  • do: block - A block containing the command's definitions.

Examples

defcommand :my_command do
  # Define subcommands, flags, and arguments here
end
Link to this macro

description(desc)

View Source (macro)

Sets the description for a command, subcommand, or flag.

Use this macro within a defcommand, subcommand, or flag block to provide a description.

Parameters

  • desc - The description text (a string).

Examples

defcommand :my_command do
  description "Performs the main operation."

  flag :verbose do
    description "Enables verbose mode."
  end
end
Link to this macro

flag(name, list)

View Source (macro)

Defines a flag (option) for a command or subcommand.

Use this macro within a command or subcommand block to declare a new flag and its properties.

Flags can have arguments too, therefore you can safely use the value/2 macro inside of it.

Parameters

  • name - The name of the flag (an atom).
  • do: block - A block containing the flag's definitions.

Examples

defcommand :my_command do
  flag :verbose do
    short :v
    description "Enables verbose mode."
  end
end
Link to this macro

short(short_name)

View Source (macro)

Defines a short alias for a flag.

Use this macro within a flag block to assign a short (single-letter) alias to a flag.

Parameters

  • short_name - The short alias for the flag (an atom).

Examples

flag :verbose do
  short :v
  description "Enables verbose mode."
end
Link to this macro

subcommand(name, list)

View Source (macro)

Defines a subcommand within the current command.

Use this macro inside a defcommand or another subcommand block to define a nested subcommand.

Parameters

  • name - The name of the subcommand (an atom).
  • do: block - A block containing the subcommand's definitions.

Examples

defcommand :parent_command do
  subcommand :child_command do
    # Define subcommands, flags, and arguments here
  end
end
Link to this macro

value(type, opts \\ [])

View Source (macro)

Defines a positional argument for a command or a flag.

Use this macro to specify an argument's type and options within a command, subcommand, or flag block.

Parameters

  • type - The type of the argument (e.g., :string, :integer). Check Nexus.CLI.value() type
  • opts - A keyword list of options (optional).

Options

  • :required - Indicates if the argument is required (boolean).
  • :as - The name of the argument (atom), required if multiple values are defined otherwise the name will be the same of the command that it defined
  • :default - Defines the default value if the argument is not provided

Examples

defcommand :my_command do
  value :string, required: true, as: :filename
end

flag :output do
  value :string, required: true
end