diff --git a/cmdliner/_doc-dir/CHANGES.md b/cmdliner/_doc-dir/CHANGES.md index 78b98382..6c91ef67 100644 --- a/cmdliner/_doc-dir/CHANGES.md +++ b/cmdliner/_doc-dir/CHANGES.md @@ -1,3 +1,36 @@ +v2.1.0 2025-11-25 Zagreb +------------------------ + +- completion: add support for powershell (#214). + Thanks to Brian Ward for the work. + +- bash completion: improve bash completion on files and directories (#238). + Thanks to Brian Ward for the report and fix. + +- bash completion: improve completion of long options with `=` (#231). + Thanks to Brian Ward for the patch. + +- zsh completion: fix completion of files and directories on glued + forms (#230). + Thanks to Brian Ward for the help. + +- zsh completion: strip ANSI escapes from doc strings. The experience + is too unreliable (#220). + +- cmdliner tool: add support for generating standalone completion + scripts via the `--standalone-completion` option. Can be used if you + distribute your software in a context where the cmdliner library + may not be installed (#243). + Thanks to Brian Ward for suggesting. + +- Add alternative instructions to the cookbook for installing tool support + files with `dune` (#250). + Thanks to Brian Ward for the patch and research. + +- `--help` output, improve graceful degradation on groff or pager + errors (#140). + Thanks to Sergey Fedorov for the report. + v2.0.0 2025-09-26 Zagreb ------------------------ diff --git a/cmdliner/_doc-dir/odoc-pages/cli.mld b/cmdliner/_doc-dir/odoc-pages/cli.mld index a66abc1e..746e1ec1 100644 --- a/cmdliner/_doc-dir/odoc-pages/cli.mld +++ b/cmdliner/_doc-dir/odoc-pages/cli.mld @@ -183,8 +183,8 @@ 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. +installed by the library. For now the [zsh], [bash], and PowerShell +([pwsh]) shells are supported. Tool developers can easily {{!install_tool_completion}install} completion definitions that invoke these completion scripts. Tool @@ -295,6 +295,45 @@ 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]. +{3:user_pwsh For PowerShell ([pwsh])} + +PowerShell Core supports tab completion but lacks the kind of +discovery mechanism that other shells have for these scripts. + +Therefore, you need to explicitly source the completion script +for cmdliner based tools. This can be done in the +{{:https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles} [$profile]} +file by adding something like the following: + +{@ps1[ +# first, load the generic cmdliner completion function +. "$(opam var share)/powershell/cmdliner_generic_completion.ps1" +# then, register each tool you want completions for +# for example: +. "$(opam var share)/powershell/cmdliner_completion.ps1" +. "$(opam var share)/powershell/odoc_completion.ps1" +# ... +]} + +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}. + +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: + +{@ps1[ +Invoke-Expression $(cmdliner tool-completion pwsh thetool) +]} + +or if you find that easier to remember: + +{@ps1[ +Register-ArgumentCompleter -Native -CommandName thetool -ScriptBlock $Global:_cmdliner_generic +]} + {2:install_completion Install} Completion scripts need to be installed in subdirectories of a @@ -310,7 +349,8 @@ 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]}} +{- For [bash] it is [$SHAREDIR/bash-completion/completions]} +{- For [pwsh] it is [$SHAREDIR/powershell]}} If that is unsatisfying you can output the completion scripts directly where you want with the [cmdliner generic-completion] and diff --git a/cmdliner/_doc-dir/odoc-pages/cookbook.mld b/cmdliner/_doc-dir/odoc-pages/cookbook.mld index e1486173..76254725 100644 --- a/cmdliner/_doc-dir/odoc-pages/cookbook.mld +++ b/cmdliner/_doc-dir/odoc-pages/cookbook.mld @@ -96,8 +96,8 @@ 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. +giving it to command {{!Cmdliner.Cmd.section-eval}evaluation functions} +via the [?argv] optional argument. These are two common cases: @@ -172,7 +172,7 @@ let cmd = Cmd.group (Cmd.info "tool") ~default @@ [Cmd_import.cmd; Cmd_serve.cmd; Cmd_user.cmd] -let main () = Cmd.value' cmd +let main () = Cmd.eval' cmd let () = if !Sys.interactive then () else exit (main ()) ]} @@ -213,22 +213,70 @@ 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 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: +{{: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: [ - [ … ] # Your regular dune build instructions + # 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/default/install/bin/tool" {os != "win32"} - "_build/default/install/bin/tool.exe" {os = "win32"} + "_build/install/default/bin/mytool" {os-family != "windows"} + "_build/install/default/bin/mytool.exe" {os-family = "windows"} "_build/cmdliner-install"]] ]} @@ -254,11 +302,11 @@ 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") + 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 string "-" & info ["o", "output-file"] ~doc ~docv:"FILE") + 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 @@ -637,7 +685,7 @@ open Cmdliner open Cmdliner.Term.Syntax let cmd = - Cmd.make (Cmd.info "TODO" ~version:"v2.0.0") @@ + Cmd.make (Cmd.info "TODO" ~version:"v2.1.0") @@ let+ unit = Term.const () in tool unit @@ -660,7 +708,7 @@ 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") + Arg.(value & pos 0 filepath "-" & info [] ~doc ~docv:"FILE") let cmd = let doc = "The tool synopsis is TODO" in @@ -672,7 +720,7 @@ let cmd = Cmd.Exit.info exit_todo ~doc:"When there is stuff todo" :: Cmd.Exit.defaults in - Cmd.make (Cmd.info "TODO" ~version:"v2.0.0" ~doc ~man ~exits) @@ + Cmd.make (Cmd.info "TODO" ~version:"v2.1.0" ~doc ~man ~exits) @@ let+ flag and+ infile in tool ~flag ~infile @@ -696,7 +744,7 @@ 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") + Arg.(value & pos 0 filepath "-" & info [] ~doc ~docv:"FILE") let hey_cmd = let doc = "The hey command synopsis is TODO" in @@ -712,7 +760,7 @@ let ho_cmd = let cmd = let doc = "The tool synopsis is TODO" in - Cmd.group (Cmd.info "TODO" ~version:"v2.0.0" ~doc) @@ + Cmd.group (Cmd.info "TODO" ~version:"v2.1.0" ~doc) @@ [hey_cmd; ho_cmd] let main () = Cmd.eval' cmd diff --git a/cmdliner/_doc-dir/odoc-pages/examples.mld b/cmdliner/_doc-dir/odoc-pages/examples.mld index 412d6025..ab1ba5f0 100644 --- a/cmdliner/_doc-dir/odoc-pages/examples.mld +++ b/cmdliner/_doc-dir/odoc-pages/examples.mld @@ -81,7 +81,7 @@ let rm_cmd = `S Manpage.s_bugs; `P "Report bugs to ."; `S Manpage.s_see_also; `P "$(b,rmdir)(1), $(b,unlink)(2)" ] in - Cmd.make (Cmd.info "rm" ~version:"v2.0.0" ~doc ~man) @@ + Cmd.make (Cmd.info "rm" ~version:"v2.1.0" ~doc ~man) @@ let+ prompt and+ recursive and+ files in rm ~prompt ~recurse:recursive files @@ -154,7 +154,7 @@ let cp_cmd = `S Manpage.s_bugs; `P "Email them to ."; ] in - Cmd.make (Cmd.info "cp" ~version:"v2.0.0" ~doc ~man ~man_xrefs) @@ + Cmd.make (Cmd.info "cp" ~version:"v2.1.0" ~doc ~man ~man_xrefs) @@ Term.ret @@ let+ verbose and+ recurse and+ force and+ srcs and+ dest in cp ~verbose ~recurse ~force srcs dest @@ -265,7 +265,7 @@ let tail_cmd = `S Manpage.s_see_also; `P "$(b,cat)(1), $(b,head)(1)" ] in - Cmd.make (Cmd.info "tail" ~version:"v2.0.0" ~doc ~man) @@ + Cmd.make (Cmd.info "tail" ~version:"v2.1.0" ~doc ~man) @@ let+ lines and+ follow and+ verb and+ pid and+ files in tail ~lines ~follow ~verb ~pid files @@ -444,7 +444,7 @@ let help_cmd = let main_cmd = let doc = "a revision control system" in let man = help_secs in - let info = Cmd.info "darcs" ~version:"v2.0.0" ~doc ~sdocs ~man in + let info = Cmd.info "darcs" ~version:"v2.1.0" ~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] diff --git a/cmdliner/_doc-dir/odoc-pages/index.mld b/cmdliner/_doc-dir/odoc-pages/index.mld index 7e42c5c0..a1ac3d20 100644 --- a/cmdliner/_doc-dir/odoc-pages/index.mld +++ b/cmdliner/_doc-dir/odoc-pages/index.mld @@ -1,4 +1,4 @@ -{0 Cmdliner {%html: v2.0.0%}} +{0 Cmdliner {%html: v2.1.0%}} Cmdliner provides a simple and compositional mechanism to convert command line arguments to OCaml values and pass them to diff --git a/cmdliner/_doc-dir/odoc-pages/tutorial.mld b/cmdliner/_doc-dir/odoc-pages/tutorial.mld index fc3075af..2819881c 100644 --- a/cmdliner/_doc-dir/odoc-pages/tutorial.mld +++ b/cmdliner/_doc-dir/odoc-pages/tutorial.mld @@ -152,7 +152,7 @@ let chorus_cmd = `S Manpage.s_bugs; `P "Email bug reports to ." ] in - Cmd.make (Cmd.info "chorus" ~version:"v2.0.0" ~doc ~man) @@ + Cmd.make (Cmd.info "chorus" ~version:"v2.1.0" ~doc ~man) @@ let+ count and+ msg in chorus ~count msg diff --git a/cmdliner/cli.html b/cmdliner/cli.html index 65bf5abd..579b14f2 100644 --- a/cmdliner/cli.html +++ b/cmdliner/cli.html @@ -1,7 +1,7 @@ -cli (cmdliner.cli)

Command line interface

This manual describes how your tool ends up interacting with shells when you use Cmdliner.

Tool invocation

For tools evaluating a command without subcommands the most general form of invocation is:

tool [OPTION]… [ARG]…

The tool automatically reponds to the --help option by printing the help. If a version string is provided in the command information, it also automatically responds to the --version option by printing this string on standard output.

Command line arguments are either optional or positional. Both can be freely interleaved but since Cmdliner accepts many optional forms this may result in ambiguities. The special token -- can be used to resolve them: anything that follows it is treated as a positional argument.

Tools evaluating commands with subcommands have this form of invocation

tool [COMMAND]… [OPTION]… [ARG]…

Commands automatically respond to the --help option by printing 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.

Arguments

Optional arguments

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

The name of an option can be short or long.

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

More than one name may refer to the same optional argument. For example in a given program the names -q, --quiet and --silent may all stand for the same boolean argument indicating the program to be quiet.

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

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

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

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

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

  • -vx will be parsed as -v -x.
  • -vxfopt will be parsed as -v -x -fopt.
  • -vxf opt will be parsed as -v -x -fopt.
  • -fvx will be parsed as -f=vx.

Positional arguments

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

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

tool --option -- but --now we -are --all positional --argu=ments

Constraints on option names

Using the cmdliner library puts the following constraints on your command line interface:

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

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:

  • "", "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.

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

tool --help
+cli (cmdliner.cli)

Command line interface

This manual describes how your tool ends up interacting with shells when you use Cmdliner.

Tool invocation

For tools evaluating a command without subcommands the most general form of invocation is:

tool [OPTION]… [ARG]…

The tool automatically reponds to the --help option by printing the help. If a version string is provided in the command information, it also automatically responds to the --version option by printing this string on standard output.

Command line arguments are either optional or positional. Both can be freely interleaved but since Cmdliner accepts many optional forms this may result in ambiguities. The special token -- can be used to resolve them: anything that follows it is treated as a positional argument.

Tools evaluating commands with subcommands have this form of invocation

tool [COMMAND]… [OPTION]… [ARG]…

Commands automatically respond to the --help option by printing 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.

Arguments

Optional arguments

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

The name of an option can be short or long.

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

More than one name may refer to the same optional argument. For example in a given program the names -q, --quiet and --silent may all stand for the same boolean argument indicating the program to be quiet.

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

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

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

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

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

  • -vx will be parsed as -v -x.
  • -vxfopt will be parsed as -v -x -fopt.
  • -vxf opt will be parsed as -v -x -fopt.
  • -fvx will be parsed as -f=vx.

Positional arguments

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

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

tool --option -- but --now we -are --all positional --argu=ments

Constraints on option names

Using the cmdliner library puts the following constraints on your command line interface:

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

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:

  • "", "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.

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

tool --help
 tool cmd --help
-tool --help=groff > tool.1

Paging

The pager is selected by looking up, in order:

  1. The MANPAGER variable.
  2. The PAGER variable.
  3. The tool less.
  4. 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.

Install

The manpages of a tool and its subcommands can be installed to a root man directory $MANDIR by invoking:

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 installing completions rather use the install tool-support command, see this cookbook tip which also has instructions on how to install if you are using opam.

Command line completion

Cmdliner programs automatically get support for shell command line completion.

The completion process happens via a 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 completion definitions that invoke these completion scripts. Tool end-users need to make sure these definitions are looked up by their shell.

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.

For zsh

The FPATH environment variable must be setup to include the directory where the generic cmdliner completion function is installed before properly initializing the completion system.

For example, for now, if you are using opam. You should add something like this to your .zshrc:

FPATH="$(opam var share)/zsh/site-functions:${FPATH}"
+tool --help=groff > tool.1

Paging

The pager is selected by looking up, in order:

  1. The MANPAGER variable.
  2. The PAGER variable.
  3. The tool less.
  4. 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.

Install

The manpages of a tool and its subcommands can be installed to a root man directory $MANDIR by invoking:

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 installing completions rather use the install tool-support command, see this cookbook tip which also has instructions on how to install if you are using opam.

Command line completion

Cmdliner programs automatically get support for shell command line completion.

The completion process happens via a protocol which is interpreted by generic shell completion scripts that are installed by the library. For now the zsh, bash, and PowerShell (pwsh) shells are supported.

Tool developers can easily install completion definitions that invoke these completion scripts. Tool end-users need to make sure these definitions are looked up by their shell.

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.

For zsh

The FPATH environment variable must be setup to include the directory where the generic cmdliner completion function is installed before properly initializing the completion system.

For example, for now, if you are using opam. You should add something like this to your .zshrc:

FPATH="$(opam var share)/zsh/site-functions:${FPATH}"
 autoload -Uz compinit
 compinit -u

Also make sure this happens before opam's zsh init script inclusion, see 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 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).

> autoload _cmdliner_generic
 > _cmdliner_generic
@@ -9,7 +9,13 @@ _cmdliner_generic:1: words: assignment to invalid subscript range

For bash

These instructions assume that you have 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 installed.

For example, for now, if you are using opam. You should add something like this to your .bashrc:

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 this opam issue.

After this, to test everything is right, check that the _cmdliner_generic function can be looked up:

> _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 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 a completion definition. You can always do it yourself by invoking:

_completion_loader _cmdliner_generic
-complete -F _cmdliner_generic thetool

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

Install

Completion scripts need to be installed in subdirectories of a share directory which we denote by the $SHAREDIR variable below. In a package installation script this variable is typically defined by:

SHAREDIR="$DESTDIR/$PREFIX/share"

The final destination directory in share depends on the shell:

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

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.

cmdliner generic-completion zsh   # Output generic zsh script on stdout
+complete -F _cmdliner_generic thetool

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

For PowerShell (pwsh)

PowerShell Core supports tab completion but lacks the kind of discovery mechanism that other shells have for these scripts.

Therefore, you need to explicitly source the completion script for cmdliner based tools. This can be done in the $profile file by adding something like the following:

# first, load the generic cmdliner completion function
+. "$(opam var share)/powershell/cmdliner_generic_completion.ps1"
+# then, register each tool you want completions for
+# for example:
+. "$(opam var share)/powershell/cmdliner_completion.ps1"
+. "$(opam var share)/powershell/odoc_completion.ps1"
+# ...

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 this opam issue.

With this setup, if you are using a cmdliner based tool named thetool that did not install a completion definition. You can always do it yourself by invoking:

Invoke-Expression $(cmdliner tool-completion pwsh thetool)

or if you find that easier to remember:

Register-ArgumentCompleter -Native -CommandName thetool -ScriptBlock $Global:_cmdliner_generic

Install

Completion scripts need to be installed in subdirectories of a share directory which we denote by the $SHAREDIR variable below. In a package installation script this variable is typically defined by:

SHAREDIR="$DESTDIR/$PREFIX/share"

The final destination directory in share depends on the shell:

  • For zsh it is $SHAREDIR/zsh/site-functions
  • For bash it is $SHAREDIR/bash-completion/completions
  • For pwsh it is $SHAREDIR/powershell

If that is unsatisfying you can output the completion scripts directly where you want with the cmdliner generic-completion and cmdliner tool-completion commands.

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.

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.

Tool completion scripts

If your tool named thetool uses Cmdliner you should install completion definitions for them. They rely on the generic scripts to be installed. These tool specific scripts can be inspected and installed via these invocations:

cmdliner tool-completion zsh thetool  # Output tool zsh script on stdout.
 cmdliner install tool-completion thetool $SHAREDIR             # All shells
diff --git a/cmdliner/cookbook.html b/cmdliner/cookbook.html
index 3e9d5cde..bb4ed03b 100644
--- a/cmdliner/cookbook.html
+++ b/cmdliner/cookbook.html
@@ -1,6 +1,6 @@
 
 cookbook (cmdliner.cookbook)

Cmdliner cookbook

A few recipes and starting blueprints to describe your command lines with Cmdliner.

Note. Some of the code snippets here assume they are done after:

open Cmdliner
-open Cmdliner.Term.Syntax

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 should rather not use.

Avoid default commands in groups

Command 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 disambiguation token -- to specify them so that they can be distinguished from command names. For example:

tool -- file …

One thing that is acceptable is to have a default command that simply shows documentation for the group of subcommands as this not interfere with tool operation.

Avoid default option values

Optional arguments 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

tool --opt[=VALUE] [FILE]

Trying to refine the following invocation to add a FILE parameter is error prone and painful:

tool --opt

There is more than one way but the easiest way is to specify:

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.

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.

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.

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 evaluation functions via the ?argv optional argument.

These are two common cases:

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

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
+open Cmdliner.Term.Syntax

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 should rather not use.

Avoid default commands in groups

Command 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 disambiguation token -- to specify them so that they can be distinguished from command names. For example:

tool -- file …

One thing that is acceptable is to have a default command that simply shows documentation for the group of subcommands as this not interfere with tool operation.

Avoid default option values

Optional arguments 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

tool --opt[=VALUE] [FILE]

Trying to refine the following invocation to add a FILE parameter is error prone and painful:

tool --opt

There is more than one way but the easiest way is to specify:

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.

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.

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.

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 evaluation functions via the ?argv optional argument.

These are two common cases:

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

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. *)
 
@@ -21,24 +21,47 @@ cmd_import.mli   cmd_serve.mli    cmd_user.mli  tool_cli.mli

The Cmd.group (Cmd.info "tool") ~default @@ [Cmd_import.cmd; Cmd_serve.cmd; Cmd_user.cmd] -let main () = Cmd.value' cmd +let main () = Cmd.eval' cmd let () = if !Sys.interactive then () else exit (main ())

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 install tool-completion and 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.

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

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.

With opam and dune

First make sure your understand the above basic instructions for opam. You then 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:

build: [
-   [ … ] # Your regular dune build instructions
+              "$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.

With dune build @install

Users of dune 3.5 or later can use the cmdliner install tool-support command described above, along with the directory-targets feature and 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):

(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 these instructions.

With opam and dune

First make sure your understand the above basic instructions for opam. You then 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:

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/default/install/bin/tool" {os != "win32"}
-               "_build/default/install/bin/tool.exe" {os = "win32"}
+               "_build/install/default/bin/mytool" {os-family != "windows"}
+               "_build/install/default/bin/mytool.exe" {os-family = "windows"}
                "_build/cmdliner-install"]]

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.

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")
+  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 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 =
+  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
@@ -110,7 +133,7 @@ open Cmdliner
 open Cmdliner.Term.Syntax
 
 let cmd =
-  Cmd.make (Cmd.info "TODO" ~version:"v2.0.0") @@
+  Cmd.make (Cmd.info "TODO" ~version:"v2.1.0") @@
   let+ unit = Term.const () in
   tool unit
 
@@ -124,7 +147,7 @@ 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")
+  Arg.(value & pos 0 filepath "-" & info [] ~doc ~docv:"FILE")
 
 let cmd =
   let doc = "The tool synopsis is TODO" in
@@ -136,7 +159,7 @@ let cmd =
     Cmd.Exit.info exit_todo ~doc:"When there is stuff todo" ::
     Cmd.Exit.defaults
   in
-  Cmd.make (Cmd.info "TODO" ~version:"v2.0.0" ~doc ~man ~exits) @@
+  Cmd.make (Cmd.info "TODO" ~version:"v2.1.0" ~doc ~man ~exits) @@
   let+ flag and+ infile in
   tool ~flag ~infile
 
@@ -150,7 +173,7 @@ 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")
+  Arg.(value & pos 0 filepath "-" & info [] ~doc ~docv:"FILE")
 
 let hey_cmd =
   let doc = "The hey command synopsis is TODO" in
@@ -166,7 +189,7 @@ let ho_cmd =
 
 let cmd =
   let doc = "The tool synopsis is TODO" in
-  Cmd.group (Cmd.info "TODO" ~version:"v2.0.0" ~doc) @@
+  Cmd.group (Cmd.info "TODO" ~version:"v2.1.0" ~doc) @@
   [hey_cmd; ho_cmd]
 
 let main () = Cmd.eval' cmd
diff --git a/cmdliner/examples.html b/cmdliner/examples.html
index c7391d5c..485cf7b3 100644
--- a/cmdliner/examples.html
+++ b/cmdliner/examples.html
@@ -53,7 +53,7 @@ let rm_cmd =
     `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" ~doc ~man) @@
+  Cmd.make (Cmd.info "rm" ~version:"v2.1.0" ~doc ~man) @@
   let+ prompt and+ recursive and+ files in
   rm ~prompt ~recurse:recursive files
 
@@ -104,7 +104,7 @@ let cp_cmd =
     `S Manpage.s_bugs;
     `P "Email them to <bugs@example.org>."; ]
   in
-  Cmd.make (Cmd.info "cp" ~version:"v2.0.0" ~doc ~man ~man_xrefs) @@
+  Cmd.make (Cmd.info "cp" ~version:"v2.1.0" ~doc ~man ~man_xrefs) @@
   Term.ret @@
   let+ verbose and+ recurse and+ force and+ srcs and+ dest in
   cp ~verbose ~recurse ~force srcs dest
@@ -188,7 +188,7 @@ let tail_cmd =
     `S Manpage.s_see_also;
     `P "$(b,cat)(1), $(b,head)(1)" ]
   in
-  Cmd.make (Cmd.info "tail" ~version:"v2.0.0" ~doc ~man) @@
+  Cmd.make (Cmd.info "tail" ~version:"v2.1.0" ~doc ~man) @@
   let+ lines and+ follow and+ verb and+ pid and+ files in
   tail ~lines ~follow ~verb ~pid files
 
@@ -338,7 +338,7 @@ let help_cmd =
 let main_cmd =
   let doc = "a revision control system" in
   let man = help_secs in
-  let info = Cmd.info "darcs" ~version:"v2.0.0" ~doc ~sdocs ~man in
+  let info = Cmd.info "darcs" ~version:"v2.1.0" ~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]
 
diff --git a/cmdliner/tutorial.html b/cmdliner/tutorial.html
index 4ae1fef2..a3fa533c 100644
--- a/cmdliner/tutorial.html
+++ b/cmdliner/tutorial.html
@@ -32,7 +32,7 @@ let count =
     `S Manpage.s_bugs;
     `P "Email bug reports to <bugs@example.org>." ]
   in
-  Cmd.make (Cmd.info "chorus" ~version:"v2.0.0" ~doc ~man) @@
+  Cmd.make (Cmd.info "chorus" ~version:"v2.1.0" ~doc ~man) @@
   let+ count and+ msg in
   chorus ~count msg