SkillAgentSearch skills...

Clique

CLI Framework for Erlang

Install / Use

/learn @basho/Clique
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Erlang CI Actions Status

Introduction

Clique is an opinionated framework for building command line interfaces in Erlang. It provides users with an interface that gives them enough power to build complex CLIs, but enough constraint to make them appear consistent.

Why Clique ?

When building a CLI for an Erlang application users frequently run into the following problems:

  • Output is inconsistent across commands and often implemented differently for each command with little re-use.
  • Output is frequently hard to read for humans, hard to parse for machines, or both.
  • Adding a new command to the system often results in glue code and extra work rather than just writing a function that gathers information or performs a given user action.
  • Setting and showing configuration often only works on a single node.
  • Configuration changes with runtime side effects are often difficult to implement.

Clique provides a standard way of implementing status, command, usage and configuration functionality while minimizing the amount of code needed to be written by users of the library.

Clique provides the application developer with the following capabilities:

  • Implement callbacks that handle a given cli command such as riak-admin handoff enable outbound
  • Register usage points to show the correct usage in the command hierarchy when an incomplete command is run or the user issues the --help flag.
  • Set, show and describe cuttlefish configuration across one or all nodes: i.e. riak-admin set anti-entropy=on --all
  • Return a standard status format that allows output of a variety of content types: human-readable, csv, html, etc... (Note that currently only human-readable, CSV, and JSON output formats are implemented)

Why Not Clique ?

  • You aren't writing a CLI
  • You don't want or need to use cuttlefish for configuration
  • You only have a few command permutations and the dependency would be overkill
  • You already wrote your own cli tool
  • You are a masochist
  • You dislike your users

CLI usage

Clique provides a consistent and flexible interface to the end user of your application. In the interest of clarity, a few examples will be given to illustrate common usage.

# Show the configuration for 2 config variables. Multiple values can be
# shown by using spaces between them. The --all flag means: give me the values
# on all nodes in the cluster.

$ riak-admin show transfer_limit leveldb.limited_developer_mem --all
+--------------+--------------+-----------------------------+
|     Node     |transfer_limit|leveldb.limited_developer_mem|
+--------------+--------------+-----------------------------+
|dev1@127.0.0.1|      4       |            true             |
|dev2@127.0.0.1|      6       |            true             |
+--------------+--------------+-----------------------------+

# Set the transfer_limit config on dev2
$ riak-admin set transfer_limit=6 --node=dev2@127.0.0.1
Set transfer limit for 'dev2@127.0.0.1' to 6

# Describe 1 or more configuration variables
# Note that the descriptions are the doc comments in the cuttlefish schema
$ riak-admin describe transfer_limit storage_backend
transfer_limit:
  Number of concurrent node-to-node transfers allowed.

storage_backend:
  Specifies the storage engine used for Riak's key-value data
  and secondary indexes (if supported).

# Run an aribtrary, user defined command
$ riak-admin handoff enable outbound
Handoff setting successfully updated

# Show usage information when a command is incompletely specified
$ riak-admin handoff enable
Usage: riak-admin handoff <enable | disable> <inbound | outbound | both> [[--node | -n] <Node>] [--all]

  Enable or disable handoffs on the specified node(s).
  If handoffs are disabled in a direction, any currently
  running handoffs in that direction will be terminated.

Options
  -n <Node>, --node <Node>
     Modify the setting on the specified node (default: local node only)
  -a, --all
     Modify the setting on every node in the cluster

Erlang API

Clique handles all parsing, validation, and type conversion of input data in a manner similar to getopt. Clique also handles all formatting and output of status. The user code registers specifications, usage documentation and callbacks in order to plug into Clique. When a command is run, the code is appropriately dispatched via the registry. Each registered callback returns a status type that allows clique to format the output in a standardized way.

Load Schemas

Clique requires applications to load their cuttlefish schemas prior to calling register_config/1 or register_config_whitelist/1. Below shows how riak_core loads schemas in a flexible manner allowing for release or test usage.

load_schema() ->
    case application:get_env(riak_core, schema_dirs) of
        {ok, Directories} ->
            ok = clique_config:load_schema(Directories);
        _ ->
            ok = clique_config:load_schema([code:lib_dir()])
    end.

register/1

Register is a convenience function that gets called by an app with a list of modules that implement the clique_handler behaviour. This behaviour implements a single callback: register_cli/0. This callback is meant to wrap the other registration functions so that each individual command or logical set of commands can live in their own module and register themselves appropriately.

%% Register the handler modules
-module(riak_core_cli_registry).

clique:register([riak_core_cluster_status_handler]).
-module(riak_core_cluster_status_handler]).
-export([register_cli/0]).

-behaviour(clique_handler).

register_cli() ->
    clique:register_config(...),
    clique:register_command(...).

register_node_finder/1

Configuration can be set and shown across nodes. In order to contact the appropriate nodes, the application needs to tell clique how to determine that. riak_core would do this in the following manner:

F = fun() ->
        {ok, MyRing} = riak_core_ring_manager:get_my_ring(),
        riak_core_ring:all_members(MyRing)
    end,
clique:register_node_finder(F).

Note that this function only needs to be called once per beam. The callback itself is stored in an ets table, and calling clique:register_node_finder/1 again will overwrite it with a new function.

register_config/2

Showing, setting and describing configuration variables is handled automatically via integration with cuttlefish. The application environment variables can be set across nodes using the installed cuttlefish schemas. In some instances however, a configuration change requires doing something else to the cluster besides just setting variables. For instance, when reducing the transfer_limit, we want to shutdown any extra handoff processes so we don't exceed the new limit.

Configuration specific behaviour can be managed by registering a callback to fire when a given configuration variable is set on the cli. The callback runs after the corresponding environment variables are set. The callback function is a 2-arity function that gets called with the original key (as a list of strings()), and the untranslated value to set (as a string()).

On the command-line, the flags can be either '--all' to run on all nodes, or --node N to run on node N instead of the local node. If no flags are given, the config change will take place on the local node (where the cli command was run) only. These flags are not visible to the callback function; rather, the callback function will be called on whichever nodes the config change is being made.

Unlike command callbacks, config callbacks need only return a short iolist describing any immediate results of the config change that may have taken place. This allows results from --all to be compiled into a table, and lets results from other invocations be displayed via simple status messages.

-spec set_transfer_limit(Key :: [string()], Val :: string()) -> Result :: string().
...

Key = ["transfer_limit"],
Callback = fun set_transfer_limit/2,
clique:register_config(Key, Callback).

register_formatter/2

By default, the clique "show" command displays the underlying config value, as stored in the corresponding application env variable (the one exception being values of type "flag", which are automatically displayed by clique as the user-facing flag value defined in the cuttlefish schema). In many cases this is fine, but sometimes there may be translations defined in the cuttlefish schema which make it desirable to show config values in a different format than the one used by the underlying Erlang code.

To show a specific config value using a different format than the underlying raw application config, you can register a config formatter against that value's config key:

F = fun(Val) ->
        case Val of
            riak_kv_bitcask_backend -> bitcask;
            riak_kv_eleveldb_backend -> leveldb;
            riak_kv_memory_backend -> memory;
            riak_kv_multi_backend -> multi
        end
    end,
clique:register_formatter(["storage_backend"], F).

register_config_whitelist/1

A lot of configuration variables are not intended to be set at runtime. In order to prevent the user from changing them and anticipating the system to use the new values, we don't allow setting of any variable by default. Each configuration variable that is settable must be added to a whitelist.

%% Fail Fast if we pass in a value that is not the name of a configuration variable
ok = register_config_whitelist(["transfer_limit", "handoff.outbound", "handoff.inbound"]).

Note that in the future we hope to remove the need for this function by adding support f

View on GitHub
GitHub Stars152
CategoryDevelopment
Updated1mo ago
Forks49

Languages

Erlang

Security Score

95/100

Audited on Feb 5, 2026

No findings