Getting Started
In this tutorial, we'll create a small calculator in OCaml, export it as a command-line tool, and demonstrate the use of the cmdlang
library along the way.
We'll create a CLI that can perform operations like:
$ ./my-calculator --op=add 1 2.5
3.5
(a classic of the genre!)
Installation
cmdlang
is currently under development and is not yet published to opam. Instead, it is available through a custom opam repository, which must be added to your opam switch.
For example, if you are using a local opam switch, follow these steps:
$ cd /path/to/your/local/switch
$ eval $(opam env)
$ opam repo add mbarbin https://github.com/mbarbin/opam-repository.git
Once this is set up, you can install cmdlang
as usual with opam:
$ opam install cmdlang
We will update this section as the project progresses.
Packaging
To keep the dependencies separate, our command will be implemented in its own library, with the runtime instantiation in a separate folder.
Lib
First, create a lib/
directory and a lib/dune
file for our library, listing cmdlang
as a dependency.
Cmdlang
is designed to expose a single module named Command
. To bind Command
directly to Cmdlang.Command
in our scope, we use the -open
flag in the dune setup. You can use different styles if you prefer.
(library
(name getting_started)
(flags :standard -open Cmdlang)
(libraries cmdlang))
Next, create an empty command-line skeleton that we will complete incrementally.
let cmd =
Command.make
~summary:"A simple calculator"
(let open Command.Std in
let+ () = Arg.return () in
())
;;
Finally, export this command via the mli:
val cmd : unit Command.t
Bin
Create a bin/
directory and a bin/dune
file to set up the build rules for our executable.
As you'll learn, cmdlang
doesn't come with its own command runner. Instead, it is designed to use existing runners from the community. For this tutorial, we'll use cmdliner
as our command runner.
Install step
$ opam install cmdlang-to-cmdliner cmdliner
Setup bin/dune
(executable
(name main)
(libraries cmdlang-to-cmdliner cmdliner getting_started))
An invocation of cmdliner
for a cmdlang
command may look like this:
let () =
Cmdliner.Cmd.eval
(Cmdlang_to_cmdliner.Translate.command
Getting_started.cmd
~name:"my-calculator"
~version:"%%VERSION%%")
|> Stdlib.exit
;;
You'll notice how we've:
- Used a cmdlang translator library to obtain a cmdliner command.
- Used the cmdliner library to evaluate (run) our command.
Implementation
With our build rules set, it's time to start coding!
Adding Operations
We'll add an operator module that supports some binary operations:
module Operator = struct
type t =
| Add
| Mul
let all = [ Add; Mul ]
let to_string = function
| Add -> "add"
| Mul -> "mul"
;;
let eval op a b =
match op with
| Add -> a +. b
| Mul -> a *. b
;;
end
Next, we'll start exposing the functionality via the command-line interface.
Adding Arguments
Let's revisit our empty skeleton:
let cmd =
Command.make
~doc:"A simple calculator"
(let open Command.Std in
let+ () = Arg.return () in
())
;;
Argument parsing is done using a style called "applicative syntax". This means you bind the arguments you wish to parse with the let+
and and+
keywords, and then you can use these arguments in the body of your command.
Let's add three arguments:
- One for the operation we'd like to compute.
- Two additional arguments to get the operands.
To do this, we need to insert lines in the let+
section and fill the body of the command with actual code:
let cmd =
Command.make
~doc:"A simple calculator"
(let open Command.Std in
let+ _ (* ADD NEW *) = _
and+ _ (* ARGUMENTS *) = _
and+ _ (* HERE *) = _ in
in
(* AND THE COMMAND BODY HERE *)
())
;;
The library is designed with ocaml-lsp completion in mind, so remember:
- Argument builders are in a module called
Command.Std.Arg
- Argument parameter builders are in a module called
Command.Std.Param
(to parse the"add"
part of the complete"--op=add"
argument)
Since we've opened Command.Std
in this section, the modules we need are Arg
and Param
.
let cmd =
Command.make
~doc:"A simple calculator"
(let open Command.Std in
let+ op = Arg.(* <== After typing "Arg." you can now enjoy
user-friendly ocaml-lsp completion. *)
...
We've found a suitable helper in the Arg
API to parse the mandatory --op
named argument. This argument expects a parameter, which is the value supplied along with the argument, such as in --op=add
.
Similarly, remember that parameter parsing helpers are in the Param
module:
let cmd =
Command.make
~doc:"A simple calculator"
(let open Command.Std in
let+ op =
Arg.named_req
[ "op" ]
~doc:"operation to perform"
(Param. (* <== After typing "Param." you can now enjoy
user-friendly ocaml-lsp completion.*)
...
Completing the Program
At this point, we have everything we need to complete our calculator.
let cmd =
Command.make
~summary:"A simple calculator"
(let open Command.Std in
let+ op =
Arg.named
[ "op" ]
(Param.enumerated (module Operator))
~docv:"OP"
~doc:"operation to perform"
and+ a = Arg.pos ~pos:0 Param.float ~docv:"a" ~doc:"first operand"
and+ b = Arg.pos ~pos:1 Param.float ~docv:"b" ~doc:"second operand"
and+ verbose = Arg.flag [ "verbose" ] ~doc:"print debug information" in
if verbose then Printf.printf "op: %s, a: %f, b: %f\n" (Operator.to_string op) a b;
print_endline (Operator.eval op a b |> string_of_float))
;;
Running the Command
That's it! We're ready to enjoy the full features of our command line tool.
$ ./my-calculator --op=mul 3 7.2 --verbose
op: mul, a: 3.000000, b: 7.200000
21.6
Our CLI includes a generated help page:
Output of "./my-calculator --help"
$ ./my-calculator --help=plain
NAME
my-calculator - A simple calculator
SYNOPSIS
my-calculator [--op=OP] [--verbose] [OPTION]… a b
ARGUMENTS
a (required)
first operand.
b (required)
second operand.
OPTIONS
--op=OP (required)
operation to perform. OP must be either 'add' or 'mul'.
--verbose
print debug information.
COMMON OPTIONS
--help[=FMT] (default=auto)
Show this help in format FMT. The value FMT must be one of auto,
pager, groff or plain. With auto, the format is pager or plain
whenever the TERM env var is dumb or undefined.
--version
Show version information.
EXIT STATUS
my-calculator exits with:
0 on success.
123 on indiscriminate errors reported on standard error.
124 on command line parsing errors.
125 on unexpected internal errors (bugs).
Additionally, we don't need to worry about handling invalid usages, this is done for us by cmdliner
:
$ ./my-calculator --op=not-found 1 2.5
my-calculator: option '--op': invalid value 'not-found', expected either
'add' or 'mul'
Usage: my-calculator [--op=OP] [--verbose] [OPTION]… a b
Try 'my-calculator --help' for more information.
[124]
$ ./my-calculator --op=add true 2.5
my-calculator: a argument: invalid value 'true', expected a floating point
number
Usage: my-calculator [--op=OP] [--verbose] [OPTION]… a b
Try 'my-calculator --help' for more information.
[124]
Conclusion
In this tutorial, we've created a command-line interface and exposed its entry point from a library. Then, we've used the cmdlang-to-cmdliner
translation step and set up dune build rules to create an executable that runs this command with cmdliner
as a backend.
While we've covered the basics, there are additional features you might want to explore, such as generating complete man pages and setting up auto-completion. We'll cover these advanced topics in other parts of the documentation.
Happy command parsing!