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
- Definition: Use the provided macros (
defcommand
,subcommand
,flag
,value
, etc.) to define your CLI's structure in a clear and organized way. - Compilation: During compilation, Nexus processes your definitions, builds an abstract syntax tree (AST), and validates your commands and flags.
- Parsing: When your application runs, Nexus parses the user input (e.g., command-line arguments) against the defined AST, handling flags, arguments, and subcommands.
- Dispatching: After successful parsing, Nexus dispatches the command to your
handle_input/2
callback, passing the parsed input. - 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 containingflags
,args
, andvalue
.
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:
- 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
- Build the Escript:
mix escript.build
- 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:
- Add Burrito Dependency: Add Burrito to your
mix.exs
:
defp deps do
[
{:burrito, github: "burrito-elixir/burrito"}
]
end
- 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
- Build the Release:
MIX_ENV=prod mix release
- 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:
- 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
- Run the Task:
mix my_cli file copy --verbose source.txt dest.txt
Additional Information
- Version and Description: By default,
version/0
anddescription/0
callbacks fetch information frommix.exs
and@moduledoc
, respectively. You can override them if needed. - Error Handling: Use the
{:error, {code, reason}}
tuple to return errors fromhandle_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
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 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
@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
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.
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
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
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
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
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
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
). CheckNexus.CLI.value()
typeopts
- 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