Packages

Create elegant commmand line interfaces with ease

Current section

Files

Jump to
tux lib tux.ex
Raw

lib/tux.ex

defmodule Tux do
@moduledoc """
*Terminal User Experience*
Tux is a **modular**, **dependency-free** Elixir library
designed for the speedy creation of elegant command line interfaces
which subscribe to the philosophy *"One module per command"*.
Its modular structure ensures its core functionalities can be overwritten
and composed to fulfill custom needs, and having no dependencies means
it can achieve a higher level of security by minimizing trusted parties.
## Features
Developing with the tux library involves typically the use of
the following featured modules:
- `Tux.Dispatcher` - command registration and dispatch macros
- `Tux.Command` - command module behaviour
- `Tux.Result` - shapes for the command results
- `Tux.Prompt` - basic user prompting and answer parsing
- `Tux.Error` - user friendly error messages
- `Tux.Config` - reading and writing of simple configuration files
- `Tux.Help` - command help message construction
- `Tux.Case` - command testing macros
## Installation
Add `tux` to your list of dependencies in `mix.exs` and optionally update
your `.formatter.exs`:
```
# mix.exs
{:tux, "~> 0.3.0"}
# .formatter.exs
import_deps: [:tux]
```
## Examples
Here's a very short example to illustrate the mechanics of the library:
1. A **command module** implements the command:
```elixir
defmodule Demo.HelloCmd do
use Tux.Command
@impl true
def about(), do: "Greet current user"
@impl true
def main(env, args), do: {:ok, "Hello \#{env.pre.user}!"}
end
```
2. A **dispatcher module** associates command names with command modules,
or groups them as sub-commands:
```elixir
defmodule Demo do
use Tux.Dispatcher
# Command registration (with options)
cmd "hello", Demo.HelloCmd, preloads: [:user]
# A preload function can run prior to command execution
def user(env), do: System.fetch_env!("USER")
end
```
3. **Build** your elixir app as an escript executable. Note that you need to
update your `mix.exs` to reflect your escript's main module.
See [mix escript.build](https://hexdocs.pm/mix/main/Mix.Tasks.Escript.Build.html)
for more details.
```
# mix.exs
def project do
[
escript: [main_module: Demo]
...
]
end
```
Generate the escript executable:
```
$ mix escript.build
Generated escript demo
$ ./demo hello
Hello tuxuser!
```
4. **Extend** your app with new commands simply by creating new
command modules, then register them with the dispatcher:
```elixir
defmodule Demo.AddCmd do
use Tux.Command
@impl true
def main(env, [x, y]) do
{a, ""} = Integer.parse(x)
{b, ""} = Integer.parse(x)
{:ok, "\#{a} + \#{b} = \#{a+b}"}
end
end
```
Register the *Demo.AddCmd* module under the *"add"* name:
```elixir
defmodule Demo do
...
cmd "add", Demo.AddCmd
cmd "hello", Demo.HelloCmd, preloads: [:user]
end
```
Rebuild the escript then run:
```sh
$ mix escript.build
$ ./demo add 1 2
1 + 2 = 3
```
## Overview
Tux was designed to be modular and dependency-free, all the while leveraging
the powerful Elixir macro system to enable the development of command line
interfaces with ease.
### 0. Concepts
Here's a summary of some the concepts used throughout the tux universe:
* *Command* – some functionality implemented by an executable.
* *Command Name* – some keyword which identifies a command.
* *Command Module* – a specialized module which implements a given command.
* *Command Dispatcher* – a specialized module responsible for grouping commands
and for delegating program execution to the appropriate command module
in response to command invocations.
* *Command Preloads* – optional functions dispatchers can execute prior to
executing commands, and whose results are made available to the command modules
as part of their environment.
### 1. Commands
Commands expose the functionality of your program/escript to the end user.
They are invoked using keywords (command names), as part of your
program/escript invocation.
By analogy, when you make use of the `mix` program and type `mix compile`,
you invoke the *compile* command of the *mix* program.
To implement commands using tux, you create *command modules* which
contain the business logic, then group them together under one or more
dispatchers.
Here's a the most minimal example of a command module created with tux:
```
defmodule PingCmd do
use Tux.Command
@impl true
def main(env, args) do
{:ok, "pong"}
end
end
```
Command modules can implement other callbacks besides the `c:Tux.Command.main/2`.
See the `Tux.Command` behaviour for more details.
Alright, so now that we have just implemented a command, how can we connect it
to the rest of the escript? Enter dispatchers:
### 2. Dispatchers
```
Dispatcher Module
├─── Command Module
├─── Command Module
└─── Dispatcher Module
├─── Command Module
└─── Command Module
```
Dispatchers are modules which contain the mappings `command names -> command modules`
and are responsible for delegating execution to the appropriate command module
in response to a command invocation.
Well, they actually do a little more than that, among which collecting the results
from command preloads, setting up the command context, recovering
from exceptions and more.
A dispatcher's functionality is implemented by the `Tux.Dispatcher` module, and it can
be injected into your own dispatcher modules via the `__using__` macro.
Here's an example of a dispatcher which associates the *ping* command
with the `PingCmd` module we've just implemented:
defmodule MyProgram do
use Tux.Dispatcher
# Register a new command module via a command name
cmd "ping", PingCmd
# Alternatively a command module can be registered under multiple
# names at once (useful for command shortcuts)
cmd ~w"pong p", PongCmd
end
**Customizing Dispatchers** – Please read the section
[Dispatcher Options](Tux.Dispatcher.html#module-dispatcher-options) for how to
customize a dispatcher's behaviour.
### 3. Dispatcher & Command Preloads
Oftentimes, commands need to obtain some data or run some business logic prior
to the actual command execution, so to help with these aspects tux offers *preloads*.
Preloads are functions a dispatcher can execute prior to command execution,
and whose results can be made available to the command module in the `env` struct,
which is the first argument pass to the command module's `c:Tux.Command.main/2`
callback.
For illustrative purposes, let's update our ping command with a preload which
can return the current datetime and make that available in the command output:
Here's the updated dispatcher module and command registration:
defmodule MyProgram do
use Tux.Dispatcher
@doc "A preload function to return the current datetime"
def date(_env), do: DateTime.utc_now()
# Command registered with preload
cmd "ping", PingModule, preloads: [:date]
end
And here's the command module making use of the preload's result:
defmodule PingCmd do
use Tux.Command
@impl true
def main(env, args) do
{:ok, "pong \#{date}"}
end
end
```
$ mix escript.build
$ ./program ping
pong 2024-06-07 20:36:56.687017Z
```
### 4. Results
Command modules should return the following results:
* `:ok`
* `:error`
* `{:ok, String.Chars.t()}`
* `{:error, String.Chars.t() | Tux.Alertable.t()}`
Please sees `String.Chars` and `Tux.Alertable` protocols for more details.
### 5. Errors
To signal a command has failed you can return a `Tux.Result` error tuple containing a
a `Tux.Error` (or a string message) value. In such a case, this value will be converted
and displayed nicely as a `Tux.Alert`.
Now, if the business logic of a command is deeply nested, it might be easier to
simply raise an exception from the depths of your code, with the downside of having
the end user see the entire stacktrace (which might not be an issue for
other developers), but nonetheless it can be unsightly.
With tux, you can rescue from command exceptions and display alerts instead. To this
end, you need to construct the dispatcher module with the `rescue: true` option, and
as a consequence, command exceptions will be shown more elegantly while still
preserving the ability to view the entire stacktrace when the command is invoked
with the `--debug` flag.
**Exit Codes** – A `Tux.Error` also contains an `exitcode` field, which you can
overwrite, and it will be used as the program/escript's exit status.
### 6. Help
Tux includes the `Tux.Help` module to assist you in creating well-structured
command help message.
When a command implements the `help/0` callback, if the said command
is invoked with the `-h` or `--help` flags, it will show something like this:
```sh
ABOUT
scan - scan strings
USAGE
strings scan [OPTS] [ARGS]
OPTIONS
--uppercase, u Uppercase endpoint name
--lowercase, u Lowercase endpoint name
NOTES
Here are some additional notes to include
in the help message.
```
Here's the pipeline used to create the above help message:
defmodule ScanCmd do
use Tux.Command
@impl true
def about(), do: "Manage systems"
@impl true
def main(_, _), do: {:ok, "System managed"}
@impl true
def help() do
Help.new()
|> Help.about("scan", "scan strings")
|> Help.usage(["strings scan [OPTS] [ARGS]"])
|> Help.options([
{"--uppercase, u", "Uppercase target name"},
{"--lowercase, u", "Lowercase target name"}
])
|> Help.section("notes", \"\"\"
Here are some additional notes to include
in the help message.
\"\"\")
|> Help.ok()
end
end
### 7. Internals
For quick reference, here's an overview of the most important modules
of the tux library:
* `Tux.Alert` - module for displaying errors (stylized)
* `Tux.Command` - command module behaviour
* `Tux.Config` - functions for reading and writing text based config files.
* `Tux.Dispatcher` - macros for command creation and dispatch
* `Tux.Env` - a struct to encode preload result and other info
* `Tux.Error` - functions user friendly errors construction.
* `Tux.Exec` - execute a command module
* `Tux.Help` - functions for creating help messages.
* `Tux.Init` - construct env prior to command execution
* `Tux.Prompt` - functions for prompting and type conversion.
* `Tux.Result` - the type returned by commands.
* `Tux.Show` - functions for user-friendly output.
---
**More Examples** – A series of runnable examples illustrating
the usage of the tux library can be found in the
[examples](https://github.com/threatfender/tux/tree/master/examples) folder
of the source repository.
"""
end