Lhooq
Lhooq

Reputation: 4441

Is there a way to group arguments in a record when using Cmdliner?

Let's say I have the following code

let main a b c d e = Format.eprintf "%B %B %B %B %B@." a b c d e

let cmd =
  let open Cmdliner in
  let a = Arg.(value & flag & info ["a"] ~doc:"a") in
  let b = Arg.(value & flag & info ["b"] ~doc:"b") in
  let c = Arg.(value & flag & info ["c"] ~doc:"c") in
  let d = Arg.(value & flag & info ["d"] ~doc:"d") in
  let e = Arg.(value & flag & info ["e"] ~doc:"e") in

  Term.(const main $ a $ b $ c $ d $ e), Term.(info "test" ~version:"1" ~doc:"abcde" ~exits:default_exits ~man:[])

let () = Cmdliner.Term.(exit @@ eval cmd)

If I execute my program with no option I will obtain false false false false false and if I use it with -ade I will obtain true false false true true which is exactly what I wanted.

Now, suppose I made a typo in my main function and wrote instead

(* Notice the d before c *)
let main a b d c e = Format.eprintf "%B %B %B %B %B@." a b c d e

If I execute my main program with -ade like previously I will obtain true false true false true which can be considered wrong.

So, what I wanted to know is if it was possible to gather options in a record to use them with their proper names, something like the following example (which doesn't work) :

open Cmdliner

type o = {a : bool Term.t;
          b : bool Term.t;
          c : bool Term.t;
          d : bool Term.t;
          e : bool Term.t;}

(* a - e are not booleans but bool Term.t which gives an obvious error *)
let main {a; b; c; d; e} = Format.eprintf "%B %B %B %B %B@." a b c d e

let cmd =
  let a = Arg.(value & flag & info ["a"] ~doc:"a") in
  let b = Arg.(value & flag & info ["b"] ~doc:"b") in
  let c = Arg.(value & flag & info ["c"] ~doc:"c") in
  let d = Arg.(value & flag & info ["d"] ~doc:"d") in
  let e = Arg.(value & flag & info ["e"] ~doc:"e") in

  let o = Term.const {a; b; c; d; e} in
  Term.(const main $ o), Term.(info "test" ~version:"1" ~doc:"abcde" ~exits:default_exits ~man:[])

let () = Cmdliner.Term.(exit @@ eval cmd)

This could be useful on big projects and would lighten the number of arguments given to the functions. Maybe there's a way to do it but all the examples I found used the first way of doing. I didn't want to open an issue on the github page so I asked it here.

Upvotes: 1

Views: 143

Answers (2)

Elarnon
Elarnon

Reputation: 56

This can be achieved with relatively few boilerplate by using labels to emulate a record with Term.t fields, for instance:

type arg = {a : bool; b : bool; c : bool; d : bool; e : bool}
                                                                    
let main {a; b; c; d; e} = Format.printf "%B %B %B %B %B@." a b c d e
                                                                    
let cmd =
  let open Cmdliner in              
  let arg ~a ~b ~c ~d ~e =                     
    Term.(const (fun a b c d e -> {a; b; c; d; e}) $ a $ b $ c $ d $ e)
  in                                            
  let a = Arg.(value & flag & info ["a"] ~doc:"a") in
  let b = Arg.(value & flag & info ["b"] ~doc:"b") in
  let c = Arg.(value & flag & info ["c"] ~doc:"c") in
  let d = Arg.(value & flag & info ["d"] ~doc:"d") in
  let e = Arg.(value & flag & info ["e"] ~doc:"e") in                                                                                    
  Term.
    ( const main $ arg ~a ~b ~c ~d ~e
    , info "test" ~version:"1" ~doc:"abcde" ~exits:default_exits ~man:[] )
                                                                                                                                         
let () = Cmdliner.Term.(exit @@ eval cmd)

By using the same name for the keyword arguments and the record fields, the risk of typos is limited to the conversion function (arg here), which is presumably much simpler than your real main function. In a large project, the conversion function could easily be generated automatically using a ppx.

Upvotes: 2

octachron
octachron

Reputation: 18892

This can be done quite directly if you write the field update functions for the record type. For instance, if we have

type arg = { a:bool; b:bool; c:bool; d:bool; e: bool }

let main {a;b;c;d;e} = Format.eprintf "%B %B %B %B %B@." a b c d e

module Update = struct
  let a a r = { r with a }
  let b b r = { r with b }
  let c c r = { r with c }
  let d d r = { r with d }
  let e e r = { r with e }
end

The only missing step is to transform Cmdliner.Term.t that directly provides the argument into terms that update a record of type arg. An implementation would be:

let cmd =
  let open Cmdliner in
  (* first the starting record *)
  let start = Term.const { a = false; b=false; c=false; d=false; e=false } in
  let transform r (update,arg) = 
    Term.( const update $ arg $ r ) in
  let arg =
    List.fold_left transform
      start
      Update.[
        a, Arg.(value & flag & info ["a"] ~doc:"a");
        b, Arg.(value & flag & info ["b"] ~doc:"b");
        c, Arg.(value & flag & info ["c"] ~doc:"c");
        d, Arg.(value & flag & info ["d"] ~doc:"d");
        e, Arg.(value & flag & info ["e"] ~doc:"e");
    ] in
  Term.(const main $ arg),
  Term.info "test"
     ~version:"1"
     ~doc:"abcde"
     ~exits:Term.default_exits
     ~man:[]

let () = Cmdliner.Term.(exit @@ eval cmd)

Upvotes: 2

Related Questions