View Source Tux.Dispatcher behaviour (Tux v0.4.0)
Dispatchers are modules which control program flow by directing the command execution to the appropriate command module in response to a given command/sub-command invoked by the CLI's end user.
Creating Dispatchers
New dispatchers are created by invoking use Tux.Dispatcher
in your own
modules, and then employing the various macros injected to register
Tux.Command
modules for their respective command names.
Example
defmodule MyProgram do
use Tux.Dispatcher, opts
cmd "ping", PingCmd, preloads: [:auth]
cmd "pong", PongCmd, preloads: [:auth, :log]
cmd "group", AnotherDispatcherWithSubcommands
cmd ~w"h hello", HelloCmd
cmd ~p".a", CommandTriggeredByDotAPrefix
# Alternatively
pre [:config, :auth, :log] do
cmd "ping", PingCmd
cmd "pong", PongCmd
end
end
Dispatcher Options
A dispatcher's behaviour can be customized by providing a list of options:
use Tux.Dispatcher, [... rescue: true, colors: false, ...]
Available dispatcher options are:
:rescue
- a boolean flag to rescue from command exceptions (default isfalse
):device
- theIO.device/0
to write the command output to (default is:stdio
):newlines
- a boolean flag to append new lines to outputs for nicer outputs (default istrue
):colors
- a boolean flag to enable/disable colored outputs (default istrue
):preloads
- a list of functions to execute prior to executing a command module (default is[]
). The results of these preloads will be accessible in the command module'sTux.Command.main/2
callback asenv.pre.*
, where*
is the function name or the key (if one was specified). – See Dispatcher Preloads below for more details.:exitwith
- the name of the function of theSystem
module to invoke in order to terminate the VM when a command returns an error result. Accepted values are:halt
and:stop
. (default is:halt
). See Erlang VM Exits below for more details.
Erlang VM Exits
When a command module returns an error, tux will terminate your program with a non-zero exit code, so the failure can be reflected in the shell status code.
By default, in the case of errors tux terminates the program with System.halt/1
(the default escript behaviour) because it is faster and we want responsive CLIs.
However, if for whatever reason you need your system to gracefully come to a stop
(say you have processes which need to perform come cleanup), you can change
this behaviour by specifying exitwith: :stop
in the dispatcher's options.
Dispatcher Preloads
In many cases, prior to the actual execution of a command, some piece of data is needed (e.g. data read from a config file, etc). And since this pattern could recur across many commands of a given CLI, tux introduces the concept of preloads.
Preloads are functions to be executed sequentially, prior to the invocation of
a command module, and whose results will be collected under the .pre
key of the
Tux.Env
struct passed to the command module's Tux.Command.main/2
callback.
Preloads can be declared at both the dispatcher definition and command registration levels, and are executed sequentially in the order in which they were defined in the tux command hierarchy. Thus, the can be declared:
- At dispatcher level – using the
:preloads
dispatcher option. (executed 1st) - At command registration level - using the
pre/2
(2nd) andcmd/3
(3rd) macros.
Preloads Format
Preloads can specified using any of the following formats (Note that key
, fun
,
and mod
are atoms) and are found under the Tux.Env
s .pre
key:
Preload Format | Example | Preload Result |
---|---|---|
fun | :config | env.pre.<fun> |
{key, fun} | {c!: :config} | env.pre.<key> |
{mod, fun, args} | {MyPre, :config, []} | env.pre.<fun> |
{key, {mod, fun, args}} | {:mc!, {MyPre, :config, []}} | env.pre.<key> |
When a preload is specified using a function name, that function must exist in the dispatcher where that preload is being specified.
Preloads Examples
Here's a list of preloads specified using multiple formats at once:
defmodule MyPreloads do
def config(env), do: "My config file"
def config(env, :upcase), do: "MY CONFIG FILE"
end
defmodule MyDispatcher do
use Tux.Dispatcher
cmd "show", MyCmd, preloads: [:conf,
c1: :conf,
c2: {MyPreloads, :config, []},
c3: {MyPreloads, :config, [:upcase]}
]
def conf(env), do: "My config file"
end
defmodule MyCmd do
use Tux.Command
def main(env, _) do
IO.inspect(env.pre.conf)
IO.inspect(env.pre.c1)
IO.inspect(env.pre.c2)
IO.inspect(env.pre.c3)
end
end
And here's an example with preloads specified at multiple levels in the tux command hierarchy:
defmodule MyDispatcher do
use Tux.Dispatcher, preloads: [:auth, one: :inc]
def auth(_env), do: true
def inc(_env), do: 1
def inc(env, prev), do: env.pre[prev] + 1
defmodule CollectCmd do
use Tux.Command
@impl true
def main(env, _args) do
true = env.pre.auth
{:ok, "Summing all preloads should yield three: #{env.pre.three}"}
end
end
pre [two: {MyDispatcher, :inc, [:one]}] do
cmd "collect", CollectCmd, preloads: [three: {MyDispatcher, :inc, [:two]}]
end
end
Dispatcher Internals
A dispatcher is broken up internally into various stages:
Summary
Types
Command module associated with a command name
Command name used for command module registration
Command registration options.
An output IO.device/0
such as :stdio
or another process.
Preloads are list of functions to execute prior to a command.
The struct which encodes the dispatcher and its options.
Callbacks
Return an {:ok, _}
tuple which contains a string or the Tux.Help
struct, and
whose purpose is to encode the dispatcher's help message.
Dispatch command execution to the appropriate command module registered for a given command name extracted from the command line arguments.
Functions
Perform sanity checks for a dispatcher's registered command names and associated command modules.
Inject the utility functions whose role is to retrieve the dispatcher options at runtime (used internally).
Inject the core dispatcher functionality for registration and dispatch of command modules.
Register a command module for a given command name in the current dispatcher.
Return a new dispatcher struct, and override its default values
with those given in the args
parameter
Register a series of commands with common preloads, thus you can replace the following block
A sigil which can be used to register a command module with a given prefix instead of a complete name.
Types
@type cmd_module() :: module()
Command module associated with a command name
@type cmd_name() :: String.t()
Command name used for command module registration
@type cmd_opts() :: [{:preloads, preloads()}]
Command registration options.
@type device() :: IO.device()
An output IO.device/0
such as :stdio
or another process.
@type preloads() :: [ atom() | {atom(), atom()} | {module(), atom(), list()} | {atom(), {module(), atom(), list()}} ]
Preloads are list of functions to execute prior to a command.
They can be specified in the following formats:
- as a FUNCTION name
- as the {KEY, FUNCTION name} tuple
- as the {MODULE, FUNCTION name, Args} tuple
- as the {KEY, {MODULE, FUNCTION name, Args}} tuple
@type t() :: %Tux.Dispatcher{ colors: boolean(), device: device(), exitwith: :halt | :quit, newlines: boolean(), preloads: preloads(), rescue: boolean() }
The struct which encodes the dispatcher and its options.
Callbacks
@callback help() :: {:ok, Tux.Help.t()}
Return an {:ok, _}
tuple which contains a string or the Tux.Help
struct, and
whose purpose is to encode the dispatcher's help message.
This callback is injected automatically when use Tux.Dispatcher
is invoked, however it can also be overwritten.
@callback main(args :: [String.t()]) :: :ok
Dispatch command execution to the appropriate command module registered for a given command name extracted from the command line arguments.
Using its main/1
function, a dispatcher can also serve as the main module
of an escript.
Functions
Perform sanity checks for a dispatcher's registered command names and associated command modules.
Inject the utility functions whose role is to retrieve the dispatcher options at runtime (used internally).
Inject the core dispatcher functionality for registration and dispatch of command modules.
Use Options:
:rescue
(boolean) - when set to true, if will rescue from command exceptions and when false it will show the exception's stacktrace (default is false).:device
(io device) - the device where to send the command results (default is :stdio).:newlines
(boolean) - whether to print the newline character with successful, non-empty command results (defaults is true).
Use Examples:
defmodule GeneralDispatcher do
use Tux.Dispatcher
# ...
end
defmodule CatchExceptionsDispatcher do
use Tux.Dispatcher, rescue: true, colors: false
# ...
end
@spec cmd(cmd_name(), cmd_module(), cmd_opts()) :: Macro.t()
Register a command module for a given command name in the current dispatcher.
This macro is imported automatically whenever you use Tux.Dispacher
.
Options
:preloads
- a list of functions to execute prior to command execution. Preload results will be accessible in the command module'smain/2
callback asenv.pre.*
. – See the Dispatcher Preloads section for preload specification format.
Example
defmodule Program do
use Tux.Dispatcher
cmd "ping", Ping
cmd "cat", Cat, preloads: [:auth, :log]
defmodule Ping do
def main(args), do: ...
end
defmodule Cat do
def main(args), do: ...
end
end
Do note that dispatchers can be infinitely nested. For example, this is valid:
defmodule Program.Design do
use Tux.Dispatcher
cmd "intro", Program.Design.Intro
cmd "advanced", Program.Design.Advanced
end
defmodule Program.Engineering do
use Tux.Dispatcher
cmd "intro", Program.Engineering.Intro
cmd "advanced", Program.Engineering.Advanced
end
defmodule Program do
use Tux.Dispatcher
cmd "design", Program.Design
cmd "engineering", Program.Engineering
end
Command Names
In tux you can define command names using:
- a single keyword, e.g.
ping
- multiple keywords (the command will be triggered by any of the given keywords),
e.g.
~w(ping p)
- a prefix (the command triggered by any string sequence which begins with the
given prefix)
~p(pi)
1. Command Names using Single Keywords
As illustrated above a command can be defined by an exact word:
cmd "foo", FooCommandModule
Invocation:
$ myprog foo
2. Command Names using Multiple Keywords
You can also register multiple names for a command at once, useful when you need multiple names (long + short) for a command.
cmd ~w(foo f), FooCommandModule
Now you can invoke both the foo
and f
commands, which point
to the same command module:
$ myprog foo
$ myprog f
3. Command Names using a Prefix Keyword
In addition to creating commands identified by keywords, tux allows you to define commands using prefixes, meaning a command module will be invoked in response to any given string sequence which begins with the prefix:
cmd ~p"fo", FooCommandModule
The command above will be triggered by any sequence of characters that
begin with foo...
. For example, these are all equivalent command invocations:
$ myprog fo
$ myprog foo
$ myprog foa
$ myprog foab
$ myprog foabc
Do note that you can extract the full keyword which triggered the command
from the env.cmd
in your command module's main
function.
Return a new dispatcher struct, and override its default values
with those given in the args
parameter:
iex> Tux.Dispatcher.new(rescue: true, colors: false)
%Tux.Dispatcher{colors: false, device: :stdio, exitwith: :halt,
newlines: true, preloads: [], rescue: true}
Register a series of commands with common preloads, thus you can replace the following block:
cmd "hello", HelloCmd, preload: [:auth, :log]
cmd "bye", ByeCmd, preload: [:auth, :log]
with a more simplified form:
pre [:auth, :log] do
cmd "hello", HelloCmd
cmd "bye", ByeCmd
end
A sigil which can be used to register a command module with a given prefix instead of a complete name.
This is useful when you want to reduce typing and trigger a given command by a certain prefix and not a full name.
Example
defmodule Program do
use Tux.Dispatcher
cmd ~p".", LetterCmd
end
The complete typed command will then be available in theTux.Env
given
to the command module:
defmodule LetterCmd do
use Tux.Command
@impl true
def main(env, _args) do
case String.split(env.cmd, ".", parts: 2) do
["", "a"] -> {:ok, "You typed A"}
["", "b"] -> {:ok, "You typed B"}
_ -> {:error, "Invalid letter"}
end
end
end
$ program .a --some-flag
You typed A
$ program .b --some-flag
You typed B