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. v1.0.4 — homepage

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.const . Terms can be applied to terms evaluating to functional values with Cmdliner.Term.($) . For example for the function:

let revolt () = print_endline "Revolt!"

the term :

open Cmdliner let revolt_t = Term .(const revolt $ const ())

is a term that evaluates to the result (and effect) of the revolt function. Terms are evaluated with Cmdliner.Term.eval :

let () = Term .exit @@ Term .eval (revolt_t, Term .info "revolt" )

This defines a command line program named "revolt" , without command line 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 = "Overrides the default message to print." in let env = Arg .env_var "CHORUS_MSG" ~doc in let doc = "The message to print." in Arg .(value & pos 0 string "Revolt!" & info [] ~env ~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!" or the value of the environment variable CHORUS_MSG if the argument is unspecified on the command line. 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 .(const 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 Manpage .s_bugs; ` P "Email bug reports to <hehey at example.org>." ] in Term .info "chorus" ~version: "%‌%VERSION%%" ~doc ~exits: Term .default_exits ~man let () = Term .exit @@ Term .eval (chorus_t, info))

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! or CHORUS_MSG env) The message to print. OPTIONS -c COUNT, --count=COUNT (absent=10) Repeat the message COUNT times. --help[=FMT] (default=auto) Show this help in format FMT. The value FMT must be one of `auto', `pager', `groff' or `plain'. With `auto', the format is `pager` or `plain' whenever the TERM env var is `dumb' or undefined. --version Show version information. EXIT STATUS chorus exits with the following status: 0 on success. 124 on command line parsing errors. 125 on unexpected internal errors (bugs). ENVIRONMENT These environment variables affect the execution of chorus: CHORUS_MSG Overrides the default message to print. 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 a specific "main" term if there is no COMMAND argument.

Documentation markup language

Manpage blocks and doc strings support the following markup language.

Markup directives $(i,text) and $(b,text) , where text is raw text respectively rendered in italics and bold.

and , where is raw text respectively rendered in italics and bold. Outside markup directives, context dependent variables of the form $(var) are substituted by marked up data. For example in a term's man page $(tname) is substituted by the term name in bold.

are substituted by marked up data. For example in a term's man page is substituted by the term name in bold. Characters $, (, ) and \ can respectively be escaped by \$, \(, \) and \\ (in OCaml strings this will be "\\$" , "\\(" , "\\)" , "\\\\" ). Escaping $ and \ is mandatory everywhere. Escaping ) is mandatory only in markup directives. Escaping ( is only here for your symmetric pleasure. Any other sequence of characters starting with a \ is an illegal character sequence.

, , , ). Escaping $ and \ is mandatory everywhere. Escaping ) is mandatory only in markup directives. Escaping ( is only here for your symmetric pleasure. Any other sequence of characters starting with a \ is an illegal character sequence. Refering to unknown markup directives or variables will generate errors on standard error during documentation generation.

Manual

Man page sections for a term are printed in the order specified by the term manual as given to Cmdliner.Term.info . Unless specified explicitely in the term's manual the following sections are automaticaly created and populated for you:

The various doc documentation strings specified by the term's subterms and additional metadata get inserted at the end of the documentation section name docs they respectively mention, in the following order:

If a docs section name is mentioned and does not exist in the term's manual, an empty section is created for it, after which the doc strings are inserted, possibly prefixed by boilerplate text (e.g. for Cmdliner.Manpage.s_environment and Cmdliner.Manpage.s_exit_status ).

If the created section is:

standard, it is inserted at the right place in the order specified here, but after a possible non-standard section explicitely specified by the term since the latter get the order number of the last previously specified standard section or the order of Cmdliner . Manpage .s_synopsis if there is no such section.

if there is no such section. non-standard, it is inserted before the Cmdliner . Manpage .s_commands section or the first subsequent existing standard section if it doesn't exist. Taking advantage of this behaviour is discouraged, you should declare manually your non standard section in the term's manual.

Ideally all manual strings should be UTF-8 encoded. However at the moment macOS (until at least 10.12) is stuck with groff 1.19.2 which doesn't support `preconv(1)`. 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 is 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 --cmdliner is reserved by the library.

is reserved by the library. The option name --help , (and --version if you specify a version string) is reserved by the library. Using it as a term or option name may result in undefined behaviour.

, (and if you specify a version string) is reserved by the library. Using it as a term or option name may result in undefined behaviour. Defining the same option or command name via two different arguments or terms is illegal and raises Invalid_argument .

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

Short flags can be grouped together to share a single dash and the group can end with a short option. For example assuming "-v" and "-x" are flags and "-f" is a short option:

"-vx" will be parsed as "-v -x" .

will be parsed as . "-vxfopt" will be parsed as "-v -x -fopt" .

will be parsed as . "-vxf opt" will be parsed as "-v -x -fopt" .

will be parsed as . "-fvx" will be parsed as "-f=vx" .

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.

Environment variables

Non-required command line arguments can be backed up by an environment variable. If the argument is absent from the command line and that the environment variable is defined, its value is parsed using the argument converter and defines the value of the argument.

For Cmdliner.Arg.flag and Cmdliner.Arg.flag_all that do not have an argument converter a boolean is parsed from the lowercased variable value as follows:

"" , "false" , "no" , "n" or "0" is false .

, , , or is . "true" , "yes" , "y" or "1" is true .

, , or is . Any other string is an error.

Note that environment variables are not supported for Cmdliner.Arg.vflag and Cmdliner.Arg.vflag_all .

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 occurrences 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 occurrence the last value of the default list [Always] is taken, i.e. the default is Always ).

type prompt = Always | Once | Never let prompt_str = function | Always -> "always" | Once -> "once" | Never -> "never" let rm prompt recurse files = Printf .printf "prompt = %s

recurse = %B

files = %s

" (prompt_str prompt) recurse ( String .concat ", " files) 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 Manpage .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 Manpage .s_bugs; ` P "Report bugs to <hehey at example.org>." ; ` S Manpage .s_see_also; ` P "$(b,rmdir)(1), $(b,unlink)(2)" ] in Term .(const rm $ prompt $ recursive $ files), Term .info "rm" ~version: "v1.0.4" ~doc ~exits: Term .default_exits ~man let () = Term .(exit @@ eval cmd)

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.

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

recurse = %B

force = %B

srcs = %s

dest = %s

" verbose recurse force ( String .concat ", " srcs) dest) 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_xrefs = [ ` Tool "mv" ; ` Tool "scp" ; ` Page (2, "umask" ); ` Page (7, "symlink" ) ] in let exits = Term .default_exits in let man = [ ` S Manpage .s_bugs; ` P "Email them to <hehey at example.org>." ; ] in Term .(ret (const cp $ verbose $ recurse $ force $ srcs $ dest)), Term .info "cp" ~version: "v1.0.4" ~doc ~exits ~man ~man_xrefs let () = Term .(exit @@ eval cmd)

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.

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

follow = %s

verb = %s

pid = %s

files = %s

" (loc_str lines) (opt_str follow_str follow) (verb_str verb) (opt_str string_of_int pid) ( String .concat ", " files) 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 ( ` Msg "unable to parse integer" ) in let print ppf p = Format .fprintf ppf "%s" (loc_str p) in Arg .conv ~docv: "N" (parse, print) 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 Manpage .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 Manpage .s_bugs; ` P "Report them to <hehey at example.org>." ; ` S Manpage .s_see_also; ` P "$(b,cat)(1), $(b,head)(1)" ] in Term .(const tail $ lines $ follow $ verb $ pid $ files), Term .info "tail" ~version: "%‌%VERSION%%" ~doc ~exits: Term .default_exits ~man let () = Term .(exit @@ eval cmd)

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 appropriate 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 default_cmd term.