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 is false)
  • :device - the IO.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 is true)
  • :colors - a boolean flag to enable/disable colored outputs (default is true)
  • :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's Tux.Command.main/2 callback as env.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 the System 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) and cmd/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.Envs .pre key:

Preload FormatExamplePreload Result
fun:configenv.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.

t()

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

cmd_module()

@type cmd_module() :: module()

Command module associated with a command name

cmd_name()

@type cmd_name() :: String.t()

Command name used for command module registration

cmd_opts()

@type cmd_opts() :: [{:preloads, preloads()}]

Command registration options.

device()

@type device() :: IO.device()

An output IO.device/0 such as :stdio or another process.

preloads()

@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

t()

@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

help()

@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.

main(args)

@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

__after_compile__(macro_env, bytecode)

Perform sanity checks for a dispatcher's registered command names and associated command modules.

__before_compile__(env)

(macro)

Inject the utility functions whose role is to retrieve the dispatcher options at runtime (used internally).

__using__(opts \\ [])

(macro)

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

cmd(name, module, opts \\ [])

(macro)
@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's main/2 callback as env.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.

new(args)

@spec new(Keyword.t()) :: t()

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}

pre(preloads, list)

(macro)
@spec pre(preloads(), [{:do, Macro.t()}]) :: Macro.t()

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

sigil_p(cmd, list)

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