Example or tutorial on Batteries.OptParse

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-

Marshall

Afaik most people either use Arg (stdlib) or the standalone cmdliner (which is very good but a bit harder to approach for beginners).

Thanks c-cube. That’s helpful. I’ll look at Arg. I’ve been looking at cmdliner and Jane Street Core’s command line module as well.

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
1 Like

@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.

Thanks.

3 Likes

@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. :slight_smile:

@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.

You may also be interested in ppx_deriving_cmdliner, which helpfully opens with a nice example.

That’s a very simple, clear example. Thanks.