Can anyone recommend some examples that illustrate the use of Batteries.OptParse, or something more tutorial-ish, even? It looks like the documentation comments will be helpful once I get started, but getting started using only the documentation comments looks daunting. Thanks-
As @c-cube mentioned, for any non-toy program you should probably use cmdliner (which requires some amount of rolling-up sleeves). Otherwise, BatOptParse is probably a bit better than Arg, if you’re already using Batteries (I dislike Arg's API, which I find uncomfortable and unnatural).
In any case, here’s a quick example using BatOptParse I just put together (disclaimer: it’s the first time I use BatOptParse). It implements a simpler version of the head unix program.
head.ml:
open Batteries
module P = BatOptParse
(* HEAD(1) supports a multiplier suffix for the argument to [bytes] and [lines]
(e.g. MB = 1000*1000, M = 1024*1024, ...).
Here we only implement support for a naked integer parameter.
Given a parser function [parse : string -> int] which implements parsing of
integers with multiplier suffixes, we could extend the current implementation
to support the multiplier as follows:
let int_with_multiplier_option ?default () =
P.Opt.value_option
"NUM" default
parse
(fun exn s -> "Invalid argument: " ^ s)
let bytes = int_with_multiplier_option ()
let lines = int_with_multiplier_option ()
*)
(* The [metavar] optional argument is a string that represents the argument of
the option, and can be mentioned in its description (see the [help] argument
to [P.OptParser.add] below).
*)
let bytes_opt = P.StdOpt.int_option ~metavar:"NUM" ()
let lines_opt = P.StdOpt.int_option ~metavar:"NUM" ~default:10 ()
(* False by default; will be true is the option is present. *)
let zero_terminated_opt = P.StdOpt.store_true ()
(* We could have more simply done:
let quiet = P.StdOpt.store_true ()
let verbose = P.StdOpt.store_true ()
and then decided arbitrarily than one overrides the other if both are present.
Here, for the sake of the exercise, we decide to mimic the behavior of the
GNU head program, where the option that appears last overrides the other.
*)
let is_verbose = ref (Some false)
let store_const const = P.Opt.{
option_metavars = [];
option_defhelp = None;
option_get = (fun _ -> !is_verbose);
option_set_value = (fun x -> is_verbose := Some x);
option_set = (fun _ _ -> is_verbose := Some const)
}
let quiet_opt = store_const false
let verbose_opt = store_const true
(* Creates a [P.OptParser.t] value, which will collect the set of options we
want to parse, associated to option names, and some documentation.
*)
let optparser : P.OptParser.t =
P.OptParser.make
~prog:"head"
~usage:"%prog - output the first part of files"
~version:"0.1"
()
(* Add our options to [optparser]. *)
let () =
P.OptParser.add optparser
~help:"print the first NUM bytes of each file"
~short_name:'c'
~long_name:"bytes"
bytes_opt;
P.OptParser.add optparser
~help:"print the first NUM lines instead of the first 10"
~short_name:'n'
~long_name:"lines"
lines_opt;
P.OptParser.add optparser
~help:"never print headers giving file names"
~short_name:'q'
~long_names:["quiet"; "silent"]
quiet_opt;
P.OptParser.add optparser
~help:"always print headers giving file names"
~short_name:'v'
~long_name:"verbose"
verbose_opt;
P.OptParser.add optparser
~help:"line delimiter is NUL, not newline"
~short_name:'z'
~long_name:"zero-terminated"
zero_terminated_opt
(* Parse the command-line options added to [optparser]. Returns the list of
non-optional arguments; in our case, a list of filenames.
*)
let files = P.OptParser.parse_argv optparser
(* Read and interpret the values of parsed options.
*)
let line_terminator =
if P.Opt.get zero_terminated_opt then Char.chr 0
else '\n'
let verbose = P.Opt.get verbose_opt
let n, mode =
match P.Opt.opt bytes_opt with
| None -> P.Opt.get lines_opt, `Lines
| Some n -> n, `Bytes
(*** Now, the actual implementation of [head]. ***)
(* Reads a line, terminated by [term]. *)
let read_until (term : char) (input : BatIO.input) : string =
let buf = Buffer.create 37 in
let rec loop () =
try
let c = input_char input in
Buffer.add_char buf c;
if c = term then Buffer.contents buf
else loop ()
with End_of_file | BatInnerIO.Input_closed ->
Buffer.contents buf
in
loop ()
(* A function that wraps some computation [f input filename], providing
resource management (cleanly closes the input at the end, if needed).
*)
let with_file (f : BatIO.input -> string -> 'a) (filename : string) : 'a =
if filename = "-" then f stdin filename
else
let input = open_in filename in
let ret = f input filename in
close_in input;
ret
let () =
List.iter (with_file (fun input filename ->
if verbose then Printf.printf "==> %s <==\n%!"
(if filename = "-" then "standard input" else filename);
for _i = 0 to n - 1 do
match mode with
| `Lines ->
output_string stdout @@ read_until line_terminator input;
flush stdout
| `Bytes ->
output_char stdout @@ input_char input
done
)) files
@armael and @c-cube one thing that I learned with the time is that perception of software is largely based on myths rather than reality.
AFAIK cmdliner is the only alternative that is being discussed here which comes with a full tutorial and examples that show how the software can handle from the simplest utility to more complex ones with code that I don’t find especially hard to understand.
I do not claim that documentation is the clearest and will gladly take any criticism to improve it but if you agree with the above I would appreciate if you could not propagate the myth that cmdliner is somehow hard to use for newcomers. If you disagree I would like you to somehow justify why exactly you think this is the case.
@c-cube, @Armael, @dbuenzli, I’m finding all of these responses helpful. I don’t want to be the cause of conflict, but I welcome hearing different points of view.
Armael, it’s extremely kind of you to generate an example–thank you. I’m studying it now.
dbuenzli, I just found the cmndliner tutorial last night. Initially, I’d only looked at the first screen of the documentation and didn’t scroll down because I felt that I didn’t understand the basic concepts well enough to benefit from standard documentation. I didn’t realize what was below, but will read it now. (For the record, RWO includes a chapter on Jane Street’s Command library. I can’t compare its thoroughness or quality with the cmdliner tutorial.)
From my point of view, all of the OCaml commandline processing interfaces I’ve looked at (at least four so far) seem very mysterious. They might seem easy to experienced OCaml programmers, but not for me, at this stage. I think commandline processing is inherently a bit complicated. It seems like it should be easy, in the abstract, but it’s not, until you get used to a particular tool. I’ve used getopt-style interfaces for years in C, bash, perl, and who knows what else. It’s easy only because it’s familiar, but that doesn’t mean it’s appropriate for all languages. It took me a while to understand Clojure’s commandline library, which is nice but different from anything else I’d used. I just need to spend the time to absorb one or more of the available OCaml libraries. Of course I want exactly the functionality I need now, with easiest learning curve.
@dbuenzli sure. It took a bit of time to me to be familiar with cmdliner, even though I already know applicative APIs and this kind of type signature. Arg, despite being imperative and non-compositional, was trivial to use from the start (i.e. when I started OCaml). In my experience newcomers know imperative programming but tend to struggly with simple functional concepts like fold; imagine what & must look like to them. This is in no way a criticism of cmdliner or its docs (which are, as always, excellent), but just my impression that functional abstractions are harder at the beginning.
In my case, I’m quite comfortable with functional programming, but not all aspects of it equally well (I still only half-appreciate monads, for example), and that doesn’t mean that the way that an unfamiliar library works is easy to grasp, initially.