This commit is contained in:
swrup 2025-11-11 02:07:51 +01:00
parent aa2ff7b2f0
commit 2f3113f55d
11742 changed files with 1223940 additions and 0 deletions

View file

@ -0,0 +1,484 @@
{0:cmdline Command line interface}
This manual describes how your tool ends up interacting
with shells when you use Cmdliner.
{1:invocation Tool invocation}
For tools evaluating a command without subcommands the most general
form of invocation is:
{v
tool [OPTION]… [ARG]…
v}
The tool automatically reponds to the [--help] option by printing
{{!help}the help}. If a version string is provided in the
{{!Cmdliner.Cmd.val-info}command information}, it also automatically
responds to the [--version] option by printing this string on standard
output.
Command line arguments are either {{!optargs}{e optional}} or
{{!posargs}{e positional}}. Both can be freely interleaved but since
[Cmdliner] accepts many optional forms this may result in
ambiguities. The special {{!posargs} token [--]} can be used to
resolve them: anything that follows it is treated as a positional
argument.
Tools evaluating commands with subcommands have this form of invocation
{v
tool [COMMAND]… [OPTION]… [ARG]…
v}
Commands automatically respond to the [--help] option by printing
{{!help}their help}. The sequence of [COMMAND] strings must be the first
strings following the tool name as soon as an optional argument is
seen the search for a subcommand stops.
{1:args Arguments}
{2:optargs Optional arguments}
An optional argument is specified on the command line by a {e name}
possibly followed by a {e value}.
The name of an option can be short or long.
{ul
{- A {e short} name is a dash followed by a single alphanumeric
character: [-h], [-q], [-I].}
{- A {e 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.
The value of an option can be specified in three different ways.
{ul
{- 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 {e 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:
{ul
{- [-vx] will be parsed as [-v -x].}
{- [-vxfopt] will be parsed as [-v -x -fopt].}
{- [-vxf opt] will be parsed as [-v -x -fopt].}
{- [-fvx] will be parsed as [-f=vx].}}
{2:posargs 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:
{v
tool --option -- but --now we -are --all positional --argu=ments
v}
{2:constraints Constraints on option names}
Using the cmdliner library puts the following constraints on your
command line interface:
{ul
{- The option names [--cmdliner] and [--__complete] are 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.}
{- Defining the same option or command name via two different
arguments or terms is illegal and raises [Invalid_argument].}}
{1:envlookup 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
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:
{ul
{- [""], ["false"], ["no"], ["n"] or ["0"] is [false].}
{- ["true"], ["yes"], ["y"] or ["1"] is [true].}
{- Any other string is an error.}}
Note that environment variables are not supported for
{!Cmdliner.Arg.vflag} and {!Cmdliner.Arg.vflag_all}.
{1:help Help and man pages}
Help and man pages are are generated when you call your tool or a subcommand
with [--help]. By default, if the [TERM] environment variable
is not [dumb] or unset, the tool tries to {{!paging}page} the manual
so that you can directly search it. Otherwise it outputs the manual
as plain text.
Alternative help formats can be specified with the optional argument
of [--help], see your own [tool --help] for more information.
{@sh[
tool --help
tool cmd --help
tool --help=groff > tool.1
]}
{2:paging Paging}
The pager is selected by looking up, in order:
{ol
{- The [MANPAGER] variable.}
{- The [PAGER] variable.}
{- The tool [less].}
{- The tool [more].}}
Regardless of the pager, it is invoked with [LESS=FRX] set in the
environment unless, the [LESS] environment variable is set in your
environment.
{2:install_tool_manpages Install}
The manpages of a tool and its subcommands can be installed to a root
[man] directory [$MANDIR] by invoking:
{@shell[
cmdliner install tool-manpages thetool $MANDIR
]}
This looks up [thetool] in the [PATH]. Use an explicit file path like
[./thetool] to directly specify an executable.
If you are also {{!install_tool_completion}installing completions}
rather use the [install tool-support] command, see this
{{!page-cookbook.tip_tool_support}cookbook tip} which also has
instructions on how to install if you are using [opam].
{1:cli_completion Command line completion}
Cmdliner programs automatically get support for shell command line
completion.
The completion process happens via a {{!completion_protocol}protocol}
which is interpreted by generic shell completion scripts that are
installed by the library. For now the [zsh] and [bash] shells are
supported.
Tool developers can easily {{!install_tool_completion}install}
completion definitions that invoke these completion scripts. Tool
end-users need to {{!user_configuration}make sure} these definitions are
looked up by their shell.
{2:user_configuration End-user configuration}
If you are the user of a cmdliner based tool, the following
shell-dependent steps need to be performed in order to benefit from
command line completion.
{3:user_zsh For [zsh]}
The [FPATH] environment variable must be setup to include the
directory where the generic cmdliner completion function is
{{!install_completion} installed} {b before} properly initializing the
completion system.
For example, {{:https://github.com/ocaml/opam/issues/6427}for now}, if
you are using [opam]. You should add something like this to your
[.zshrc]:
{@sh[
FPATH="$(opam var share)/zsh/site-functions:${FPATH}"
autoload -Uz compinit
compinit -u
]}
Also make sure this {b happens before} [opam]'s [zsh] init script
inclusion, see {{:https://github.com/ocaml/opam/issues/6428}this
issue}. Note that these instruction do not react dynamically
to [opam] switches changes so you may see odd completion behaviours
when you do so, see this {{:https://github.com/ocaml/opam/issues/6427}this
opam issue}.
After this, to test everything is right, check that the [_cmdliner_generic]
function can be looked by invoking it (this will result in an error).
{@sh[
> autoload _cmdliner_generic
> _cmdliner_generic
_cmdliner_generic:1: words: assignment to invalid subscript range
]}
If the function cannnot be found make sure the [cmdliner] library is
installed, that the generic scripts were
{{!install_generic_completion}installed} and that the
[_cmdliner_generic] file can be found in one of the directories
mentioned in the [FPATH] variable.
With this setup, if you are using a cmdliner based tool named
[thetool] that did not {{!install_tool_completion}install} a completion
definition. You can always do it yourself by invoking:
{@sh[
autoload _cmdliner_generic
compdef _cmdliner_generic thetool
]}
{3:user_bash For [bash]}
These instructions assume that you have
{{:https://repology.org/project/bash-completion/versions}[bash-completion]}
installed and setup in some way in your [.bashrc].
The [XDG_DATA_DIRS] environment variable must be setup to include the
[share] directory where the generic cmdliner completion function is
{{!install_completion}installed}.
For example, {{:https://github.com/ocaml/opam/issues/6427}for now}, if
you are using [opam]. You should add something like this to your
[.bashrc]:
{@sh[
XDG_DATA_DIRS="$(opam var share):${XDG_DATA_DIRS}"
]}
Note that these instruction do not react dynamically to [opam]
switches changes so you may see odd completion behaviours when you do
so, see this {{:https://github.com/ocaml/opam/issues/6427}this opam
issue}.
After this, to test everything is right, check that the [_cmdliner_generic]
function can be looked up:
{@sh[
> _completion_loader _cmdliner_generic
> declare -F _cmdliner_generic &>/dev/null && echo "Found" || echo "Not found"
Found!
]}
If the function cannot be found make sure the [cmdliner] library is
installed, that the generic scripts were
{{!install_generic_completion}installed} and that the
[_cmdliner_generic] file can be looked up by [_completion_loader].
With this setup, if you are using a cmdliner based tool named
[thetool] that did not {{!install_tool_completion}install} a completion
definition. You can always do it yourself by invoking:
{@sh[
_completion_loader _cmdliner_generic
complete -F _cmdliner_generic thetool
]}
{b Note.} {{:https://github.com/scop/bash-completion/commit/9efc596735c4509001178f0cf28e02f66d1f7703}It seems} [_completion_loader] was deprecated in
bash-completion [2.12] in favour of [_comp_load] but many distributions
are on [< 2.12] and in [2.12] [_completion_loader] simply calls
[_comp_load].
{2:install_completion Install}
Completion scripts need to be installed in subdirectories of a
{{:https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch04s11.html}[share]}
directory which we denote by the [$SHAREDIR] variable below. In a
package installation script this variable is typically defined by:
{@sh[
SHAREDIR="$DESTDIR/$PREFIX/share"
]}
The final destination directory in [share] depends on the shell:
{ul
{- For [zsh] it is [$SHAREDIR/zsh/site-functions]}
{- For [bash] it is [$SHAREDIR/bash-completion/completions]}}
If that is unsatisfying you can output the completion scripts directly
where you want with the [cmdliner generic-completion] and
[cmdliner tool-completion] commands.
{3:install_generic_completion Generic completion scripts}
The generic completion scripts must be installed by the
[cmdliner] library. They should not be part of your tool install. If
they are not installed you can inspect and install them with the
following invocations, invoke with [--help] for more information.
{@sh[
cmdliner generic-completion zsh # Output generic zsh script on stdout
cmdliner install generic-completion $SHAREDIR # All shells
cmdliner install generic-completion --shell zsh $SHAREDIR # Only zsh
]}
Directories are created as needed. Use option [--dry-run] to see which
paths would be written by an [install] invocation.
{3:install_tool_completion Tool completion scripts}
If your tool named [thetool] uses Cmdliner you should install completion
definitions for them. They rely on the {{!install_generic_completion}generic
scripts} to be installed. These tool specific scripts can be inspected
and installed via these invocations:
{@sh[
cmdliner tool-completion zsh thetool # Output tool zsh script on stdout.
cmdliner install tool-completion thetool $SHAREDIR # All shells
cmdliner install tool-completion --shell zsh thetool $SHAREDIR # Only zsh
]}
Directories are created as needed. Use option [--dry-run] to see which
paths would be written by an [install] invocation.
If you are also {{!install_tool_manpages}installing manpages} rather
use the [install tool-support] command, see this
{{!page-cookbook.tip_tool_support}cookbook tip} which also has
instructions on how to install if you are using [opam].
{2:completion_protocol Completion protocol}
There is no standard that allows tools and shells to interact to
perform shell command line completion. Completion is supposed to
happen through idiosyncratic, ad-hoc, obscure and brain damaging
shell-specific completion scripts.
To alleviate this, Cmdliner defines one generic script per shell and
interacts with it using the protocol described below. The protocol can
be used to implement generic completion scripts for other shells. The
protocol is versioned but can change even between minor versions of
Cmdliner. Generic scripts for popular shells can be inspected via
the [cmdliner generic-completion] command.
The protocol betwen the shell completion {e script} and a
cmdliner based {e tool} is as follows:
{ol
{- When completion is requested the script invokes the tool with a
modified command line:
{ul
{- The first argument to the tool ([Sys.argv.(1)]) must be the
option [--__complete].}
{- The (possibly empty) argument [ARG] on which the completion is
requested must be replaced by {e exactly} [--__complete=ARG]. Note
that this can happen after the [--] token, this is the reason
why we have an explicit [--__complete] argument in [Sys.argv.(1)]:
it indicates the command line parser must operate in a special mode.}}}
{- The tool responds by writing on standard output a list of
completion directives which match the [completions] rule of the grammar
given below.}
{- The script interprets the completion directives according
to the given semantics below so that the shell can display the
completions. The script is free to ignore directives
or data that it is unable to present.}}
The following ABNF grammar is described using the notations of
{{:https://www.rfc-editor.org/rfc/rfc5234}RFC 5234} and
{{:https://www.rfc-editor.org/rfc/rfc7405}RFC 7405}. A few constraints
are not expressed by the grammar:
{ul
{- Except in the [completion] rule, the byte stream may contain ANSI escape
sequences introduced by the byte [0x1B].}
{- After stripping the ANSI escape sequences, the resulting byte stream must
be valid UTF-8 text.}}
{@abnf[
completions = version nl directives
version = "1"
directives = *(directive nl)
directive = message / group / %s"files" / %s"dirs" / %"restart"
message = %s"message" nl text nl %s"message-end"
group = %s"group" nl group_name nl *item
group_name = *pchar
item = %s"item" nl completion nl item_doc nl %s"item-end"
completion = *pchar
item_doc = text
text = *(pchar / nl)
nl = %0A
pchar = %20-%7E / %8A-%FF
]}
The semantics of directives is as follows:
{ul
{- A [message] directive defines a message to be reported to the user.
It is multi-line ANSI styled text which cannot have a line that is
exactly made of the text [message-end] as it is used to signal the
end of the message. Messages should be reported in the order they
are received.}
{- A [group] directive defines an informational [group_name] followed
by a possibly empty list of completion items that are part of the
group. An item provides a [completion] value, this is a string that
defines what the requested [ARG] value can be replaced with. It is
followed by an [item_doc], multi-line ANSI styled text which cannot
have a line that is exactly made of the text [item-end] as it is
used to signal the end of the item.}
{- A [file] directive indicates that the script should add existing
files staring with [ARG] to completion values.}
{- A [dir] directive indicates that the script should add existing
directories starting with [ARG] to completion values.}
{- A [restart] directive indicates that the script should restart
shell completion as if the command line was starting after the leftmost
[--] disambiguation token. The directive never gets emited if
there is no [--] on the command line.}}
You can easily inspect the completions of any cmdliner based tool by
invoking it like the protocol suggests. For example for the [cmdliner]
tool itself:
{@shell[
cmdliner --__complete --__complete=
]}
{1:error_message_styling Error message ANSI styling}
Since Cmdliner 2.0 error messages printed on [stderr] use styled text
with ANSI escapes unless one of the following conditions is met:
{ul
{- The [NO_COLOR] environment variable is set and different
from the empty string. Yes, even if you have [NO_COLOR=false], that's
what the particularly dumb {:https://no-color.org} standard says.}
{- The [TERM] environment variable is [dumb].}
{- The [TERM] environment variable is unset and {!Sys.backend_type} is
not [Other "js_of_ocaml"]. Yes, browser consoles support
ANSI escapes. Yes, you can run Cmdliner in your browser.}}
{1:legacy_prefix_specification Legacy prefix specification}
Before Cmdliner 2.0, command names, long option names and
{!Cmdliner.Arg.enum} values could be specified by a prefix as long as
the prefix was not ambiguous.
This turned out to be a mistake. It makes the user experience of the
tool unstable as it evolves: former user established shortcuts or
invocations in scripts may be broken by new command, option and
enumerant additions.
Therefore this behaviour was unconditionally removed in Cmdliner
2.0. If you happen to have scripts that rely on it, you can invoke
them with [CMDLINER_LEGACY_PREFIXES=true] set in the environment to
recover the old behaviour. {b However the scripts should be fixed: this
escape hatch will be removed in the future.}
The [CMDLINER_LEGACY_PREFIX=true] escape hatch should not be used for
interactive tool interaction. In particular the behaviour of Cmdliner
completion support under this setting is undefined.

View file

@ -0,0 +1,720 @@
{0 [Cmdliner] cookbook}
A few recipes and starting {{!blueprints}blueprints} to describe your
command lines with {!Cmdliner}.
{b Note.} Some of the code snippets here assume they are done after:
{[
open Cmdliner
open Cmdliner.Term.Syntax
]}
{1:tips Tips and pitfalls}
Command line interfaces are a rather crude and inexpressive user
interaction medium. It is tempting to try to be nice to users in
various ways but this often backfires in confusing context sensitive
behaviours. Here are a few tips and Cmdliner features you {b should
rather not use}.
{2:tip_avoid_default_command Avoid default commands in groups}
Command {{!Cmdliner.Cmd.group}groups} can have a default command, that
is be of the form [tool [CMD]]. Except perhaps at the top level of
your tool, it's better to avoid them. They increase command line
parsing ambiguities.
In particular if the default command has positional arguments, users
are forced to use the {{!cli.posargs}disambiguation token [--]} to
specify them so that they can be distinguished from command
names. For example:
{@sh[
tool -- file …
]}
One thing that is acceptable is to have a default command that simply
{{!cmds_show_docs}shows documentation} for the group of subcommands as
this not interfere with tool operation.
{2:tip_avoid_default_option_values Avoid default option values}
Optional arguments {{!Cmdliner.Arg.opt}with values} can have a default
value, that is be of the form [--opt[=VALUE]]. In general it is better
to avoid them as they lead to context sensitive command lines
specifications and surprises when users refine invocations. For examples
suppose you have the synopsis
{@sh[
tool --opt[=VALUE] [FILE]
]}
Trying to refine the following invocation to add a [FILE] parameter is
error prone and painful:
{@sh[
tool --opt
]}
There is more than one way but the easiest way is to specify:
{@sh[
tool --opt -- FILE
]}
which is not obvious unless you have [tool]'s cli hard wired in your
brain. This would have been a careless refinement if [--opt] did not
have a default option value.
{2:tip_avoid_required_opt Avoid required optional arguments}
Cmdliner allows to define required optional arguments. Avoid doing
this, it's a contradiction in the terms. In command line interfaces
optional arguments are defined to be… optional, not doing so is
surprising for your users. Use required positional arguments if
arguments are required by your command invocation.
Required optional arguments can be useful though if your tool is not
meant to be invoked manually but rather through scripts and has many
required arguments. In this case they become a form of labelled
arguments which can make invocations easier to understand.
{2:tip_avoid_manpages Avoid making manpages your main documentation}
Unless your tool is very simple, avoid making manpages the main
documentation medium of your tool. The medium is rather limited and
even though you can convert them to HTML, its cross references
capabilities are rather limited which makes discussing your tool
online more difficult.
Keep information in manpages to the minimum needed to operate your
tool without having to leave the terminal too much and defer reference
manuals, conceptual information and tutorials to a more evolved medium
like HTML.
{2:tip_migrating Migrating from other conventions}
If you are porting your command line parsing to [Cmdliner] and that
you have conventions that clash with [Cmdliner]'s ones but you need to
preserve backward compatibility, one way of proceeding is to
pre-process {!Sys.argv} into a new array of the right shape before
giving it to command {{!Cmdliner.Cmd.section-eval}evaluation
functions} via the [?argv] optional argument.
These are two common cases:
{ul
{- Long option names with a single dash like [-warn-error]. In this
case simply prefix an additional [-] to these arguments when they
occur in {!Sys.argv} before the [--] argument; after it, all arguments are
positional and to be treated literally.}
{- Long option names with a single letter like [--X]. In this
case simply chop the first [-] to make it a short option when they
occur in {!Sys.argv} before the [--] argument; after it all arguments are
positional and to be treated literally.}}
{2:tip_src_structure Source code structure}
In general Cmdliner wants you to see your tools as regular OCaml functions
that you make available to the shell. This means adopting the following
source structure:
{[
(* Implementation of your command. Except for exit codes does not deal with
command line interface related matters and is independent from
Cmdliner. *)
let exit_ok = 0
let tool … = …; exit_ok
(* Command line interface. Adds metadata to your [tool] function arguments
so that they can be parsed from the command line and documented. *)
open Cmdliner
open Cmdliner.Term.Syntax
let cmd = … (* Has a term that invokes [tool] *)
let main () = Cmd.eval' cmd
let () = if !Sys.interactive then () else exit (main ())
]}
In particular it is good for your readers' understanding that your
program has a single point where it {!Stdlib.exit}s. This structure is
also useful for playing with your program in the OCaml toplevel
(REPL), you can invoke its [main] function without having the risk of it
[exit]ing the toplevel.
If your tool named [tool] is growing into multiple commands which
have a lot of definitions it is advised to:
{ul
{- Gather command line definition commonalities such as argument
converters or common options in a module called [Tool_cli].}
{- Define each command named [name] in a separate module [Cmd_name] which
exports its command as a [val cmd : int Cmd.t] value.}
{- Gather the commands with {!Cmdliner.Cmd.group} in a source file
called [tool_main.ml].}}
For an hypothetic tool named [tool] with commands [import], [serve]
and [user], this leads to the following set of files:
{[
cmd_import.ml cmd_serve.ml cmd_user.ml tool_cli.ml tool_main.ml
cmd_import.mli cmd_serve.mli cmd_user.mli tool_cli.mli
]}
The [.mli] files simply export commands:
{[
val cmd : int Cmdliner.Cmd.t
]}
And the [tool_main.ml] gathers them with a {!Cmdliner.Cmd.group}:
{[
let cmd =
let default = Term.(ret (const (`Help (`Auto, None)))) (* show help *) in
Cmd.group (Cmd.info "tool") ~default @@
[Cmd_import.cmd; Cmd_serve.cmd; Cmd_user.cmd]
let main () = Cmd.value' cmd
let () = if !Sys.interactive then () else exit (main ())
]}
{2:tip_tool_support Installing completions and manpages}
The [cmdliner] tool can be used to install completion scripts and
manpages for you tool and its subcommands by using the dedicated
{{!page-cli.install_tool_completion}[install tool-completion]} and
{{!page-cli.install_tool_manpages}[install tool-manpages]} subcommands.
To install both directly (and possibly other support files in the future)
it is more concise to use the [install
tool-support] command. Invoke with [--help] for more information.
{3:tip_tool_support_with_opam With [opam]}
If you are installing your package with [opam] for a tool named [tool]
located in the build at the path [$BUILD/tool], you can add the following
instruction after your build instructions in the [build:] field of
your [opam] file (also works if your build system is not using a
[.install] file).
{@sh[
build: [
[ … ] # Your regular build instructions
["cmdliner" "install" "tool-support"
"--update-opam-install=%{_:name}%.install"
"$BUILD/tool" "_build/cmdliner-install"]]
]}
You need to specify the path to the built executable, as it cannot be
looked up in the [PATH] yet. Also more than one tool can be specified
in a single invocation and there is a syntax for specifying the actual
tool name if it is renamed on install; see [--help] for more
details.
If [cmdliner] is only an optional dependency of your package use the
opam filter [{cmdliner:installed}] after the closing bracket of the command
invocation.
{3:tip_tool_support_with_opam_dune With [opam] and [dune]}
First make sure your understand the
{{!tip_tool_support_with_opam}above basic instructions} for [opam].
You then
{{:https://dune.readthedocs.io/en/stable/reference/packages.html#generating-opam-files}need to figure out} how to add the [cmdliner install] instruction to the [build:]
field of the opam file after your [dune] build instructions. For a tool named
[tool] the result should eventually look this:
{@sh[
build: [
[ … ] # Your regular dune build instructions
["cmdliner" "install" "tool-support"
"--update-opam-install=%{_:name}%.install"
"_build/default/install/bin/tool" {os != "win32"}
"_build/default/install/bin/tool.exe" {os = "win32"}
"_build/cmdliner-install"]]
]}
{1:conventions Conventions}
By simply using Cmdliner you are already abiding to a great deal
of command line interface conventions. Here are a few other ones that
are not necessarily enforced by the library but that are good to
adopt for your users.
{2:conv_use_dash Use ["-"] to specify [stdio] in file path arguments}
Whenever a command line argument specifies a file path to read or
write you should let the user specify [-] to denote standard in or
standard out, if possible. If you worry about a file sporting this
name, note that the user can always specify it using [./-] for
the argument.
Very often tools default to [stdin] or [stdout] when a file
input or output is unspecified, here is typical argument definitions
to support these conventions:
{[
let infile =
let doc = "$(docv) is the file to read from. Use $(b,-) for $(b,stdin)" in
Arg.(value & opt string "-" & info ["i", "input-file"] ~doc ~docv:"FILE")
let outfile =
let doc = "$(docv) is the file to write to. Use $(b,-) for $(b,stdout)" in
Arg.(value & opt string "-" & info ["o", "output-file"] ~doc ~docv:"FILE")
]}
Here is {!Stdlib} based code to read to a string a file or standard
input if [-] is specified:
{[
let read_file file =
let read file ic = try Ok (In_channel.input_all ic) with
| Sys_error e -> Error (Printf.sprintf "%s: %s" file e)
in
let binary_stdin () = In_channel.set_binary_mode In_channel.stdin true in
try match file with
| "-" -> binary_stdin (); read file In_channel.stdin
| file -> In_channel.with_open_bin file (read file)
with Sys_error e -> Error e
]}
Here is {!Stdlib} based code to write a string to a file or standard output
if [-] is specified:
{[
let write_file file s =
let write file s oc = try Ok (Out_channel.output_string oc s) with
| Sys_error e -> Error (Printf.sprintf "%s: %s" file e)
in
let binary_stdout () = Out_channel.(set_binary_mode stdout true) in
try match file with
| "-" -> binary_stdout (); write file s Out_channel.stdout
| file -> Out_channel.with_open_bin file (write file s)
with Sys_error e -> Error e
]}
{2:conv_env_defaults Environment variables as default modifiers}
Cmdliner has support to back values defined by arguments with
environment variables. The value specified via an environment variable
should never take over an argument specified explicitely on the
command line. The environment variable should be seen as providing
the default value when the argument is absent.
This is exactly what Cmdliner's support for environment variables does,
see {!env_args}
{1:args Arguments}
{2:args_positional How do I define a positional argument?}
Positional arguments are extracted from the command line using
{{!Cmdliner.Arg.posargs}these combinators} which use zero-based
indexing. The following example extracts the first argument and if
the argument is absent from the command line it evaluates
to ["Revolt!"].
{[
let msg =
let doc = "$(docv) is the message to utter." and docv = "MSG" in
Arg.(value & pos 0 string "Revolt!" & info [] ~doc ~docv)
]}
{2:args_optional How do I define an optional argument?}
Optional arguments are extracted from the command line using
{{!Cmdliner.Arg.optargs}these combinators}. The actual option
name is defined in the {!Cmdliner.Arg.val-info} structure without
dashes. One character strings define short options, others long
options (see the {{!page-cli.optargs}parsed syntax}).
The following defines the [-l] and [--loud] options. This is a simple
command line argument without a value also known as a command line {e
flag}. The term [loud] evaluates to [false] when the argument is
absent on the command line and [true] otherwise.
{[
let loud =
let doc = "Say the message loudly." in
Arg.(value & flag & info ["l"; "loud"] ~doc)
]}
The following defines the [-m] and [--message] options. The term [msg] evalutes
to ["Revolt!"] when the option is absent on the command line.
{[
let msg =
let doc = "$(docv) is the message to utter." and docv = "MSG" in
Arg.(value & opt string "Revolt!" & info ["m"; "message"] ~doc ~docv)
]}
{2:args_required How do I define a required argument?}
Some of the constraints on the presence of arguments occur when the
specification of arguments is {{!Cmdliner.Arg.argterms}converted} to
terms. The following says that the first positional argument is required:
{[
let msg =
let msg = "$(docv) is the message to utter." and docv = "MSG" in
Arg.(required & pos 0 (some string) None & info [] ~absent ~doc ~docv)
]}
The value [msg] ends up being a term of type [string]. If the argument
is not provided, Cmdliner will automatically bail out during evaluation
with an error message.
Note that while it is possible to define required positional argument
it is {{!tip_avoid_required_opt}discouraged}.
{2:args_detect_absent How can I know if an argument was absent?}
Most {{!Cmdliner.Arg.posargs}positional} and
{{!Cmdliner.Arg.optargs}optional} arguments have a default value. You
can use a [None] for the default argument and the {!Cmdliner.Arg.some} or
{!Cmdliner.Arg.some'} combinators on your argument converter which simply
wrap its result in a [Some].
{[
let msg =
let msg = "$(docv) is the message to utter." in
let absent = "Random quote." in
Arg.(value & pos 0 (some string) None & info [] ~absent ~doc ~docv:"MSG")
]}
There is more than one way to document the value when it is
absent. See {!args_absent_doc}
{2:args_absent_doc How do I document absent argument behaviours?}
There are three ways to document the behaviour when an argument is
unspecified on the command line.
{ul
{- If you specify a default value in the argument combinator, this value
gets printed in bold using the {{!Cmdliner.Arg.conv_printer}printer}
of the converter.}
{- If you are using the {!Cmdliner.Arg.some'} and {!Cmdliner.Arg.some}
there is an optional [none] argument that allows you to specify
the default value. If you can exhibit this value at definition
point use {!Cmdliner.Arg.some'}, the underlying converter's
{{!Cmdliner.Arg.conv_printer}printer} will be used. If not
you can specify it as a string rendered in bold via {!Cmdliner.Arg.some}.}
{- If you want to describe a more complex, but short, behaviour use
the [~absent] parameter of {!Cmdliner.Arg.val-info}. Using this
parameter overrides the two previous ways. See
{{!args_detect_absent}this} example. }}
{2:args_completion How can I customize positional and option value completion?}
Positional argument values and option values are completed according
to the {{!Cmdliner.Arg.argconv}argument converter} you use for defining
the optional or positional argument.
A couple of predefined argument converter like {!Cmdliner.Arg.path},
{!Cmdliner.Arg.filepath} and {!Cmdliner.Arg.dirpath} or
{!Cmdliner.Arg.enum} automatically handle this for you.
If you would like to perform custom or more elaborate context
sensitive completions you can define your own argument converter with
a completion defined with {!Cmdliner.Arg.Completion.make}.
Here is an example where the first positional argument is completed
with the filenames found in a directory specified via the [--dir]
option (which defaults to the current working directory if unspecified).
{[
let dir = Arg.(value & opt dirpath "." & info ["d"; "dir"])
let dir_filenames_conv =
let complete dir ~token = match dir with
| None -> Error "Could not determine directory to lookup"
| Some dir ->
match Array.to_list (Sys.readdir dir) with
| exception Sys_error e -> Error (String.concat ": " [dir; e])
| fnames ->
let fnames = List.filter (String.starts_with ~prefix:token) fnames in
Ok (List.map Arg.Completion.string fnames)
in
let completion = Arg.Completion.make ~context:dir complete in
Arg.Conv.of_conv ~completion Arg.string
let pos0 = Arg.(required & pos 0 (some dir_filenames_conv) None & info [])
]}
Note that when you use [pos0] in a command line definition you also
need to make sure [dir] is part of the term otherwise the context will
always be [None]:
{[
let+ pos0 and+ dir and+ … in …
]}
{1:envs Environment variables}
{2:env_args How can environment variables define defaults?}
As mentioned in {!conv_env_defaults}, any non-required argument can be
defined by an environment variable when absent. This works by
specifying the [env] argument in the argument's {!Cmdliner.Arg.val-info}
information. For example:
{[
let msg =
let doc = "$(docv) is the message to utter." and docv = "MSG" in
let env = Cmd.Env.info "MESSAGE" in
Arg.(value & pos 0 string "Revolt!" & info [] ~env ~doc ~docv)
]}
When the first positional argument is absent it takes the default
value ["Revolt!"], unless the [MESSAGE] variable is defined in
the environment in which case it takes its value.
Cmdliner handles the environment variable lookup for you. By using the
[msg] term in your command definition all this gets automatically
documented in the tool help.
{2:env_cmd How do I document environment variables influencing a command?}
Environment variable that are used to change {{!env_args}argument
defaults} automatically get documented in a command's man page when
you use the argument's term in the command's term.
However if your command implementation looks up other variables and you
wish to document them in the command's man page, use the [envs]
argument of {!Cmdliner.Cmd.val-info} or the [docs_env] argument
of {!Cmdliner.Arg.val-info}.
This documents in the {!Cmdliner.Manpage.s_environment} manual section
of [tool] that [EDITOR] is looked up to find the tool to invoke to
edit the files:
{[
let editor_env = "EDITOR"
let tool … = … Sys.getenv_opt editor_env
let cmd =
let env = Cmd.Env.info editor_env ~doc:"The editor used to edit files." in
Cmd.make (Cmd.info "tool" ~envs:[env]) @@
]}
{1:cmds Commands}
{2:cmds_exit_code_docs How do I document command exit codes?}
Exit codes are documentd by {!Cmdliner.Cmd.Exit.type-info} values and
must be given to the command's {!Cmdliner.Cmd.type-info} value via the
[exits] optional arguments. For example:
{[
let conf_not_found = 1
let tool … =
let tool_cmd =
let exits =
Cmd.Exit.info conf_not_found "if no configuration could be found." ::
Cmd.Exit.defaults
in
Cmd.make (Cmd.info "mycmd" ~exits) @@
]}
{2:cmds_show_docs How do I show help in a command group's default?}
While it is usually {{!tip_avoid_default_command}not advised} to have a default
command in a group, just showing docs is acceptable. A term can request
Cmdliner's generated help by using {!Cmdliner.Term.val-ret}:
{[
let group_cmd =
let default = Term.(ret (const (`Help (`Auto, None)))) (* show help *) in
Cmd.group (Cmd.info "group") ~default @@
[first_cmd; second_cmd]
]}
{2:cmds_which_eval Which [Cmd] evaluation function should I use?}
There are (too) many {{!Cmdliner.Cmd.section-eval}command evaluation}
functions. They have grown organically in a rather ad-hoc manner. Some
of these are there for backwards compatibility reasons and advanced
usage for complex tools.
Here are the main ones to use and why you may want to use them which
essentially depends on how you want to handle errors and exit codes
in your tool function.
{ul
{- {!Cmdliner.Cmd.val-eval}. This forces your tool function to return [()].
The evaluation function always returns an exit code of [0] unless a
command line parsing error occurs.}
{- {!Cmdliner.Cmd.eval'}. {b Recommended}. This forces your tool function to
return an exit code [exit] which is returned by the evaluation function
unless a command line parsing error occurs. This is the recommended
function to use as it forces you to think about how to report errors and
design useful exit codes for users.}
{- {!Cmdliner.Cmd.eval_result} is akin to {!Cmdliner.Cmd.val-eval} except
it forces your function to return either [Ok ()] or [Error msg].
The evaluation function returns with exit code [0] unless [Error msg] is
computed in which case [msg] is printed on the error stream prefixed by the
executable name and the evaluation function returns with
exit code {!Cmdliner.Cmd.Exit.some_error}.}
{- {!Cmdliner.Cmd.eval_result'} is akin to {!Cmdliner.Cmd.eval_result}, except
the [Ok] case carries an exit code which is returned by the evaluation
function.}}
{2:cmds_howto_complete How can my tool support command line completion?}
The command line interface manual has all
{{!page-cli.cli_completion}the details} and
{{!page-cli.install_tool_completion} specific instructions} for
complementing your tool install. See also {!tip_tool_support}.
{2:cmds_listing How can I list all the commands of my tool?}
In a shell the invocation [cmdliner tool-commands $TOOL] lists every
command of the tool $TOOL.
{2:cmds_errmsg_styling How can I suppress error message styling?}
Since Cmdliner 2.0, error message printed on [stderr] use styled text
with ANSI escapes. Styled text is disabled if one of the conditions
mentioned {{!page-cli.error_message_styling}here} is met.
If you want to be more aggressive in suppressing them you can use the
[err] formatter argument of {{!Cmdliner.Cmd.section-eval}command
evaluation} functions with a suitable formatter on which a function
like
{{:https://erratique.ch/software/more/doc/More/Fmt/index.html#val-strip_styles}
this one} has been applied that automatically strips the styling.
{1:manpage Manpages}
{2:manpage_hide How do I prevent an item from being automatically listed?}
In general it's not a good idea to hide stuff from your users but in
case an item needs to be hidden you can use the special
{!Cmdliner.Manpage.s_none} section name. This ensures the item does
not get listed in any section.
{[
let secret = Arg.(value & flag & info ["super-secret"] ~docs:Manpage.s_none)
]}
{2:manpage_synopsis How can I write a better command synopsis section?}
Define the {!Cmdliner.Manpage.s_synopsis} section in the manpage of
your command. It takes over the one generated by Cmdliner. For example:
{[
let man = [
`S Manpage.s_synopsis;
`P "$(cmd) $(b,--) $(i,TOOL) [$(i,ARG)]…"; `Noblank;
`P "$(cmd) $(i,COMMAND) …";
`S Manpage.s_description;
`P "Without a command $(cmd) invokes $(i,TOOL)"; ]
]}
{2:manpage_install How can I install all the manpages of my tool?}
The command line interface manual
{{!page-cli.install_tool_manpages}the details} on how to install the manpages
of your tool and its subcommands. See also {!tip_tool_support}.
{1:blueprints Blueprints}
These blueprints when copied to a [src.ml] file can be compiled and run with:
{@sh[
ocamlfind ocamlopt -package cmdliner -linkgpkg src.ml
./a.out --help
]}
More concrete examples can be found on the {{!page-examples}examples page}
and the {{!page-tutorial}tutorial} may help too.
These examples follow a conventional {!tip_src_structure}.
{2:blueprint_min Minimal}
A minimal example.
{@ocaml name=blueprint_min.ml[
let tool () = Cmdliner.Cmd.Exit.ok
open Cmdliner
open Cmdliner.Term.Syntax
let cmd =
Cmd.make (Cmd.info "TODO" ~version:"v2.0.0+dune") @@
let+ unit = Term.const () in
tool unit
let main () = Cmd.eval' cmd
let () = if !Sys.interactive then () else exit (main ())
]}
{2:blueprint_tool A simple tool}
This is a tool that has a flag, an optional positional argument for
specifying an input file. It also responds to the [--version] option.
{@ocaml name=blueprint_tool.ml[
let exit_todo = 1
let tool ~flag ~infile = exit_todo
open Cmdliner
open Cmdliner.Term.Syntax
let flag = Arg.(value & flag & info ["flag"] ~doc:"The flag")
let infile =
let doc = "$(docv) is the input file. Use $(b,-) for $(b,stdin)." in
Arg.(value & pos 0 string "-" & info [] ~doc ~docv:"FILE")
let cmd =
let doc = "The tool synopsis is TODO" in
let man = [
`S Manpage.s_description;
`P "$(cmd) does TODO" ]
in
let exits =
Cmd.Exit.info exit_todo ~doc:"When there is stuff todo" ::
Cmd.Exit.defaults
in
Cmd.make (Cmd.info "TODO" ~version:"v2.0.0+dune" ~doc ~man ~exits) @@
let+ flag and+ infile in
tool ~flag ~infile
let main () = Cmd.eval' cmd
let () = if !Sys.interactive then () else exit (main ())
]}
{2:blueprint_cmds A tool with subcommands}
This is a tool with two subcommands [hey] and [ho]. If your tools
grows many subcommands you may want to follow these
{{!tip_src_structure}source code conventions}.
{@ocaml name=blueprint_cmds.ml[
let hey () = Cmdliner.Cmd.Exit.ok
let ho () = Cmdliner.Cmd.Exit.ok
open Cmdliner
open Cmdliner.Term.Syntax
let flag = Arg.(value & flag & info ["flag"] ~doc:"The flag")
let infile =
let doc = "$(docv) is the input file. Use $(b,-) for $(b,stdin)." in
Arg.(value & pos 0 file "-" & info [] ~doc ~docv:"FILE")
let hey_cmd =
let doc = "The hey command synopsis is TODO" in
Cmd.make (Cmd.info "hey" ~doc) @@
let+ unit = Term.const () in
ho ()
let ho_cmd =
let doc = "The ho command synopsis is TODO" in
Cmd.make (Cmd.info "ho" ~doc) @@
let+ unit = Term.const () in
ho unit
let cmd =
let doc = "The tool synopsis is TODO" in
Cmd.group (Cmd.info "TODO" ~version:"v2.0.0+dune" ~doc) @@
[hey_cmd; ho_cmd]
let main () = Cmd.eval' cmd
let () = if !Sys.interactive then () else exit (main ())
]}

View file

@ -0,0 +1,453 @@
{0 Examples}
The examples are self-contained, cut and paste them in a file to play
with them. See also the suggested {{!page-cookbook.tip_src_structure}source
code structure} and program {{!page-cookbook.blueprints}blueprints}.
{1:example_rm A [rm] command}
We define the command line interface of an [rm] command with the
synopsis:
{v
rm [OPTION]… FILE…
v}
The [-f], [-i] and [-I] flags define the prompt behaviour of [rm]. It
is 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 is no occurrence the last value of the default list
[[Always]] is taken. This means the default prompt behaviour is [Always].
{@ocaml name=example_rm.ml[
(* 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
open Cmdliner.Term.Syntax
let files = Arg.(non_empty & pos_all file [] & info [] ~docv:"FILE")
let prompt =
let always =
let doc = "Prompt before every removal." in
Always, Arg.info ["i"] ~doc
in
let never =
let doc = "Ignore nonexistent files and never prompt." in
Never, Arg.info ["f"; "force"] ~doc
in
let once =
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
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 rm_cmd =
let doc = "Remove files or directories" in
let man = [
`S Manpage.s_description;
`P "$(cmd) 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 $(b,-), for example
$(b,-foo), use one of these commands:";
`Pre "$(cmd) $(b,-- -foo)"; `Noblank;
`Pre "$(cmd) $(b,./-foo)";
`P "$(cmd.name) removes symbolic links, not the files referenced by the
links.";
`S Manpage.s_bugs; `P "Report bugs to <bugs@example.org>.";
`S Manpage.s_see_also; `P "$(b,rmdir)(1), $(b,unlink)(2)" ]
in
Cmd.make (Cmd.info "rm" ~version:"v2.0.0+dune" ~doc ~man) @@
let+ prompt and+ recursive and+ files in
rm ~prompt ~recurse:recursive files
let main () = Cmd.eval rm_cmd
let () = if !Sys.interactive then () else exit (main ())
]}
{1:example_cp A [cp] command}
We define the command line interface of a [cp] command with the synopsis:
{v
cp [OPTION]… SOURCE… DEST
v}
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 [DEST] the {!Cmdliner.Arg.string} type and verify
the constraint at the beginning of the implementation of [cp]. If the
constraint is unsatisfied we return an [`Error] result. By using
{!Cmdliner.Term.val-ret} on the command's term for [cp], [Cmdliner]
handles the error reporting.
{@ocaml name=example_cp.ml[
(* Implementation, we check the dest argument and print the args *)
let cp ~verbose ~recurse ~force srcs dest =
let many = List.length srcs > 1 in
if many && (not (Sys.file_exists dest) || not (Sys.is_directory dest))
then `Error (false, dest ^ ": 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
open Cmdliner.Term.Syntax
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
let docv = "DEST" in
Arg.(required & pos ~rev:true 0 (some string) None & info [] ~docv ~doc)
let cp_cmd =
let doc = "Copy files" in
let man_xrefs =
[`Tool "mv"; `Tool "scp"; `Page ("umask", 2); `Page ("symlink", 7)]
in
let man = [
`S Manpage.s_bugs;
`P "Email them to <bugs@example.org>."; ]
in
Cmd.make (Cmd.info "cp" ~version:"v2.0.0+dune" ~doc ~man ~man_xrefs) @@
Term.ret @@
let+ verbose and+ recurse and+ force and+ srcs and+ dest in
cp ~verbose ~recurse ~force srcs dest
let main () = Cmd.eval cp_cmd
let () = if !Sys.interactive then () else exit (main ())
]}
{1:example_tail A [tail] command}
We define the command line interface of a [tail] command with the
synopsis:
{v
tail [OPTION]… [FILE]…
v}
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_arg]
{{!Cmdliner.Arg.type-conv}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.
{@ocaml name=example_tail.ml[
(* 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
open Cmdliner.Term.Syntax
let loc_arg =
let parser 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
let pp ppf p = Format.fprintf ppf "%s" (loc_str p) in
Arg.Conv.make ~docv:"N" ~parser ~pp ()
let lines =
let doc = "Output the last $(docv) lines or use $(i,+)$(docv) to start \
output after the $(i,N)-1th line."
in
Arg.(value & opt loc_arg (true, 10) & info ["n"; "lines"] ~docv:"N" ~doc)
let follow =
let doc = "Output appended data as the file grows. $(docv) specifies how \
the file should be tracked, by its $(b,name) or by its \
$(b,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 quiet =
let doc = "Never output headers giving file names." in
Quiet, Arg.info ["q"; "quiet"; "silent"] ~doc
in
let verbose =
let doc = "Always output headers giving file names." in
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 tail_cmd =
let doc = "Display the last part of a file" in
let man = [
`S Manpage.s_description;
`P "$(cmd) 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 <bugs@example.org>.";
`S Manpage.s_see_also;
`P "$(b,cat)(1), $(b,head)(1)" ]
in
Cmd.make (Cmd.info "tail" ~version:"v2.0.0+dune" ~doc ~man) @@
let+ lines and+ follow and+ verb and+ pid and+ files in
tail ~lines ~follow ~verb ~pid files
let main () = Cmd.eval tail_cmd
let () = if !Sys.interactive then () else exit (main ())
]}
{1:example_darcs A [darcs] command}
We define the command line interface of a [darcs] command with the
synopsis:
{v
darcs [COMMAND] …
v}
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 the section
{!Cmdliner.Manpage.s_common_options}.
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.val-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] term.
{@ocaml name=example_darcs.ml[
(* 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
let parse = Cmdliner.Arg.Conv.parser conv in
match parse 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
open Cmdliner.Term.Syntax
(* Help sections common to all commands *)
let help_secs = [
`S Manpage.s_common_options;
`P "These options are common to all commands.";
`S "MORE HELP";
`P "Use $(tool) $(i,COMMAND) --help for help on a single command.";`Noblank;
`P "Use $(tool) $(b,help patterns) for help on patch matching."; `Noblank;
`P "Use $(tool) $(b,help environment) for help on environment variables.";
`S Manpage.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 = Manpage.s_common_options 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 $(tool) command." in
Arg.(value & opt (some string) None & info ["prehook"] ~docs ~doc)
in
Term.(const copts $ debug $ verb $ prehook)
(* Commands *)
let sdocs = Manpage.s_common_options
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 Manpage.s_description;
`P "Turns the current directory into a Darcs repository. Any
existing files and subdirectories become …";
`Blocks help_secs; ]
in
Cmd.make (Cmd.info "initialize" ~doc ~sdocs ~man) @@
let+ copts_t and+ repodir in
initialize copts_t repodir
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 Manpage.s_description;
`P "Creates a patch from changes in the working tree. If you specify
a set of files…";
`Blocks help_secs; ]
in
Cmd.make (Cmd.info "record" ~doc ~sdocs ~man) @@
let+ copts_t and+ pname and+ author and+ all and+ ask_deps and+ files in
record copts_t pname author all ask_deps files
let help_cmd =
let topic =
let doc = "The topic to get help on. $(b,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 Manpage.s_description;
`P "Prints help about darcs commands and other subjects…";
`Blocks help_secs; ]
in
Cmd.make (Cmd.info "help" ~doc ~man) @@
Term.ret @@
let+ copts_t and+ man_format = Arg.man_format
and+ choice_names = Term.choice_names and+ topic in
help copts_t man_format choice_names topic
let main_cmd =
let doc = "a revision control system" in
let man = help_secs in
let info = Cmd.info "darcs" ~version:"v2.0.0+dune" ~doc ~sdocs ~man in
let default = Term.(ret (const (fun _ -> `Help (`Pager, None)) $ copts_t)) in
Cmd.group info ~default [initialize_cmd; record_cmd; help_cmd]
let main () = Cmd.eval main_cmd
let () = if !Sys.interactive then () else exit (main ())
]}

View file

@ -0,0 +1,46 @@
{0 Cmdliner {%html: <span class="version">v2.0.0+dune</span>%}}
Cmdliner provides a simple and compositional mechanism
to convert command line arguments to OCaml values and pass them to
your functions.
The library automatically handles command line completion, syntax
errors, help messages and UNIX man page generation. It supports
programs with single or multiple commands (like [git]) and respect
most of the
{{:http://www.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap12.html}
POSIX} and
{{:http://www.gnu.org/software/libc/manual/html_node/Argument-Syntax.html}
GNU} conventions.
{1:manuals Manuals}
The following manuals are available.
{ul
{- The {{!page-tutorial}tutorial} makes you write your first command line
interface with Cmdliner.}
{- The {{!page-cookbook}cookbook} has a few off-the-shelf recipes,
tips about {{!page-cookbook.tip_src_structure}source code structure},
and {{!page-cookbook.blueprints}blueprints} to define your command lines
with Cmdliner.}
{- The {{!page-cli}command line interface manual} describes how command
lines and environment variables are parsed by Cmdliner and how command line
completion is performed. This can be communicated to the users of your
tools.}
{- The {{!page-tool_man}tool man page} manual describes how
Cmdliner generates man pages for your tools and their commands and how
you can format them.}
{- The {{!page-examples}examples page} has examples of a some
classic UNIX tools with their command line interface implemented by
Cmdliner.}}
{1:library Library [cmdliner]}
{!modules: Cmdliner}
{!modules:
Cmdliner.Arg
Cmdliner.Cmd
Cmdliner.Manpage
Cmdliner.Term
}

View file

@ -0,0 +1,73 @@
{0:tool_man Tool man pages}
See also the {{!page-cli.help}section} about man pages in the command
line interface manual.
{1:manual Man page generation}
Man page sections for a command are printed in the order specified by
the [man] value given to {!Cmdliner.Cmd.val-info}. Unless
specified explicitly in the [man] value the following sections
are automatically created and populated for you:
{ul
{- {{!Cmdliner.Manpage.s_name}[NAME]} section.}
{- {{!Cmdliner.Manpage.s_synopsis}[SYNOPSIS]} section.}}
The various [doc] documentation strings specified by the command's
term arguments get inserted at the end of the documentation section
they respectively mention in their [docs] argument:
{ol
{- For commands, see {!Cmdliner.Cmd.val-info}.}
{- For positional arguments, see {!Cmdliner.Arg.type-info}. Those are listed iff
both the [docv] and [doc] string is specified by {!Cmdliner.Arg.val-info}.}
{- For optional arguments, see {!Cmdliner.Arg.val-info}.}
{- For exit statuses, see {!Cmdliner.Cmd.Exit.val-info}.}
{- For environment variables, see {!Cmdliner.Cmd.Env.val-info}.}}
If a [docs] section name is mentioned and does not exist in the command's
[man] value, 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:
{ul
{- {{!Cmdliner.Manpage.standard_sections}standard}, it
is inserted at the right place in the order specified
{{!Cmdliner.Manpage.standard_sections}here}, but after a
possible non-standard
section explicitly specified by the command's [man] value 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.}
{- 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 command's
manual page.}}
Finally note that the header of empty sections are dropped from the
output. This allows you to share section placements among many
commands and render them only if something actually gets inserted in
it.
{1:doclang Documentation markup language}
Manpage {{!Cmdliner.Manpage.block}blocks} and the doc strings of the
various [info] values support the following markup language.
{ul
{- Markup directives [$(i,text)] and [$(b,text)], where [text] 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 command
man page [$(cmd)] is substituted by the command's invocation 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.}
{- Referring to unknown markup directives or variables will generate
errors on standard error during documentation generation.}}

View file

@ -0,0 +1,228 @@
{0:tutorial Tutorial}
See also the {{!page-cookbook}cookbook},
{{!page-cookbook.blueprints}blueprints} and
{{!page-examples}examples}.
{1:terms Commands and terms}
With [Cmdliner] your tool's [main] function evaluates a command.
A command is a value of type {!Cmdliner.Cmd.t} which gathers a command
name and a term of type {!Cmdliner.Term.t}. A term represents both a
command line syntax fragment and an expression to be evaluated that
implements your tool. The type parameter of the term (and the command)
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.app}.
For example, in a [revolt.ml] file, for the function:
{@ocaml name=example_revolt1.ml[
let revolt () = print_endline "Revolt!"
]}
the term :
{@ocaml name=example_revolt1.ml[
open Cmdliner
let revolt_term = Term.app (Term.const revolt) (Term.const ())
]}
is a term that evaluates to the result (and effect) of the [revolt]
function. This term can be associated to a command:
{@ocaml name=example_revolt1.ml[
let cmd_revolt = Cmd.make (Cmd.info "revolt") revolt_term
]}
and evaluated with {!Cmdliner.Cmd.val-eval}:
{@ocaml name=example_revolt1.ml[
let main () = Cmd.eval cmd_revolt
let () = if !Sys.interactive then () else exit (main ())
]}
This defines a command line tool named ["revolt"] (this name will be
used in error reporting and documentation generation), without command
line arguments, that just prints ["Revolt!"] on [stdout].
{@sh[
> ocamlfind ocamlopt -linkpkg -package cmdliner -o revolt revolt.ml
> ./revolt
Revolt!
]}
{1:term_syntax Term syntax}
There is a special syntax that uses OCaml's
{{:https://ocaml.org/manual/5.3/bindingops.html}binding operators} for
writing terms which is less error prone when the number of arguments
you want to give to your function grows. In particular it allows you to
easily lift functions which have labels.
So in fact the program we have just shown above is usually rather
written this way:
{@ocaml name=example_revolt2.ml[
let revolt () = print_endline "Revolt!"
open Cmdliner
open Cmdliner.Term.Syntax
let cmd_revolt =
Cmd.make (Cmd.info "revolt") @@
let+ () = Term.const () in
revolt ()
let main () = Cmd.eval cmd_revolt
let () = if !Sys.interactive then () else exit (main ())
]}
{1:args_as_terms Command line arguments as terms}
The combinators in the {!Cmdliner.Arg} module allow to extract command
line arguments as terms. These terms can then be applied to lifted
OCaml functions to be evaluated. A term that uses terms that correspond
to command line argument implicitely defines a command line syntax
fragment. We show this on an concrete example.
In a [chorus.ml] file, consider the [chorus] function that prints
repeatedly a given message :
{@ocaml name=example_chorus.ml[
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:
{@sh[
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:
{@ocaml name=example_chorus.ml[
open Cmdliner
open Cmdliner.Term.Syntax
let count =
let doc = "Repeat the message $(docv) times." in
Arg.(value & opt int 10 & info ["c"; "count"] ~doc ~docv:"COUNT")
]}
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:
{@ocaml name=example_chorus.ml[
let msg =
let env =
let doc = "Overrides the default message to print." in
Cmd.Env.info "CHORUS_MSG" ~doc
in
let doc = "The message to print." in
Arg.(value & pos 0 string "Revolt!" & info [] ~env ~doc ~docv:"MSG")
]}
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.
We can now define a term and command for invoking the [chorus] function
using the {{!term_syntax}term syntax} and the obscure but handy
{{:https://ocaml.org/manual/5.2/bindingops.html#ss%3Aletops-punning}
let-punning} OCaml notation. This also shows that the
value {!Cmdliner.Cmd.val-info} can be given more
information about the term we execute which is notably used to
to generate the tool's man page.
{@ocaml name=example_chorus.ml[
let chorus_cmd =
let doc = "Print a customizable message repeatedly" in
let man = [
`S Manpage.s_bugs;
`P "Email bug reports to <bugs@example.org>." ]
in
Cmd.make (Cmd.info "chorus" ~version:"v2.0.0+dune" ~doc ~man) @@
let+ count and+ msg in
chorus ~count msg
let main () = Cmd.eval chorus_cmd
let () = if !Sys.interactive then () else exit (main ())
]}
Since we provided a [~version] string, the tool will automatically
respond to the [--version] option by printing this string.
Besides a tool using {!Cmdliner.Cmd.val-eval} always responds to the
[--help] option by showing the tool's man page
{{!page-tool_man.manual}generated} using the information you provided
with {!Cmdliner.Cmd.val-info} and {!Cmdliner.Arg.val-info}. Here is
the manual generated by our example:
{v
> ocamlfind ocamlopt -linkpkg -package cmdliner -o chorus chorus.ml
> ./chorus --help
NAME
chorus - Print a customizable message repeatedly
SYNOPSIS
chorus [--count=COUNT] [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.
COMMON OPTIONS
--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.
123 on indiscriminate errors reported on standard error.
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 <bugs@example.org>.
v}
If a pager is available, this output is written to a pager. This help
is also available in plain text or in the
{{:http://www.gnu.org/software/groff/groff.html}groff} man page format
by invoking the program with the option [--help=plain] or
[--help=groff].
And with this you should master the basics of Cmdliner, for examples
of more complex command line definitions consult the
{{!page-examples}examples}. For more tips, off-the-shelf recipes and
conventions have look at the {{!page-cookbook}cookbook}.