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

Release 0.9.3 - Daniel Bünzli <daniel.buenzli at 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 (revolt_t, Term.info "revolt") 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 count =

      let doc = "Repeat the message $(docv) times." in

      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 msg =

      let doc = "The message to print." in

      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 =

      let doc = "print a customizable message repeatedly" in

      let man = [ `S "BUGS"; `P "Email bug reports to <hehey at example.org>.";] in

      Term.info "chorus" ~version:"1.6.1" ~doc ~man

    let () = match Term.eval (chorus_t, info) 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 with term information. 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.

The `"SYNOPSIS"` section of a man page is generated automatically from a term's information and its arguments. To substitute your own instead, start the term's information man page with a `"SYNOPSIS"` section.

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

- The option name `--help`, (and `--version` if you specify a version string) is reserved by the module. Using it as a term or option name may result in undefined behaviour.
- The evaluation of a term in which the same option name is defined by more than one argument is undefined.

# Command line syntax

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

- `prog [OPTION]... [ARG]...`

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:

- `prog COMMAND [OPTION]... [ARG]...`

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.

- A short name is a dash followed by a single alphanumeric character: `"-h"`, `"-q"`, `"-I"`.
- A long name is two dashes followed by alphanumeric characters and dashes: `"--help"`, `"--silent"`, `"--ignore-case"`.

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.

- As the next token on the command line: `"-o a.out"`, `"--output a.out"`.
- Glued to a short name: `"-oa.out"`.
- Glued to a long name after an equal character: `"--output=a.out"`.

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 doc = "Prompt before every removal." in

      let always = Always, Arg.info ["i"] ~doc in

      let doc = "Ignore nonexistent files and never prompt." in

      let never = Never, Arg.info ["f"; "force"] ~doc in

      let 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

      let once = Once, Arg.info ["I"] ~doc in

      Arg.(last & vflag_all [Always] [always; never; once])

    let recursive =

      let doc = "Remove directories and their contents recursively." in

      Arg.(value & flag & info ["r"; "R"; "recursive"] ~doc)

    let cmd =

      let doc = "remove files or directories" in

      let man = [

        `S "DESCRIPTION";

        `P "$(tname) 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 "$(tname) removes symbolic links, not the files referenced by the

            links.";

        `S "BUGS"; `P "Report bugs to <hehey at example.org>.";

        `S "SEE ALSO"; `P "$(b,rmdir)(1), $(b,unlink)(2)" ]

      in

      Term.(pure rm $ prompt $ recursive $ files),

      Term.info "rm" ~version:"1.6.1" ~doc ~man

    let () = match Term.eval cmd 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 verbose =

      let doc = "Print file names as they are copied." in

      Arg.(value & flag & info ["v"; "verbose"] ~doc)

    let recurse =

      let doc = "Copy directories recursively." in

      Arg.(value & flag & info ["r"; "R"; "recursive"] ~doc)

    let force =

      let doc = "If a destination file cannot be opened, remove it and try again."in

      Arg.(value & flag & info ["f"; "force"] ~doc)

    let srcs =

      let doc = "Source file(s) to copy." in

      Arg.(non_empty & pos_left ~rev:true 0 file [] & info [] ~docv:"SOURCE" ~doc)

    let dest =

      let doc = "Destination of the copy. Must be a directory if there is more

               than one $(i,SOURCE)." in

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

             ~doc)

    let cmd =

      let doc = "copy files" in

      let man = [

        `S "BUGS";

        `P "Email them to <hehey at example.org>.";

        `S "SEE ALSO";

        `P "$(b,mv)(1), $(b,scp)(1), $(b,umask)(2), $(b,symlink)(7)" ]

      in

      Term.(ret (pure cp $ verbose $ recurse $ force $ srcs $ dest)),

      Term.info "cp" ~version:"1.6.1" ~doc ~man

    let () = match Term.eval cmd 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 doc = "Output appended data as the file grows. $(docv) specifies how the

                   file should be tracked, by its `name' or by its `descriptor'." in

      let follow = Arg.enum ["name", Name; "descriptor", Descriptor] in

      Arg.(value & opt (some follow) ~vopt:(Some Descriptor) None &

           info ["f"; "follow"] ~docv:"ID" ~doc)

    let verb =

      let doc = "Never output headers giving file names." in

      let quiet = Quiet, Arg.info ["q"; "quiet"; "silent"] ~doc in

      let doc = "Always output headers giving file names." in

      let verbose = Verbose, Arg.info ["v"; "verbose"] ~doc in

      Arg.(last & vflag_all [Quiet] [quiet; verbose])

    let pid =

      let doc = "With -f, terminate after process $(docv) dies." in

      Arg.(value & opt (some int) None & info ["pid"] ~docv:"PID" ~doc)

    let files = Arg.(value & (pos_all non_dir_file []) & info [] ~docv:"FILE")

    let cmd =

      let doc = "display the last part of a file" in

      let man = [

        `S "DESCRIPTION";

        `P "$(tname) 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 "$(b,cat)(1), $(b,head)(1)" ]

      in

      Term.(pure tail $ lines $ follow $ verb $ pid $ files),

      Term.info "tail" ~version:"1.6.1" ~doc ~man

    let () = match Term.eval cmd 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 man_format cmds topic = match topic with

    | None -> `Help (`Pager, None) (* help about the program. *)

    | Some topic ->

        let topics = "topics" :: "patterns" :: "environment" :: cmds in

        let conv, _ = Cmdliner.Arg.enum (List.rev_map (fun s -> (s, s)) topics) in

        match conv topic with

        | `Error e -> `Error (false, e)

        | `Ok t when t = "topics" -> List.iter print_endline topics; `Ok ()

        | `Ok t when List.mem t cmds -> `Help (man_format, Some t)

        | `Ok t ->

            let page = (topic, 7, "", "", ""), [`S topic; `P "Say something";] in

            `Ok (Cmdliner.Manpage.print man_format 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 `$(mname) $(i,COMMAND) --help' for help on a single command.";`Noblank;

     `P "Use `$(mname) help patterns' for help on patch matching."; `Noblank;

     `P "Use `$(mname) 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 =

        let doc = "Give only debug output." in

        Arg.(value & flag & info ["debug"] ~docs ~doc)

      in

      let verb =

        let doc = "Suppress informational output." in

        let quiet = Quiet, Arg.info ["q"; "quiet"] ~docs ~doc in

        let doc = "Give verbose output." in

        let verbose = Verbose, Arg.info ["v"; "verbose"] ~docs ~doc in

        Arg.(last & vflag_all [Normal] [quiet; verbose])

      in

      let prehook =

        let doc = "Specify command to run before this $(mname) command." in

        Arg.(value & opt (some string) None & info ["prehook"] ~docs ~doc)

      in

      Term.(pure copts $ debug $ verb $ prehook)

    (* Commands *)

    let initialize_cmd =

      let repodir =

        let doc = "Run the program in repository directory $(docv)." in

        Arg.(value & opt file Filename.current_dir_name & info ["repodir"]

               ~docv:"DIR" ~doc)

      in

      let doc = "make the current directory a repository" in

      let man = [

        `S "DESCRIPTION";

        `P "Turns the current directory into a Darcs repository. Any

           existing files and subdirectories become ..."] @ help_secs

      in

      Term.(pure initialize $ copts_t $ repodir),

      Term.info "initialize" ~sdocs:copts_sect ~doc ~man

    let record_cmd =

      let pname =

        let doc = "Name of the patch." in

        Arg.(value & opt (some string) None & info ["m"; "patch-name"] ~docv:"NAME"

               ~doc)

      in

      let author =

        let doc = "Specifies the author's identity." in

        Arg.(value & opt (some string) None & info ["A"; "author"] ~docv:"EMAIL"

               ~doc)

      in

      let all =

        let doc = "Answer yes to all patches." in

        Arg.(value & flag & info ["a"; "all"] ~doc)

      in

      let ask_deps =

        let doc = "Ask for extra dependencies." in

        Arg.(value & flag & info ["ask-deps"] ~doc)

      in

      let files = Arg.(value & (pos_all file) [] & info [] ~docv:"FILE or DIR") in

      let doc = "create a patch from unrecorded changes" in

      let man =

        [`S "DESCRIPTION";

         `P "Creates a patch from changes in the working tree. If you specify

                a set of files ..."] @ help_secs

      in

      Term.(pure record $ copts_t $ pname $ author $ all $ ask_deps $ files),

      Term.info "record" ~doc ~sdocs:copts_sect ~man

    let help_cmd =

      let topic =

        let doc = "The topic to get help on. `topics' lists the topics." in

        Arg.(value & pos 0 (some string) None & info [] ~docv:"TOPIC" ~doc)

      in

      let doc = "display help about darcs and darcs commands" in

      let man =

        [`S "DESCRIPTION";

         `P "Prints help about darcs commands and other subjects..."] @ help_secs

      in

      Term.(ret (pure help $ copts_t $ Term.man_format $ Term.choice_names $topic)),

      Term.info "help" ~doc ~man

    let default_cmd =

      let doc = "a revision control system" in

      let man = help_secs in

      Term.(ret (pure (fun _ -> `Help (`Pager, None)) $ copts_t)),

      Term.info "darcs" ~version:"1.6.1" ~sdocs:copts_sect ~doc ~man

    let cmds = [initialize_cmd; record_cmd; help_cmd]

    let () = match Term.eval_choice default_cmd cmds with

    | `Error _ -> exit 1 | _ -> exit 0