Something went wrong. Try again.
How a command-line program ends
Something went wrong. Try again.
8.7 kB · 201 lines
OCaml
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202(** Exit statuses and closed-pipe handling for cmdliner programs.
A command-line tool piped into [head], [grep -q] or an [awk] script that stops early sees its reader close the pipe while it still has output to write. That is the normal end of a pipeline, and {!run} ends such a program quietly with status [0] instead of reporting an error.
{!run} is the whole [main] of a cmdliner program. {!info} and {!exits} make the program's manual describe the statuses {!run} exits with. *)
val run : ?argv:string array -> ?refusal:(exn -> string option) -> unit Cmdliner.Cmd.t -> 'a(** [run cmd] evaluates [cmd] as the program's whole run and ends the process with the status the evaluation answers. It never returns, so a [main] ends on it:
{[ let main cmd = Cli_exit.run cmd ]}
[argv] is the command line to evaluate (defaults to {!Sys.argv}). A program that reads an option of its own before dispatch passes it: a group dispatches on the first token after the program name, so an option written before the subcommand is read as the subcommand name. The program strips the option and hands over what remains, which rewriting {!Sys.argv} cannot do because its length is fixed:
{[ let main cmd = match Array.to_list Sys.argv with | program :: "-C" :: directory :: rest -> Sys.chdir directory; Cli_exit.run ~argv:(Array.of_list (program :: rest)) cmd | _ -> Cli_exit.run cmd ]}
[refusal] maps the exceptions that are conditions the user can act on, as opposed to defects in the program, to a message. An exception it maps to [Some message] is reported as [message] on stderr under the program's own name, with no backtrace and no "internal error", and the process ends at {!refused}:
{[ let main cmd = Cli_exit.run ~refusal:(function Failure m -> Some m | _ -> None) cmd ]}
SIGPIPE is ignored before [cmd] is evaluated, so a write to a closed pipe raises an error instead of killing the process. [run] answers that error by ending the process with status [0], printing nothing on stderr and discarding the output still buffered for the closed reader.
Help is written to standard output and never sent to a pager, whatever [TERM] says; an explicit [--help=pager] still pages. {!sdocs} and {!standard_options} make the manual say so. Cmdliner still decides the styling of the help from [NO_COLOR] and [TERM].
An exception nothing expected is reported the way cmdliner reports one, on stderr under the program's own name, and the process ends with {!Cmdliner.Cmd.Exit.internal_error}. The report carries no terminal styling.
A closed pipe met by the run, a refusal and an exception nothing expected propagate out of [run] to the top of the program, unwinding every frame and finaliser on the way, and are reported there: [run] installs the report as the runtime's handler ({!Printexc.set_uncaught_exception_handler}). A cancellation that reaches the top keeps the runtime's own report.
A write to {!Eio.Stdenv.stdout} needs {!writing_output}, because [run] cannot tell Eio's report of that closed pipe from one on a socket. *)
val run' : ?argv:string array -> ?refusal:(exn -> string option) -> ?parse_error:(string -> int) -> int Cmdliner.Cmd.t -> 'a(** [run' cmd] is {!run} for a command whose term answers the exit status, as {!Cmdliner.Cmd.eval'} evaluates it: the process ends with the status the term answers, and a closed pipe, a refusal or an exception nothing expected ends it as {!run} does.
{[ let main cmds = Cli_exit.run' (Cmdliner.Cmd.group (Cmdliner.Cmd.info "tool") cmds) ]}
[parse_error] ends a command line that does not parse, which otherwise ends with {!cli_error} after cmdliner's report on stderr: an unknown option or command, a value its converter refuses, a missing argument, or an error of the term's own ({!Cmdliner.Term.ret}), which cmdliner answers the same way. Cmdliner's usage line, when it prints one, is still written on stderr; what it says is wrong is given to [parse_error] instead of being printed, as one line of plain text without the program's name ([unknown option '--colour']), and the process ends with the status [parse_error] answers. That line reads the same under every [TERM]: a name cmdliner sets in bold is quoted, as its plain styling quotes it. What cmdliner writes on stderr keeps its styling only when stderr is a terminal, whatever [TERM] says:
{[ let main cmd = Cli_exit.run' ~parse_error:(fun message -> print_endline ("refused: " ^ message); 2) cmd ]}
With [parse_error], anything else cmdliner writes on stderr, such as a deprecation warning, is written once the evaluation has ended. *)
val refused : int(** [refused] is the exit status of a refusal, {!Cmdliner.Cmd.Exit.some_error} ([1]). {!exits} declares it. *)
val cli_error : int(** [cli_error] is {!Cmdliner.Cmd.Exit.cli_error}, the exit status of a command line with an unknown option, an invalid value or an unexpected argument. *)
val exits : Cmdliner.Cmd.Exit.info list(** [exits] are the entries of a command's EXIT STATUS manual section: success, {!refused}, {!cli_error} and a defect in the program. {!info} passes this list to cmdliner, which generates the section from it.
A command with an ending of its own appends to it:
{[ let exits = Cmdliner.Cmd.Exit.info 3 ~doc:"on a deadline that expired." :: Cli_exit.exits ]} *)
val sdocs : Cmdliner.Manpage.section_name(** [sdocs] is the [~sdocs] argument for [Cmdliner.Cmd.info]. Cmdliner writes its own manual item for [--help], which describes the paging that {!run} disables, and offers no way to replace it. [sdocs] names a section the manual does not print, which hides that item, and {!standard_options} writes the correct items back. Use the two together in each command's info, or use {!info}, which applies both. *)
val standard_options : version:bool -> Cmdliner.Manpage.block list(** [standard_options ~version] is the common options section for a command whose info carries {!sdocs}: [--help], said the way {!run} answers it, and [--version] when [version] is [true]. Append it to the command's [~man].
[version] says whether the program's main command declares a version, since cmdliner adds [--version] to every command of such a program.
{!info} already includes this section; call [standard_options] only where a program needs the section without the rest of {!info}'s defaults. *)
val info : ?deprecated:string -> ?man_xrefs:Cmdliner.Manpage.xref list -> ?man:Cmdliner.Manpage.block list -> ?envs:Cmdliner.Cmd.Env.info list -> ?exits:Cmdliner.Cmd.Exit.info list -> ?docs:Cmdliner.Manpage.section_name -> ?doc:string -> ?version:string -> ?with_version:bool -> string -> Cmdliner.Cmd.info(** [info name] is [Cmdliner.Cmd.info name] with {!sdocs} and {!standard_options} already applied, so a command built through it cannot omit them: [~sdocs] is fixed to {!sdocs}, and {!standard_options}'s section is appended to [~man].
[version] is cmdliner's own [~version], the version string of the tool; pass it on the main command only. [with_version] is passed to {!standard_options} as [~version] (defaults to [true], since cmdliner lists [--version] on every command of a program whose main declares one). A program with no [--version] passes [~with_version:false] everywhere.
{[ let info = Cli_exit.info "tool" ~version:"1.0" ~doc:"A tool." let sub_info = Cli_exit.info "sub" ~doc:"A subcommand." ]}
[exits] defaults to {!exits}; a command with an ending of its own passes that list with its entry prepended. [deprecated], [man_xrefs], [envs], [docs] and [doc] are cmdliner's own {!Cmdliner.Cmd.val-info} arguments. *)
val writing_output : (unit -> 'a) -> 'a(** [writing_output f] is [f ()], and it ends the process the way {!run} does when the writes [f] made to the program's own output met a reader that has gone.
Wrap a write to {!Eio.Stdenv.stdout} with it:
{[ let cat env source = Cli_exit.writing_output @@ fun () -> Eio.Flow.copy source (Eio.Stdenv.stdout env) ]}
Eio reports a closed pipe as a reset connection that does not name the flow. A program that also writes to sockets must report their resets, so only the call site can say that a write went to the program's own output. *)