At my work, we've been using Core.Command and Async.Command to write the command-line interfaces to our programs. Historically we've used Command.Spec, which is now deprecated in favor of Command.Param. This post is documentation with examples for myself and the team for how to use the new interface for this module.

For this document, I'll be using Core.Command, but everything works the same for Async.Command.

Overview

Skip this if you just want examples.

Command.Param implements an applicative functor, which as far as I can tell is just a monad but with no bind function. This means you basically just have three functions to work with: map, both, and return, and then some helpers.

  • return takes any value and turns it into a Command.Param.t
  • both takes two params and turns them into one param
  • map takes a param and a function that takes the param's value and returns a new value, returning a new Command.Param.t with the return value

Command.Arg_type has ways to create params that accept various types (like strings, ints, anything that can be converted from a string, etc.), and Command.basic expects a unit -> unit function "param".

Commands with no arguments

In the simplest case, where you would use Command.Spec.empty before, you can use Command.Param.return to wrap a function:

open Core

let () =
  Command.basic ~summary:"example command" begin
    Command.Param.return begin fun () ->
      print_endline "running example"
    end
  end
  |> Command.run

The most readable way I've found of writing this uses the @@ operator:

let () =
  Command.basic ~summary:"example command" begin
    Command.Param.return @@ fun () ->
    print_endline "running example"
  end
  |> Command.run

Commands with arguments

For commands with arguments, you use the same functions as in Command.Spec. The only difference is that you combine them with map instead of <*>.

  • Command.Param.flag creates a flag (starting with - or --, can be optional)
  • Command.Param.anon creates an anonymous (positional) required argument.

Core's documentation recommends that you write these using ppx_let, which requires opening Command.Let_syntax. Note that if you're using Async, you may need to re-open Deferred.Let_syntax in the body of your main function.

let () =
  Command.basic ~summary:"a more complicated example" begin
    let open Command.Let_syntax in
    let open Command.Param in
    let%map optional = flag ~doc:"OPTIONAL an optional flag" "-o" no_arg
    and positional = anon ("positional_arg" %: string) in
    fun () ->
      printf "optional arg is %s and positional arg is %s"
        (Bool.to_string optional) positional
  end
  |> Command.run

[%map_open]

The documentation shows example using [%map_open] (also from ppx_let), which looks like this:

let () =
  let open Command.Let_syntax in
  let command =
    Command.basic ~summary:"a more complicated example"
      [%map_open
        let optional = flag ~doc:"OPTIONAL an optional flag" "-o" no_arg
        and positional = anon ("positional_arg" %: string) in
        fun () ->
          printf "optional arg is %s and positional arg is %s"
            (Bool.to_string optional) positional
      ]
  in
  Command.run command

The advantage of this version is that you don't need to open Command.Param (it's automatically open on the right hand side of the = statements).

You do still need to open Command.Let_syntax for this to work, and it only works if you bind the result to a name using let.

Without ppx_let

I don't actually recommending doing this, but you can write this just using both and map. Note that there is no bind, so this becomes harder and harder to write for each additional argument you want to add. all does exist, but is only useful if all of your params are the same type. I assume this is why the documentation recommends using ppx_let.

let () =
  Command.basic ~summary:"a more complicated example" begin
    let open Command.Param in
    both
      (flag ~doc:"OPTIONAL an optional flag" "-o" no_arg)
      (anon ("positional_arg" %: string))
    |> map ~f:(fun (optional, positional) ->
        fun () ->
          printf "optional arg is %s and positional arg is %s"
            (Bool.to_string optional) positional)
  end
  |> Command.run

If you really hate ppx_let and only have up to 3 arguments, you can also use map2 and map3:

let () =
  Command.basic ~summary:"a more complicated example" begin
    let open Command.Param in
    map2
      (flag ~doc:"OPTIONAL an optional flag" "-o" no_arg)
      (anon ("positional_arg" %: string))
      ~f:(fun optional positional ->
          fun () ->
            printf "optional arg is %s and positional arg is %s"
              (Bool.to_string optional) positional)
  end
  |> Command.run

Again, I don't really recommend this, but it might be useful to understand what's happening under the hood.

Backwards compatibility

One last thing to note is that Command.Spec.t is an alias for Command.Param.t, which makes it easier to change your codebase one file at a time. If you have definitions like:

let uuid_param : Uuid.t Command.Spec.t =
  Command.Arg_type.create Uuid.of_string_exn

... you can continue using this for both Spec and Param commands.