The clic library

mavryk-clic is an OCaml combinator library for writing self-documenting command-line argument parsers. Clic is similar to cmdliner, but (unlike cmdliner) mavryk-clic allows to define a domain-specific language as a subset of a natural language, by mixing keyword and positional arguments. For example, in mavkit-client, commands look like this, thanks to mavryk-clic:

mavkit-client list understood protocols
mavkit-client compute chain id from block hash <hash>
mavkit-client originate contract <contract_alias> transferring <initial_balance> from <originator> running <script>

clic is used for most of the binaries distributed with Mavkit, such as mavkit-client and mavkit-codec. A notable exception is mavkit-node which uses cmdliner.

In this tutorial, we will give a gentle introduction to clic by demonstrating how to implement a wallet command inspired by those of mavkit-client. Impatient readers will find the full example in this file.

Wallet example

Command-line parsing in clic is centered around commands. A command roughly corresponds to one action of the command-line application. For instance, mavkit-client get balance and mavkit-client run script <script> are two different commands. To demonstrate the use of clic, we will add the following command to a dummy mavkit-client: list known contracts. As the name indicates, this command outputs the contracts known to the wallet.

Commands execute in a user-supplied context. Typically, the context serves as an abstraction barrier between the command and its execution environment. This mechanism allows commands to be defined once, but reused in different contexts. An example where this useful is for swapping out the normal context for a dry-run context, allowing users to simulate the effect of commands before running them for real.

Note that using contexts is not required: it can be side-stepped by defining commands that execute over a context of type unit.

In the running example, we define a context whose job is to contain the actual implementation of the commands.

We first declare CONTEXT as the signature of modules that contain a list_known_contracts function. As we add more commands to the example, we will also extend the context. For convenience, we add a type alias context for first-class CONTEXT modules.

module type CONTEXT = sig
  val list_known_contracts : unit -> unit
end

type context = (module CONTEXT)

We define a Dummy_context that satisfies the CONTEXT signature, with a place-holder function simply printing what the actual function would do.

module Dummy_context : CONTEXT = struct
  let list_known_contracts () =
    Format.printf "<Print the list of known contracts>\n"
end

Commands are defined through the Mavryk_clic.command function. It has the following signature:

Mavryk_clic.command :
  ?group:Mavryk_clic.group ->
  desc:string ->
  ('b, 'ctx) Mavryk_clic.options ->
  ('a, 'ctx) Mavryk_clic.params ->
  ('b -> 'a) -> 'ctx Mavryk_clic.command

First, commands have a group and description that are used to generate documentation. Groups are used to organize commands of related functionality. This is convenient for applications such as mavkit-client that defines a large number of commands which are grouped on themes such as querying, testing, and address management. You can see the documentation online.

In our example, using groups is not required, but we add a group to demonstrate the feature. A group is just a name and a description of the commands in that group.

let wallet_group =
  {Mavryk_clic.name = "wallet_group"; title = "Wallet-related commands"}

The third argument to Mavryk_clic.command specifies the set of options that commands take, which modulate its behavior (think --verbose or --output json). The value of the options will be collected as a value of type 'b.

The command is specified through a sequence of params, given as the fourth argument to Mavryk_clic.command. Params can be prefixes: fixed strings that must be given when calling the command. Above, we mentioned the get balance command of mavkit-client. The sequence get balance is an example of such a prefix. A param can also define a hole to be filled by the user on the command line. An example is given by mavkit-client get balance for <contract>. Here, the command consists of a sequence of prefixes get balance for followed by the hole <contract>, filled by the user on the command-line. No matter how the params specification is constructed, it is terminated by the combinator Mavryk_clic.stop. The params specification will construct a function type 'a, which together with the type 'b from the options is used to construct the signature 'b -> 'a that the command handler must adhere to.

Note also that by construction, the type 'a will always be of the form ... -> 'ctx -> unit tzresult Lwt.t, so that commands always receive a context and must return unit in the tzresult Lwt.t monad.

The fifth argument Mavryk_clic.command is the command handler. This function implements the actual command. It is passed any supplied command-line options (as a value of type 'b) and the contents of any holes in the params (which are, respectively, types of the arguments of the function type 'a).

An example will be helpful to illustrate the signature of the command handler. A command that takes no options will have 'b = unit. If, furthermore, its params have no holes, then 'a = context -> unit tzresult Lwt.t. The command is thus a function

unit -> context -> unit tzresult Lwt.t

In other words, taking no arguments except for () and the context, and returning unit wrapped in the tzresult and Lwt.t monads.

As a second illustration of the signature of the command handler, consider a command get balance for <contract>, where <contract> is of type Contract.t. Additionally, the command should consume a --output <format> option (where <format> is a string such as "json", "csv", etc.). In this case, 'b = string option, 'a = Contract.t -> context -> unit tzresult Lwt.t, and the command handler a function with the signature:

string option -> Contract.t -> context -> unit tzresult Lwt.t

We now have enough meat on our bones to define the list known contracts command.

module List_known_contracts = struct
  let options = Mavryk_clic.no_options

  let params = Mavryk_clic.(prefixes ["list"; "known"; "contracts"] stop)

  let list_known_contracts_handler :
      unit -> context -> unit Mavryk_error_monad.Error_monad.tzresult Lwt.t =
   fun () ctxt ->
    let module C = (val ctxt) in
    C.list_known_contracts () ;
    Lwt_result_syntax.return_unit

  let command =
    Mavryk_clic.command
      ~group:wallet_group
      ~desc:"Prints the list of known contracts"
      options
      params
      list_known_contracts_handler
end

let commands = [List_known_contracts.command]

We wrap the command and its related definitions in a module List_known_contracts. We specify that the command should have no options through Mavryk_clic.no_options. We specify that the params is a list of prefixes without holes. We then define the command handler list_known_contracts_handler. As the command has no options and its params no holes, the signature of the handler becomes:

unit -> context -> unit tzresult Lwt.t

This command handler does no more than unwrap and call the appropriate function of the context. Finally we add the command to the full list of commands commands that the application will provide.

Having thus defined the commands, we now define the entrypoint of our application:

let () =
  (* 1. Setup formatter with color *)
  ignore
    Mavryk_clic.(
      setup_formatter
        Format.std_formatter
        (if Unix.isatty Unix.stdout then Ansi else Plain)
        Short) ;
  (* 2. Setup context and dispatch commands *)
  let ctxt = (module Dummy_context : CONTEXT) in
  let result =
    Lwt_main.run
      (Mavryk_clic.dispatch commands ctxt (Array.to_list Sys.argv |> List.tl))
  in
  (* 3. Handle results *)
  match result with
  | Ok () -> ()
  | Error [Mavryk_clic.Help _command] ->
      Format.printf "<display help>\n" ;
      exit 0
  | Error _ ->
      Format.printf "Could not parse command-line arguments.\n" ;
      exit 1

It consists of three sections. We first setup a formatter that depending on whether the command is executed in a tty (as opposed to e.g. being piped to a file) enables color in the output. Then, we pack our context in a first-class module, that we pass to the Mavryk_clic.dispatch. This function takes the full list of commands, as defined by commands, the context, and the list of raw command-line arguments passed through the application. The list of command-line arguments should not contain the first element (the name of the program itself), so this is why the List.tl function is used. The dispatch function will parse the arguments, and call the appropriate command handler if a valid command was given. If this is the case, Ok () is returned. If no arguments have been passed, or if --help is given, then Error [Mavryk_clic.Help _command] is returned. In this case the application should print the appropriate usage instruction. If some other unrecognized arguments are given we give a placeholder error message, which we’ll replace with something more helpful below.

We use dune to compile the example, with the following dune file:

(executable
 (name clic_example)
 (libraries
  mavkit-libs.clic
  lwt.unix))

The dependencies of the example are mavryk-clic and lwt.unix which can be installed through opam install mavryk-clic lwt. Let’s try it out:

$ dune exec ./clic_example.exe -- list known contracts
<Print the list of known contracts>

which is as expected. Giving no arguments, or when passed the --help flags, our placeholder help message is output:

$ dune exec ./clic_example.exe -- --help
<display help>
$ dune exec ./clic_example.exe --
<display help>

Similarly, if we attempt to call an unrecognized command:

$ dune exec ./clic_example.exe -- foobar
Could not parse command-line arguments.

Conclusion

This example demonstrates how to define a simple clic application with one simple command. This is far from a complete demonstration of clic. clic also includes facilities for generating interactive, searchable documentation, with both command-line and HTML outputs. clic also gives facilities for implementing shell auto-completion. For more information, refer to clic's API documentation .