package clim
Install
    
    dune-project
 Dependency
Authors
Maintainers
Sources
md5=ae7eafe6c0eeac8a3b5288aa809ffd48
    
    
  sha512=5e5636385f455eb71934b7aa810089d1748fbacd463451e9032a7fb1f8a86a4eaf9ec327119c663f0230ee38ef4983a01c63e138f4172459c92b41f39ad1960a
    
    
  doc/clim.html
Clim
Clim stands for Command Line Interface Maker and helps making clean and nice command line interfaces for your binaries.
There are various ways of building CLI in OCaml. The most obvious one is using the Arg module from the standard library. While this modules does the job, the result is quite basic and not as nice as one would expect from modern binaries.
There are other libraries that help but the most effective in this job is probably Cmdliner which provides a rich and nice API for builing CLIs. However, Cmdliner's API isn't very user friendly for it needs to define several intermediate values which introduce noise in the binary source code. Furthermore, the library doesn't suit well for CLI composition which happens quite often in my own programming experience.
The Clim library provides some layout above Cmdliner to simplify the latter usage. With it, there are mainly two ways of defining CLI: an incremental one and a object-oriented one.
Incremental CLI
The incremental way allows the user to define its CLI along its code with few functions inherited from Cmdliner. It uses three concepts : the configuration, the argument and the command.
Configuration
A CLI is basically stored in a configuration which may be constructed very simply by:
open Clim
let cfg = create ()With Clim binaries are constructed from such a configuration.
Arguments
Now arguments are specified by using functions deriving directly from Cmdliner but in a more concise way to avoid intermediate object construction.
For example, the following code:
  let foo = register cfg @@ value @@ opt
    ~doc:"Foo parameter"
    string
    "foo"
    ["f"; "foo"]will add a optional string parameter foo with a default value "foo" which will be customizable through the CLI with options -f or --foo. The main difference with Cmdliner is the register function which adds the parameter to cfg and returns a function whose type is unit -> 'a where 'a depends on the given type specification. Here, foo is a unit -> string function and is a getter to the foo parameter value.
Any function using foo underlying value can use it by calling it:
  let main () = Format.printf "foo = %s@." (foo ())Commands
When all arguments are defined, the final CLI can be defined using the command function:
   let foo_cmd = command ~cfg ~doc:"Foo printing." mainwhich accepts several optional arguments customizing the resulting man page.
To execute this command, you must use the run function:
  let () = run foo_cmdand that's all. The resulting binary will respond to --help or other CLI options automatically and give a nice looking man page.
Inheritance
With the configuration system, it's possible (and encouraged) to share configuration between binaries in order to simplify the code but also give related binaries a sound CLI.
When releasing a binary package, just expose its configuration and command so that related binaries designed to extend the previous one can inherit them with:
   (* extending the previous configuration *)
      let ext_cfg = from cfgThat way, we can add the bar option:
      let bar = register ext_cfg @@ value @@ opt
          ~doc:"Bar parameter"
          int
          0
          ["b"; "bar"]and even change the CLI behavior:
  let bar_cmd = {
    foo_cmd with
    cmd = (fun () ->
        foo_cmd.cmd ();
        let b = bar () in
        Format.printf "square(bar) = %i@." (b * b));
    doc = foo_cmd ^ " Prints also the square of bar parameter.";
  }
  let () = run bar_cmdAs you can see, running the binary will print the foo value but also the square of bar.
Lwt
Clim is fully compatible with Lwt. Simply give a Lwt thread to command to produce a 'a Lwt.t command value. Running this command will produce a 'a Lwt.t value that should be passed to Lwt_main.run.
Object-oriented CLI
While the incremental CLI definition works well for new binaries, it suits not well for existing ones as the migration from any old system to this one will likely need a complete rewriting of the CLI definition. For example, a binary wrote with Arg isn't easily converted to the incremental CLI definition as the former is a centralized definition design which likely appears in the final binary source file. Furthermore, it uses some kind of continuation style while the incremental definition simply gives accessors.
The object-oriented CLI definition is somehow a merge between the incremental CLI definition, the centralized design with continuations and a Python argparse like usage. The resulting API benefits from the three systems and is very recomended for your CLI definitions.
Definition
To define an object-oriented CLI : simply inherit from Clim.cli and define the entrypoint method (equivalent to the command definition) and add arguments in the initalizer using the Clim.cli.arg or Clim.cli.set methods.
class foo = object(self)
  inherit [_] cli
  val mutable who = "world"
  method entrypoint () = Format.printf "Hello %s!@." who
  initializer
    self#add (value @@ opt string who ~doc:"Someone" ["w"; "who"]) (fun w -> who <- w)
endThe command is fully customizable by defining or overloading the appropriate methods. If you write a generic application which will likely be extended, use a class or simply an direct OCaml object else.
Overloading
Obviously, inheriting will do the job:
class bar = object(self)
  inherit foo as super
  val mutable from = "Paris"
  method! entrypoint () = super#entrypoint (); Format.printf "(from %s)@." from
  initializer
    self#add (value @@ opt string who ~doc:"Where" ["f"; "from"]) (fun f -> from <- f)
endExecution
Simply call the Clim.cli.run method to run the underlying command:
  let _ = bar#runAs for the incremental API, Lwt is fully supported by giving the result to Lwt_main.run