Module Cmdliner


module Cmdliner: sig .. end
Declarative definition of command line interfaces.

Cmdliner provides a simple and compositional mechanism to convert command line arguments to OCaml values and pass them to your functions. The module automatically handles syntax errors, help messages and UNIX man page generation. It supports programs with single or multiple commands (like darcs or git) and respect most of the POSIX and GNU conventions.

Consult the basics, details about the supported command line syntax and examples of use. Open the module to use it, it defines only three modules in your scope.

Version 0.9.0 - daniel.buenzl i@erratique.ch



Interface


module Manpage: sig .. end
Man page specification.
module Term: sig .. end
Terms.
module Arg: sig .. end
Terms for command line arguments.

Basics

With Cmdliner your program evaluates a term. A term is a value of type Cmdliner.Term.t. The type parameter indicates the type of the result of the evaluation.

One way to create terms is by lifting regular OCaml values with Cmdliner.Term.pure. Terms can be applied to terms evaluating to functional values with Cmdliner.Term.($) (the type for terms is an applicative functor). For example for the function:

let revolt () = print_endline "Revolt!"

    the term :
open Cmdliner;;

let revolt_t = Term.(pure revolt $ pure ())

    is a term that evaluates to the result (and effect) of the revolt 
    function.
    Terms are evaluated with Cmdliner.Term.eval:
let () = match Term.eval (Term.info "revolt") revolt_t with 
| `Error _ -> exit 1 | _ -> exit 0

    This defines a command line program named "revolt", without command line 
    arguments arguments, that just prints "Revolt!" on stdout.
> ./revolt 
Revolt!

    The combinators in the Cmdliner.Arg module allow to extract command
    line argument data as terms. These terms can then be applied to
    lifted OCaml functions to be evaluated by the program. 

Terms corresponding to command line argument data that are part of a term evaluation implicitly define a command line syntax. We show this on an concrete example.

Consider the chorus function that prints repeatedly a given message :

let chorus count msg = 
  for i = 1 to count do print_endline msg done

    we want to make it available from the command line
    with the synopsis:
chorus [-c COUNT | --count=COUNT] [MSG]

    where COUNT defaults to 10 and MSG defaults to "Revolt!". 
    We first define a term corresponding to the --count
    option:
let doc = "Repeat the message $(docv) times."
let count = Arg.(value & opt int 10 & info ["c""count"] ~docv:"COUNT" ~doc)

    This says that count is a term that evaluates to the 
    value of an optional argument of type int that 
    defaults to 10 if unspecified and whose option name is
    either -c or --count. The arguments doc and docv are used to 
    generate the option's man page information. 

The term for the positional argument MSG is:

let doc = "The message to print."
let msg = Arg.(value & pos 0 string "Revolt!" & info [] ~docv:"MSG" ~doc)

    which says that msg is a term whose value is 
    the positional argument at index 0 of type string and
    defaults to "Revolt!" if unspecified. Here again
    doc and docv are used for the man page information.

The term for executing chorus with these command line arguments is :

let chorus_t = Term.(pure chorus $ count $ msg)

    and we are now ready to define our program:
let info = Term.info "chorus" ~version:"1.6.1"
    ~doc:"print a customizable message repeatedly"
    ~man:[ `S "BUGS"`P "Email bug reports to <hehey at example.org>.";]

let () = match Term.eval info chorus_t with `Error _ -> exit 1 | _ -> exit 0

    The info value created with Cmdliner.Term.info gives more information
    about the term we execute and is used to generate the program's
    man page. Since we provided a ~version string, the program will
    automatically respond to the --version option by printing this
    string.

A program using Cmdliner.Term.eval always responds to the --help option by showing the man page about the program generated using the information you provided with Cmdliner.Term.info and Cmdliner.Arg.info. Here is the output generated by our example :

> ./chorus --help
NAME
       chorus - print a customizable message repeatedly

SYNOPSIS
       chorus [OPTION]... [MSG]

ARGUMENTS
       MSG (absent=Revolt!)
           The message to print.

OPTIONS
       -c COUNT, --count=COUNT (absent=10)
           Repeat the message COUNT times.

       --help[=FMT] (default=pager)
           Show this help in format FMT (pager, plain or groff).

       --version
           Show version information.

BUGS
       Email bug reports to <hehey at example.org>.

If a pager is available, this output is written to a pager. This help is also available in plain text or in the groff man page format by invoking the program with the option --help=plain or --help=groff.

For examples of more complex command line definitions look and run the examples.

Multiple terms

Cmdliner also provides support for programs like darcs or git that have multiple commands each with their own syntax:

prog COMMAND [OPTION]... ARG...

    A command is defined by coupling a term information
    with a term. The term information defines the command name and its
    man page. Given a list of commands the function
    Cmdliner.Term.eval_choice will execute the term corresponding to the
    COMMAND argument or or a specific "main" term if there is
    no COMMAND argument.

Manual

Man page sections are printed in the order specified by Cmdliner.Term.info. The man page information of an argument is listed in alphabetical order at the end of the text of the section specified by its argument information. Positional arguments are also listed iff both the docv and doc string is specified in their argument information.

If an argument information mentions a section not specified in Cmdliner.Term.info, an empty section is created for it. This section is inserted just after the "SYNOPSIS" section or after a section named "DESCRIPTION" if there is one.

Ideally all manual strings should be UTF-8 encoded. However at the moment Groff (at least 1.19.2) doesn't seem to cope with UTF-8 input and UTF-8 characters beyond the ASCII set will look garbled. Regarding UTF-8 output, generating the man page with -Tutf8 maps the hyphen-minus U+002D to the minus sign U+2212 which makes it difficult to search it in the pager, so -Tascii is used for now. Conclusion is that it may be better to stick to the ASCII set for now. Please contact the author if something seems wrong in this reasoning or if you know a work around this.

Miscellaneous

Command line syntax

For programs evaluating a single term the most general form of invocation is:

The program automatically reponds to the --help option by printing the help. If a version string is provided in the term information, it also automatically responds to the --version option by printing this string.

Command line arguments are either optional or positional. Both can be freely interleaved but since Cmdliner accepts many optional forms this may result in ambiguities. The special token -- can be used to resolve them.

Programs evaluating multiple terms also add this form of invocation:

Commands automatically respond to the --help option by printing their help. The COMMAND string must be the first string following the program name and may be specified by a prefix as long as it is not ambiguous.

Optional arguments

An optional argument is specified on the command line by a name possibly followed by a value.

The name of an option can be short or long.

More than one name may refer to the same optional argument. For example in a given program the names "-q", "--quiet" and "--silent" may all stand for the same boolean argument indicating the program to be quiet. Long names can be specified by any non ambiguous prefix.

The value of an option can be specified in three different ways.

Glued forms are especially useful if the value itself starts with a dash as is the case for negative numbers, "--min=-10".

An optional argument without a value is either a flag (see Cmdliner.Arg.flag, Cmdliner.Arg.vflag) or an optional argument with an optional value (see the ~vopt argument of Cmdliner.Arg.opt).

Positional arguments

Positional arguments are tokens on the command line that are not option names and are not the value of an optional argument. They are numbered from left to right starting with zero.

Since positional arguments may be mistaken as the optional value of an optional argument or they may need to look like option names, anything that follows the special token "--" on the command line is considered to be a positional argument.

Examples

These examples are in the test directory of the distribution.

A rm command

We define the command line interface of a rm command with the synopsis:

rm [OPTION]... FILE...

    The -f, -i and -I flags define the prompt behaviour of rm,
    represented in our program by the prompt type. If more than one
    of these flags is present on the command line the last one takes
    precedence.

To implement this behaviour we map the presence of these flags to values of the prompt type by using Cmdliner.Arg.vflag_all. This argument will contain all occurences of the flag on the command line and we just take the Cmdliner.Arg.last one to define our term value (if there's no occurence the last value of the default list [Always] is taken, i.e. the default is Always).

(* Implementation of the command, we just print the args. *)

type prompt = Always | Once | Never
let prompt_str = function 
  | Always -> "always" | Once -> "once" | Never -> "never"

let rm prompt recurse files =
  Printf.printf "prompt = %s\nrecurse = %b\nfiles = %s\n"
    (prompt_str prompt) recurse (String.concat ", " files)

(* Command line interface *)

open Cmdliner;;

let files = Arg.(non_empty & pos_all file [] & info [] ~docv:"FILE")
let prompt =
  let always = AlwaysArg.info ["i"
      ~doc:"Prompt before every removal." in
  let never = NeverArg.info ["f""force"]
      ~doc:"Ignore nonexistent files and never prompt." in
  let once = OnceArg.info ["I"]
      ~doc:"Prompt once before removing more than three files, or when
            removing recursively. Less intrusive than $(b,-i), while 
            still giving protection against most mistakes."
 in
  Arg.(last & vflag_all [Always] [always; never; once])

let recursive = Arg.(value & flag & info ["r""R""recursive"
                     ~doc:"Remove directories and their contents recursively.")


let rm_t = Term.(pure rm $ prompt $ recursive $ files)
let info = Term.info "rm" ~version:"1.6.1" ~doc:"remove files or directories"
    ~man:
    [`S "DESCRIPTION";
     `P "rm removes each specified $(i,FILE). By default it does not remove
         directories, to also remove them and their contents, use the
         option $(b,--recursive) ($(b,-r) or $(b,-R))."
;
     `P "To remove a file whose name starts with a `-', for example
         `-foo', use one of these commands:"
;
     `P "> rm -- -foo"`Noblank;
     `P "> rm ./-foo";
     `P "rm removes symbolic links, not the files referenced by the links.";
     `S "BUGS"`P "Report bugs to <hehey at example.org>.";
     `S "SEE ALSO"`P "rmdir(1), unlink(2)"]

let () = match Term.eval info rm_t with `Error _ -> exit 1 | _ -> exit 0

    

A cp command

We define the command line interface of a cp command with the synopsis:

cp [OPTION]... SOURCE... DEST 

    The DEST argument must be a directory if there is more than 
    one SOURCE. This constraint is too complex to be expressed by the
    combinators of Cmdliner.Arg. Hence we just give it the Cmdliner.Arg.string type
    and verify the constraint at the beginning of the cp
    implementation. If unsatisfied we return an `Error and
    by using Cmdliner.Term.ret on the lifted result cp_t of cp, 
    Cmdliner handles the error reporting.
(* Implementation, we check the dest argument and print the args *)

let cp verbose recurse force srcs dest =
  if List.length srcs > 1 && 
    (not (Sys.file_exists dest) || not (Sys.is_directory dest)) 
  then
    `Error (false, dest ^ " is not a directory"
  else 
    `Ok (Printf.printf 
           "verbose = %b\nrecurse = %b\nforce = %b\nsrcs = %s\ndest = %s\n" 
            verbose recurse force (String.concat ", " srcs) dest)

(* Command line interface *)

open Cmdliner;;

let doc = "Print file names as they are copied." 
let verbose = Arg.(value & flag & info ["v""verbose"] ~doc) 

let doc = "Copy directories recursively."
let recurse = Arg.(value & flag & info ["r""R""recursive"] ~doc)

let doc = "If a destination file cannot be opened, remove it and try again."
let force = Arg.(value & flag & info ["f""force"] ~doc) 

let doc = "Source file(s) to copy."
let srcs = Arg.(non_empty & pos_left ~rev:true 0 file [] & info [] 
                ~docv:"SOURCE" ~doc) 

let doc = "Destination of the copy. Must be a directory if there is more 
           than one $(i,SOURCE)."

let dest = Arg.(required & pos ~rev:true 0 (some string) None & info [] 
                ~docv:"DEST" ~doc)

let cp_t = Term.(ret (pure cp $ verbose $ recurse $ force $ srcs $ dest))
let info = Term.info "cp" ~version:"1.6.1" ~doc:"copy files" ~man:
    [`S "BUGS"`P "Email them to <hehey at example.org>.";
     `S "SEE ALSO"`P "mv(1), scp(1), umask(2), symlink(7)"]

let () = match Term.eval info cp_t with `Error _ -> exit 1 | _ -> exit 0

A tail command

We define the command line interface of a tail command with the synopsis:

tail [OPTION]... [FILE]...

The --lines option whose value specifies the number of last lines to print has a special syntax where a + prefix indicates to start printing from that line number. In the program this is represented by the loc type. We define a custom loc argument converter for this option.

The --follow option has an optional enumerated value. The argument converter follow, created with Cmdliner.Arg.enum parses the option value into the enumeration. By using Cmdliner.Arg.some and the ~vopt argument of Cmdliner.Arg.opt, the term corresponding to the option --follow evaluates to None if --follow is absent from the command line, to Some Descriptor if present but without a value and to Some v if present with a value v specified.

(* Implementation of the command, we just print the args. *)

type loc = bool * int
type verb = Verbose | Quiet 
type follow = Name | Descriptor

let str = Printf.sprintf 
let opt_str sv = function None -> "None" | Some v -> str "Some(%s)" (sv v)
let loc_str (rev, k) = if rev then str "%d" k else str "+%d" k
let follow_str = function Name -> "name" | Descriptor -> "descriptor"
let verb_str = function Verbose -> "verbose" | Quiet -> "quiet"

let tail lines follow verb pid files = 
  Printf.printf "lines = %s\nfollow = %s\nverb = %s\npid = %s\nfiles = %s\n"
    (loc_str lines) (opt_str follow_str follow) (verb_str verb) 
    (opt_str string_of_int pid) (String.concat ", " files)

(* Command line interface *)

open Cmdliner;;

let lines = 
  let loc =
    let parse s = try
      if s <> "" && s.[0] <> '+' then `Ok (true, int_of_string s) else
      `Ok (false, int_of_string (String.sub s 1 (String.length s - 1))) 
    with Failure _ -> `Error "unable to parse integer"
    in
    parse, fun ppf p -> Format.fprintf ppf "%s" (loc_str p)
  in
  Arg.(value & opt loc (true, 10) & info ["n""lines"] ~docv:"N" 
         ~doc:"Output the last $(docv) lines or use $(i,+)$(docv) to start 
               output after the $(i,N)-1th line."

let follow = 
  let follow = Arg.enum ["name"Name"descriptor"Descriptorin
  Arg.(value & opt (some follow) ~vopt:(Some DescriptorNone & 
       info ["f""follow"] ~docv:"ID" 
         ~doc:"Output appended data as the file grows. $(docv) specifies how the
               file should be tracked, by its `name' or by its `descriptor'."

let verb = 
  let quiet = QuietArg.info ["q""quiet""silent"]
      ~doc:"Never output headers giving file names." in 
  let verbose = VerboseArg.info ["v""verbose"]
      ~doc:"Always output headers giving file names." in 
  Arg.(last & vflag_all [Quiet] [quiet; verbose])

let pid = Arg.(value & opt (some int) None & info ["pid"] ~docv:"PID"
                ~doc:"With -f, terminate after process $(docv) dies.")
let files = Arg.(value & (pos_all non_dir_file []) & info [] ~docv:"FILE")

let tail_t = Term.(pure tail $ lines $ follow $ verb $ pid $ files)
let info = Term.info "tail" ~version:"1.6.1" 
    ~doc:"display the last part of a file" ~man:
    [`S "DESCRIPTION";
     `P "tail prints the last lines of each $(i,FILE) to standard output. If
         no file is specified reads standard input. The number of printed
         lines can be  specified with the $(b,-n) option."
;
     `S "BUGS"`P "Report them to <hehey at example.org>.";
     `S "SEE ALSO"`P "cat(1), head(1)" ]

let () = match Term.eval info tail_t with `Error _ -> exit 1 | _ -> exit 0

A darcs command

We define the command line interface of a darcs command with the synopsis:

darcs [COMMAND] ...

The --debug, -q, -v and --prehook options are available in each command. To avoid having to pass them individually to each command we gather them in a record of type copts. By lifting the record constructor copts into the term copts_t we now have a term that we can pass to the commands to stand for an argument of type copts. These options are documented in a section called COMMON OPTIONS, since we also want to put --help and --version in this section, the term information of commands makes a judicious use of the sdocs parameter of Cmdliner.Term.info.

The help command shows help about commands or other topics. The help shown for commands is generated by Cmdliner by making an approriate use of Cmdliner.Term.ret on the lifted help function.

If the program is invoked without a command we just want to show the help of the program as printed by Cmdliner with --help. This is done by the no_cmd term.

(* Implementations, just print the args. *)

type verb = Normal | Quiet | Verbose
type copts = { debug : bool; verb : verb; prehook : string option }

let str = Printf.sprintf 
let opt_str sv = function None -> "None" | Some v -> str "Some(%s)" (sv v)
let opt_str_str = opt_str (fun s -> s)
let verb_str = function 
  | Normal -> "normal" | Quiet -> "quiet" | Verbose -> "verbose"

let pr_copts oc copts = Printf.fprintf oc 
    "debug = %b\nverbosity = %s\nprehook = %s\n" 
    copts.debug (verb_str copts.verb) (opt_str_str copts.prehook)

let initialize copts repodir = Printf.printf
    "%arepodir = %s\n" pr_copts copts repodir

let record copts name email all ask_deps files = Printf.printf
    "%aname = %s\nemail = %s\nall = %b\nask-deps = %b\nfiles = %s\n" 
    pr_copts copts (opt_str_str name) (opt_str_str email) all ask_deps 
    (String.concat ", " files)

let help copts cmds topic = match topic with
| None -> `Help (`PagerNone(* help about the program. *)
| Some topic -> 
    if List.mem topic cmds then `Help (`Pager, (Some topic)) else
    let page = (topic, 7, """"""), [`S topic; `P "Say something";] in
    `Ok (Cmdliner.Manpage.print `Pager Format.std_formatter page)

open Cmdliner;;

(* Help sections common to all commands *)

let copts_sect = "COMMON OPTIONS"
let help_secs = [ 
  `S copts_sect; 
  `P "These options are common to all commands.";
  `S "MORE HELP";
  `P "Use `darcs $(i,COMMAND) --help' for help on a single command.";`Noblank;
  `P "Use `darcs help patterns' for help on patch matching."`Noblank;
  `P "Use `darcs help environment' for help on environment variables.";
  `S "BUGS"`P "Check bug reports at http://bugs.example.org.";]

(* Options common to all commands *)

let copts debug verb prehook = { debug; verb; prehook }
let copts_t = 
  let docs = copts_sect in 
  let debug = Arg.(value & flag & info ["debug"] ~docs
        ~doc:"Give only debug output."in
  let verb =
    let quiet = QuietArg.info ["q""quiet"] ~docs
        ~doc:"Suppress informational output." in
    let verbose = VerboseArg.info ["v""verbose"] ~docs
        ~doc:"Give verbose output." in 
    Arg.(last & vflag_all [Normal] [quiet; verbose]) 
  in 
  let prehook = Arg.(value & opt (some string) None & info ["prehook"] ~docs
        ~doc:"Specify command to run before this darcs command.")
  in
  Term.(pure copts $ debug $ verb $ prehook)
    
(* Commands *)

let initialize_cmd = 
  let open Term in
  let info = info "initialize" ~sdocs:copts_sect
      ~doc:"make the current directory a repository" ~man:
      ([`S "DESCRIPTION";
        `P "Turns the current directory into a Darcs repository. Any
            existing files and subdirectories become ..."
] @ help_secs);
  in
  info, pure initialize $ copts_t $ 
  Arg.(value & opt file Filename.current_dir_name & info ["repodir"]
         ~docv:"DIR" ~doc:"Run the program in repository directory $(docv).")

let record_cmd =
  let open Term in
  let info = info "record"
      ~doc:"create a patch from unrecorded changes" ~sdocs:copts_sect ~man:
      ([`S "DESCRIPTION";
        `P "Creates a patch from changes in the working tree. If you specify 
            a set of files ..."
] @ help_secs)
  in 
  info,
  pure record $ copts_t $
  Arg.(value & opt (some string) None & info ["m""patch-name"] ~docv:"NAME" 
         ~doc:"Name of the patch.") $
  Arg.(value & opt (some string) None & info ["A""author"] ~docv:"EMAIL"
         ~doc:"Specifies the author's identity.") $
  Arg.(value & flag & info ["a""all"]
         ~doc:"Answer yes to all patches.") $
  Arg.(value & flag & info ["ask-deps"]
         ~doc:"Ask for extra dependencies.") $
  Arg.(value & (pos_all file) [] & info [] ~docv:"FILE or DIR")

let help_cmd = 
  let open Term in
  let info = info "help" ~sdocs:copts_sect
      ~doc:"display help about darcs and darcs commands" ~man:
      ([`S "DESCRIPTION";
        `P "Without a $(i,TOPIC), prints a list of darcs commands and a short
            description of each one ..."
] @ help_secs)
  in
  info,
  ret (pure help $ copts_t $ Term.choice_names $
  Arg.(value & pos 0 (some string) None & info [] ~docv:"TOPIC" 
         ~doc:"The topic to get help on: a $(i,COMMAND), `patterns' or 
               `environment'."
))

let cmds = [initialize_cmd; record_cmd; help_cmd]
let no_cmd = Term.(ret (pure help $ copts_t $ Term.choice_names $ pure None))
let info = Term.info "darcs" ~version:"1.6.1" ~sdocs:copts_sect
    ~doc:"a revision control system" ~man:help_secs

let () = match Term.eval_choice info no_cmd cmds with 
| `Error _ -> exit 1 | _ -> exit 0