Command-line parsing (Real world OCaml)

I am reading the chapter Command-Line Parsing - Real World OCaml, which uses the Command library in Core. After giving the signatures

#show Command.basic ;;
val basic : unit Command.basic_command
#show Command.basic_command ;;
type nonrec 'result basic_command =
    summary:string ->
    ?readme:(unit -> string) ->
    (unit -> 'result) Command.Spec.param -> Command.t

it states “It makes sense that Command.basic wants a parser that returns a function”. But it seems that no example is given that makes use of this. More precisely, we can define a function

let basic2 ~summary ?readme a = 
  Command.basic ~summary ?readme
  (Command.Param.map a ~f:(fun x -> (fun () -> x)))

which has the simpler signature

val basic2 :
  summary:string ->
  ?readme:(unit -> string) -> unit Command.Spec.param -> Command.t

and in all the examples of the chapter, replace Command.basic with basic2. There is probably something I’m not seeing ?

Command.basic distinguishes between exceptions raised while parsing the command line parameters and exceptions raised in the body of the unit -> unit thunk. That distinction goes away in your basic2.

1 Like

Thanks for your answer, but I do not understand: are you talking about the exceptions raised by Command.basic or by Command.Param.map ? Can you give a short example, or even a sketch, where behaviors of basic and basic2 would differ ?

Sure, compare the output of the following. Here is a command defined with Command.basic:

let command_basic =
  Command.basic ~summary:"This command is defined using [Command.basic]."
    (let%map_open.Command () = return () in
     fun () -> failwith "Raising an exception.")
Uncaught exception:

  (Failure "Raising an exception.")

Raised at Stdlib.failwith in file "stdlib.ml", line 29, characters 17-33
Called from Core_kernel__Command.For_unix.run.(fun) in file "src/command.ml", line 2453, characters 8-238
Called from Base__Exn.handle_uncaught_aux in file "src/exn.ml", line 111, characters 6-10

And here is a command defined with basic2:

let command_basic2 =
  basic2 ~summary:"This command is defined using [basic2]."
    (let%map_open.Command () = return () in
     failwith "Raising an exception.")
Error parsing command line:

  (Failure "Raising an exception.")

For usage information, run

  main.exe basic2 -help

Notice how in the latter case, even though we raised an exception in what was meant to be the body of our command, the exception was presented to the user as a parsing error.

1 Like

Wow, something subtle might be going on, because the two commands look synonymous to me (once you expand the definition of basic2 inside command_basic2), but they behave differently… I should probably read again the chapter Error Handling - Real World OCaml to understand this (or any other tutorial that you may recommend on that). Thanks !

For the record: I have to understand the difference between the following two functions:

let command_basic =
  Command.basic ~summary:""
    (let%map_open.Command () = return () in
     fun () -> failwith "")

let command_basic2 =
  Command.basic ~summary:""
    (Command.Param.map
      (let%map_open.Command () = return () in
       failwith "")
      ~f:(fun x -> (fun () -> x)))

I’m not sure there’s anything nuanced about error handling at issue here. It’s just about how Command.Param.t works:

  • A 'a Param.t is essentially a recipe for consuming 0 or more command-line arguments and producing a 'a.
  • Command.basic takes in a (unit -> unit) Param.t, (a) uses it to produce a unit -> unit function, and (b) calls that function.
  • Because of the separation between steps (a) and (b), they can be executed in different contexts; for example, different exception handlers can be in place.
  • Your command_basic2 forces all the computation, including the call to failwith, to be done in step (a); conversely, in command_basic the call to failwith happens in step (b).

It might help to think about the following example, which is similar in spirit:

type 'a t = [ `outer_param ] -> 'a

let map (t : 'a t) ~(f : 'a -> 'b) : 'b t = fun `outer_param -> f (t `outer_param)

let exec : (unit -> unit) t -> unit =
 fun outer_function ->
  printf "Before calling outer function.\n";
  let inner_function = outer_function `outer_param in
  printf "Before calling inner function.\n";
  inner_function ()
;;

let a = exec (fun `outer_param () -> failwith "")
let b = exec (map (fun `outer_param -> failwith "") ~f:(fun x () -> x))

Calling a will cause both “Before calling outer function” and “Before calling inner function” to be printed before the exception is raised, but if we call b then only the former is printed before the exception. Does it make sense why?

2 Likes

Thanks, that’s clear. The point was to be reminded when evaluation happens or is delayed. (I think your previous version before the edit, with “option”, was simpler to understand.)

There are several command line parsing libraries in opam, if you are dissatisfied with your current status.
My biased choice is towards minicli.
Here is an example:

If we are bringing up alternatives, there is also the Arg module which comes built-in with the standard OCaml distribution. It’s somewhat primitive in style, but does get the job done (unless your needs are extremely complex). I wrote up a post on it: Quick-and-dirty pure command-line arguments in OCaml - DEV Community

3 Likes

Thanks @UnixJunkie and @yawaramin. It is indeed a bit hard to choose:

  • Arg is in Stdlib, a big pro, but does it always use linux syntax for command arguments? Your post on making it functional is interesting.
  • Core.Command is nicely explained in a good textbook, but uses Base/Core (as the rest of the book), which are incompatible with Stdlib.
  • Cmdliner uses linux syntax, but seems to have a steeper learning curve (though I guess once one has seen such a “command-line parser”, it is not too hard to adapt to others).
1 Like

@user1 note that there is no such thing as “linux syntax”. There is the POSIX syntax and the GNU one and cmdliner supports both.

Did you try the tutorial (also available to you via odig doc cmdliner) ?

1 Like

Unless you are sure that you will never need git-like sub commands or online help pages, Cmdliner is worth the effort. It’s a good idea to start from a template project that has the basics covered and extend this as needed.

2 Likes

Well, Arg can also do subcommands thanks to parse_dynamic :slight_smile:

I agree it’s not elegant, but it can certainly get the job done.

As for online help pages, that’s a good point. If that is required, cmdliner does nicely. However, traditionally a quick reference is printed out for --help, and online manual pages are maintained separately, so I wouldn’t assume one size fits all.

1 Like