mirror of
https://github.com/c-cube/linol.git
synced 2025-12-06 11:15:46 -05:00
768 lines
27 KiB
Text
768 lines
27 KiB
Text
{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.eval' 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:top_tool_support_with_dune_install With [dune build @install]}
|
|
|
|
Users of [dune] 3.5 or later can use the [cmdliner install
|
|
tool-support] command described above, along with the
|
|
{{:https://dune.readthedocs.io/en/stable/reference/dune/rule.html#directory-targets}
|
|
[directory-targets]}
|
|
feature and
|
|
{{:https://dune.readthedocs.io/en/latest/reference/dune/install.html}
|
|
[install]} stanza, to hook into the existing [dune build @install] command.
|
|
|
|
For an executable named [mytool] defined in a [dune] file, you can add
|
|
the following to the same [dune] file to populate the completion
|
|
scripts and manpages when running [dune build @install] (which is what
|
|
is used by [opam install]):
|
|
|
|
{@dune[
|
|
(rule
|
|
(target (dir cmdliner-support))
|
|
(deps mytool.exe)
|
|
(action
|
|
(ignore-stdout
|
|
(run cmdliner install tool-support ./mytool.exe:mytool cmdliner-support))))
|
|
|
|
(install
|
|
(section share_root)
|
|
(dirs (cmdliner-support/share as .)))
|
|
]}
|
|
|
|
If these features of dune are not available to your project, you may
|
|
instead need to update the [opam] file, see {{!tip_tool_support_with_opam_dune}
|
|
these instructions}.
|
|
|
|
{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 add a [.opam.template]} file which inserts the [cmdliner
|
|
install] instruction to the [build:] field of the opam file after your
|
|
[dune] build instructions. For a tool named [mytool] the
|
|
[mytool.opam.template] file should contain:
|
|
|
|
{@sh[
|
|
build: [
|
|
# These first two commands are what dune generates if you don't have
|
|
# a template
|
|
["dune" "subst"] {dev}
|
|
[
|
|
"dune"
|
|
"build"
|
|
"-p"
|
|
name
|
|
"-j"
|
|
jobs
|
|
"@install"
|
|
"@runtest" {with-test}
|
|
"@doc" {with-doc}
|
|
]
|
|
# This is the important command for cmdliner's files
|
|
["cmdliner" "install" "tool-support"
|
|
"--update-opam-install=%{_:name}%.install"
|
|
"_build/install/default/bin/mytool" {os-family != "windows"}
|
|
"_build/install/default/bin/mytool.exe" {os-family = "windows"}
|
|
"_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 filepath "-" & 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 filepath "-" & 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.1.0") @@
|
|
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 filepath "-" & 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.1.0" ~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 filepath "-" & 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.1.0" ~doc) @@
|
|
[hey_cmd; ho_cmd]
|
|
|
|
let main () = Cmd.eval' cmd
|
|
let () = if !Sys.interactive then () else exit (main ())
|
|
]}
|