tezos_clic.ml
(*****************************************************************************)
(* *)
(* Open Source License *)
(* Copyright (c) 2018 Dynamic Ledger Solutions, Inc. <contact@tezos.com> *)
(* *)
(* Permission is hereby granted, free of charge, to any person obtaining a *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *)
(* and/or sell copies of the Software, and to permit persons to whom the *)
(* Software is furnished to do so, subject to the following conditions: *)
(* *)
(* The above copyright notice and this permission notice shall be included *)
(* in all copies or substantial portions of the Software. *)
(* *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *)
(* DEALINGS IN THE SOFTWARE. *)
(* *)
(*****************************************************************************)
open Error_monad
module StringMap = Map.Make (String)
module StringSet = Set.Make (String)
type ('p, 'ctx) parameter = {
converter : 'ctx -> string -> 'p tzresult Lwt.t;
autocomplete : ('ctx -> string list tzresult Lwt.t) option;
}
let parameter ?autocomplete converter = {converter; autocomplete}
let compose_parameters {converter = c1; autocomplete = a1'}
{converter = c2; autocomplete = a2'} =
let open Lwt_result_syntax in
{
converter =
(fun ctx s ->
let*! r = c1 ctx s in
match r with Ok r -> return r | Error _ -> c2 ctx s);
autocomplete =
(match a1' with
| None -> a2'
| Some a1 -> (
match a2' with
| None -> a1'
| Some a2 ->
Some
(fun ctx ->
let* r1 = a1 ctx in
let* r2 = a2 ctx in
return (List.concat [r1; r2]))));
}
let map_parameter ~f {converter; autocomplete} =
let open Lwt_result_syntax in
{
converter =
(fun ctx s ->
let+ v = converter ctx s in
f v);
autocomplete;
}
let map_es_parameter ~f {converter; autocomplete} =
let open Lwt_result_syntax in
{
converter =
(fun ctx s ->
let* v = converter ctx s in
f ctx v);
autocomplete;
}
type label = {long : string; short : char option}
type ('a, 'ctx) arg =
| Arg : {
doc : string;
label : label;
placeholder : string;
kind : ('p, 'ctx) parameter;
}
-> ('p option, 'ctx) arg
| DefArg : {
doc : string;
label : label;
placeholder : string;
kind : ('p, 'ctx) parameter;
default : string;
}
-> ('p, 'ctx) arg
| Switch : {label : label; doc : string} -> (bool, 'ctx) arg
| Constant : 'a -> ('a, 'ctx) arg
| Map : {
spec : ('a, 'ctx) arg;
converter : 'ctx -> 'a -> 'b tzresult Lwt.t;
}
-> ('b, 'ctx) arg
| Pair : ('a, 'ctx) arg * ('b, 'ctx) arg -> ('a * 'b, 'ctx) arg
(* A simple structure for command interpreters.
This is more generic than the exported one, see end of file. *)
type ('a, 'ctx) params =
| Prefix : string * ('a, 'ctx) params -> ('a, 'ctx) params
| Param :
string * string * ('p, 'ctx) parameter * ('a, 'ctx) params
-> ('p -> 'a, 'ctx) params
| Stop : ('ctx -> unit tzresult Lwt.t, 'ctx) params
| Seq :
string * string * ('p, 'ctx) parameter
-> ('p list -> 'ctx -> unit tzresult Lwt.t, 'ctx) params
| NonTerminalSeq :
string
* string
* ('p, 'ctx) parameter
* string list
* ('a -> 'b, 'ctx) params
-> ('p list -> 'a -> 'b, 'ctx) params
type ('a, 'ctx) options = ('a, 'ctx) arg
let aggregate spec = spec
(* A command group *)
type group = {name : string; title : string}
(* A command wraps a callback with its type and info *)
type 'arg command =
| Command : {
params : ('a, 'iarg) params;
options : ('b, 'iarg) options;
handler : 'b -> 'a;
desc : string;
group : group option;
conv : 'arg -> 'iarg;
}
-> 'arg command
type error += Bad_argument of int * string
type error += Unterminated_command : string list * 'ctx command list -> error
type error += Command_not_found : string list * 'ctx command list -> error
type error += Unknown_option : string * 'ctx command option -> error
type error += Option_expected_argument : string * 'ctx command option -> error
type error += Bad_option_argument : string * 'ctx command option -> error
type error += Multiple_occurrences : string * 'ctx command option -> error
type error += Extra_arguments : string list * 'ctx command -> error
let trim s =
(* config-file workaround *)
TzString.split_no_empty '\n' s |> List.map String.trim |> String.concat "\n"
let print_desc ppf doc =
let short, long =
match String.index_opt doc '\n' with
| None -> (doc, None)
| Some len ->
( String.sub doc 0 len,
Some (String.sub doc (len + 1) (String.length doc - len - 1)) )
in
match long with
| None -> Format.fprintf ppf "%s" short
| Some doc ->
Format.fprintf
ppf
"%s@{<full>@\n @[<hov 0>%a@]@}"
short
Format.pp_print_text
doc
let print_label ppf = function
| {long; short = None} -> Format.fprintf ppf "--%s" long
| {long; short = Some short} -> Format.fprintf ppf "-%c --%s" short long
let rec print_options_detailed :
type ctx a. Format.formatter -> (a, ctx) options -> unit =
fun ppf -> function
| Arg {label; placeholder; doc; _} ->
Format.fprintf
ppf
"@{<opt>%a <%s>@}: %a"
print_label
label
placeholder
print_desc
doc
| DefArg {label; placeholder; doc; default; _} ->
Format.fprintf
ppf
"@{<opt>%a <%s>@}: %a"
print_label
label
placeholder
print_desc
(doc ^ "\nDefaults to `" ^ default ^ "`.")
| Switch {label; doc} ->
Format.fprintf ppf "@{<opt>%a@}: %a" print_label label print_desc doc
| Constant _ -> ()
| Pair (speca, specb) ->
Format.fprintf
ppf
"%a@,%a"
print_options_detailed
speca
print_options_detailed
specb
| Map {spec; converter = _} -> print_options_detailed ppf spec
let rec has_args : type a ctx. (a, ctx) arg -> bool = function
| Constant _ -> false
| Arg _ | DefArg _ | Switch _ -> true
| Pair (speca, specb) -> has_args speca || has_args specb
| Map {spec; _} -> has_args spec
let rec print_options_brief :
type ctx a. Format.formatter -> (a, ctx) arg -> unit =
fun ppf -> function
| DefArg {label; placeholder; _} ->
Format.fprintf ppf "[@{<opt>%a <%s>@}]" print_label label placeholder
| Arg {label; placeholder; _} ->
Format.fprintf ppf "[@{<opt>%a <%s>@}]" print_label label placeholder
| Switch {label; _} -> Format.fprintf ppf "[@{<opt>%a@}]" print_label label
| Constant _ -> ()
| Pair (speca, specb) ->
Format.fprintf
ppf
"%a@ %a"
print_options_brief
speca
print_options_brief
specb
| Map {spec; converter = _} -> print_options_brief ppf spec
let print_highlight highlight_strings formatter str =
let rec print_string = function
| [] -> Format.fprintf formatter "%s" str
| regex :: tl -> (
match Re.Str.full_split regex str with
| [] | [Re.Str.Text _] -> print_string tl
| list ->
List.iter
(function
| Re.Str.Text text -> Format.fprintf formatter "%s" text
| Re.Str.Delim delimiter ->
Format.fprintf formatter "@{<hilight>%s@}" delimiter)
list)
in
print_string (List.map Re.Str.regexp_string highlight_strings)
let print_commandline ppf (highlights, options, args) =
let print_suffix =
Format.(pp_print_list ~pp_sep:pp_print_space (print_highlight highlights))
in
let rec print : type a ctx. Format.formatter -> (a, ctx) params -> unit =
fun ppf -> function
| Stop -> Format.fprintf ppf "%a" print_options_brief options
| Seq (n, _, _) when not (has_args options) ->
Format.fprintf ppf "[@{<arg>%s@}...]" n
| Seq (n, _, _) ->
Format.fprintf ppf "[@{<arg>%s@}...] %a" n print_options_brief options
| NonTerminalSeq (n, _, _, suffix, Stop) when not (has_args options) ->
Format.fprintf ppf "[@{<arg>%s@}...] @{<kwd>%a@}" n print_suffix suffix
| NonTerminalSeq (n, _, _, suffix, next) ->
Format.fprintf
ppf
"[@{<arg>%s@}...] @{<kwd>%a@} %a %a"
n
print_suffix
suffix
print
next
print_options_brief
options
| Prefix (n, Stop) when not (has_args options) ->
Format.fprintf ppf "@{<kwd>%a@}" (print_highlight highlights) n
| Prefix (n, next) ->
Format.fprintf
ppf
"@{<kwd>%a@} %a"
(print_highlight highlights)
n
print
next
| Param (n, _, _, Stop) when not (has_args options) ->
Format.fprintf ppf "@{<arg>%s@}" n
| Param (n, _, _, next) -> Format.fprintf ppf "@{<arg>%s@} %a" n print next
in
Format.fprintf ppf "@{<commandline>%a@}" print args
let rec print_params_detailed :
type a b ctx. (b, ctx) arg -> Format.formatter -> (a, ctx) params -> unit =
fun spec ppf -> function
| Stop -> print_options_detailed ppf spec
| Seq (n, desc, _) ->
Format.fprintf ppf "@{<arg>%s@}: %a" n print_desc (trim desc) ;
if has_args spec then
Format.fprintf ppf "@,%a" print_options_detailed spec
| NonTerminalSeq (n, desc, _, _, next) ->
Format.fprintf ppf "@{<arg>%s@}: %a" n print_desc (trim desc) ;
if has_args spec then
Format.fprintf ppf "@,%a" (print_params_detailed spec) next
| Prefix (_, next) -> print_params_detailed spec ppf next
| Param (n, desc, _, Stop) ->
Format.fprintf ppf "@{<arg>%s@}: %a" n print_desc (trim desc) ;
if has_args spec then
Format.fprintf ppf "@,%a" print_options_detailed spec
| Param (n, desc, _, next) ->
Format.fprintf
ppf
"@{<arg>%s@}: %a@,%a"
n
print_desc
(trim desc)
(print_params_detailed spec)
next
let contains_params_args : type a ctx. (a, ctx) params -> (_, ctx) arg -> bool =
fun params args ->
let rec help : (a, ctx) params -> bool = function
| Stop -> has_args args
| Seq (_, _, _) -> true
| NonTerminalSeq (_, _, _, _, _) -> true
| Prefix (_, next) -> help next
| Param (_, _, _, _) -> true
in
help params
let print_command :
type ctx.
?prefix:(Format.formatter -> unit -> unit) ->
?highlights:string list ->
Format.formatter ->
ctx command ->
unit =
fun ?(prefix = fun _ () -> ())
?(highlights = [])
ppf
(Command {params; desc; options; _}) ->
if contains_params_args params options then
Format.fprintf
ppf
"@{<command>%a%a@{<short>@,@{<commanddoc>%a@,%a@}@}@}"
prefix
()
print_commandline
(highlights, options, params)
print_desc
desc
(print_params_detailed options)
params
else
Format.fprintf
ppf
"@{<command>%a%a@{<short>@,@{<commanddoc>%a@}@}@}"
prefix
()
print_commandline
(highlights, options, params)
print_desc
desc
type ex_command = Ex : _ command -> ex_command
let group_commands commands =
let grouped, ungrouped =
List.fold_left
(fun (grouped, ungrouped) (Ex (Command {group; _}) as command) ->
match group with
| None -> (grouped, command :: ungrouped)
| Some group -> (
match
List.find_opt (fun ({name; _}, _) -> group.name = name) grouped
with
| None -> ((group, ref [command]) :: grouped, ungrouped)
| Some ({title; _}, r) ->
if title <> group.title then
invalid_arg "Tezos_clic.usage: duplicate group name" ;
r := command :: !r ;
(grouped, ungrouped)))
([], [])
commands
in
List.map
(fun (g, c) -> (g, List.rev !c))
(match ungrouped with
| [] -> grouped
| l ->
grouped @ [({name = "misc"; title = "Miscellaneous commands"}, ref l)])
let print_group print_command ppf ({title; _}, commands) =
Format.fprintf
ppf
"@{<title>%s@}@,@{<list>%a@}"
title
(Format.pp_print_list print_command)
commands
type formatter_state =
Format.formatter_out_functions * Format.formatter_stag_functions * bool
type format = Plain | Ansi | Html
type verbosity = Terse | Short | Details | Full
let setup_formatter ppf format verbosity =
let skip = ref false in
let ((orig_out_functions, _, _) as orig_state) =
( Format.pp_get_formatter_out_functions ppf (),
Format.pp_get_formatter_stag_functions ppf (),
Format.pp_get_print_tags ppf () )
in
(Format.pp_print_flush ppf () ;
Format.pp_set_formatter_out_functions
ppf
{
out_string =
(fun s b a ->
if s = "\000\000\000" then skip := true
else if s = "\255\255\255" then skip := false
else if not !skip then orig_out_functions.out_string s b a);
out_spaces = (fun n -> if not !skip then orig_out_functions.out_spaces n);
out_newline =
(fun () -> if not !skip then orig_out_functions.out_newline ());
out_flush = (fun () -> if not !skip then orig_out_functions.out_flush ());
out_indent = orig_out_functions.out_indent;
} ;
let levels = ref [] in
let setup_level (level, op) =
if op level verbosity then Format.fprintf ppf "@<0>%s" "\255\255\255"
else Format.fprintf ppf "@<0>%s" "\000\000\000"
in
let push_level level =
levels := level :: !levels ;
setup_level level
in
let pop_level () =
match !levels with
| _ :: level :: rest ->
levels := level :: rest ;
setup_level level
| [_] | [] -> Stdlib.failwith "Tezos_clic: unclosed verbosity tag"
in
push_level (Terse, ( <= )) ;
let push_level_tag = function
| Format.String_tag tag ->
let push op = function
| "full" -> push_level (Full, op)
| "details" -> push_level (Details, op)
| "short" -> push_level (Short, op)
| "terse" -> push_level (Terse, op)
| tag ->
Stdlib.failwith
("Tezos_clic: invalid semantic string tag <" ^ tag ^ ">")
in
if String.length tag > 0 && tag.[0] = '=' then
push ( = ) (String.sub tag 1 (String.length tag - 1))
else if String.length tag > 0 && tag.[0] = '-' then
push ( > ) (String.sub tag 1 (String.length tag - 1))
else push ( <= ) tag
| _stag -> Stdlib.failwith "Tezos_clic: invalid semantic tag"
in
let pop_level_tag = function
| Format.String_tag "full"
| Format.String_tag "details"
| Format.String_tag "short"
| Format.String_tag "terse"
| Format.String_tag "-full"
| Format.String_tag "-details"
| Format.String_tag "-short"
| Format.String_tag "-terse"
| Format.String_tag "=full"
| Format.String_tag "=details"
| Format.String_tag "=short"
| Format.String_tag "=terse" ->
pop_level ()
| Format.String_tag tag ->
Stdlib.failwith
("Tezos_clic: invalid semantic string tag <" ^ tag ^ ">")
| _stag -> Stdlib.failwith "Tezos_clic: invalid semantic tag"
in
match format with
| Ansi ->
let color_num = function
| `Auto -> None
| `Black -> Some 0
| `Red -> Some 1
| `Green -> Some 2
| `Yellow -> Some 3
| `Blue -> Some 4
| `Magenta -> Some 5
| `Cyan -> Some 6
| `White -> Some 7
in
let ansi_format ppf (fg, bg, b, u) =
Format.fprintf ppf "@<0>%s" "\027[0m" ;
match
(match color_num fg with
| Some n -> [string_of_int (30 + n)]
| None -> [])
@ (match color_num bg with
| Some n -> [string_of_int (40 + n)]
| None -> [])
@ (if b then ["1"] else [])
@ if u then ["4"] else []
with
| [] -> ()
| l -> Format.fprintf ppf "@<0>%s" ("\027[" ^ String.concat ";" l ^ "m")
in
let ansi_stack = ref [(`Auto, `Auto, false, false)] in
let push_ansi_format (fg, bg, b, u) =
let format =
match !ansi_stack with
| (pfg, pbg, pb, pu) :: _ ->
( Option.value ~default:pfg fg,
Option.value ~default:pbg bg,
pb || b,
pu || u )
| [] -> assert false
in
ansi_stack := format :: !ansi_stack ;
Format.fprintf ppf "@<0>%a" ansi_format format
in
let pop_ansi_format () =
Format.fprintf ppf "@<0>%s" "\027[0m" ;
match !ansi_stack with
| _ :: format :: rest ->
ansi_stack := format :: rest ;
Format.fprintf ppf "@<0>%a" ansi_format format
| [_] | [] -> Stdlib.failwith "Tezos_clic: unclosed ansi format"
in
Format.pp_set_formatter_stag_functions
ppf
{
mark_open_stag = (fun _ -> "");
mark_close_stag = (fun _ -> "");
print_open_stag =
(function
| Format.String_tag "title" ->
push_ansi_format (None, None, true, true)
| Format.String_tag "commandline" -> Format.fprintf ppf "@[<hov 4>"
| Format.String_tag "commanddoc" -> Format.fprintf ppf " @[<v 0>"
| Format.String_tag "opt" ->
push_ansi_format (Some `Green, None, false, false)
| Format.String_tag "arg" ->
push_ansi_format (Some `Yellow, None, false, false) ;
Format.fprintf ppf "<"
| Format.String_tag "kwd" ->
push_ansi_format (None, None, false, true)
| Format.String_tag "error" ->
push_ansi_format (Some `Red, None, true, true)
| Format.String_tag "warning" ->
push_ansi_format (Some `Yellow, None, true, true)
| Format.String_tag "hilight" ->
push_ansi_format (Some `White, Some `Yellow, true, true)
| Format.String_tag "list" -> Format.fprintf ppf " @[<v 0>"
| Format.String_tag "command" -> Format.fprintf ppf "@[<v 0>"
| Format.String_tag "document" -> Format.fprintf ppf "@[<v 0>"
| other -> push_level_tag other);
print_close_stag =
(function
| Format.String_tag "title" ->
Format.fprintf ppf ":" ;
pop_ansi_format ()
| Format.String_tag "commandline" -> Format.fprintf ppf "@]"
| Format.String_tag "commanddoc" -> Format.fprintf ppf "@]"
| Format.String_tag "opt" -> pop_ansi_format ()
| Format.String_tag "arg" ->
Format.fprintf ppf ">" ;
pop_ansi_format ()
| Format.String_tag "kwd" -> pop_ansi_format ()
| Format.String_tag "error" -> pop_ansi_format ()
| Format.String_tag "warning" -> pop_ansi_format ()
| Format.String_tag "hilight" -> pop_ansi_format ()
| Format.String_tag "command" | Format.String_tag "list" ->
Format.fprintf ppf "@]"
| Format.String_tag "document" -> Format.fprintf ppf "@]"
| other -> pop_level_tag other);
} ;
Format.pp_set_print_tags ppf true
| Plain ->
Format.pp_set_formatter_stag_functions
ppf
{
mark_open_stag = (fun _ -> "");
mark_close_stag = (fun _ -> "");
print_open_stag =
(function
| Format.String_tag "title" -> ()
| Format.String_tag "commandline" -> Format.fprintf ppf "@[<hov 4>"
| Format.String_tag "commanddoc" -> Format.fprintf ppf " @[<v 0>"
| Format.String_tag "opt" -> ()
| Format.String_tag "arg" -> Format.fprintf ppf "<"
| Format.String_tag "kwd" -> ()
| Format.String_tag "hilight" -> ()
| Format.String_tag "error" -> ()
| Format.String_tag "warning" -> ()
| Format.String_tag "list" -> Format.fprintf ppf " @[<v 0>"
| Format.String_tag "command" -> Format.fprintf ppf "@[<v 0>"
| Format.String_tag "document" -> Format.fprintf ppf "@[<v 0>"
| other -> push_level_tag other);
print_close_stag =
(function
| Format.String_tag "title" -> Format.fprintf ppf ":"
| Format.String_tag "commandline" -> Format.fprintf ppf "@]"
| Format.String_tag "commanddoc" -> Format.fprintf ppf "@]"
| Format.String_tag "opt" -> ()
| Format.String_tag "arg" -> Format.fprintf ppf ">"
| Format.String_tag "kwd" -> ()
| Format.String_tag "error" -> ()
| Format.String_tag "warning" -> ()
| Format.String_tag "hilight" -> ()
| Format.String_tag "command" | Format.String_tag "list" ->
Format.fprintf ppf "@]"
| Format.String_tag "document" -> Format.fprintf ppf "@]"
| other -> pop_level_tag other);
} ;
Format.pp_set_print_tags ppf true
| Html ->
Format.pp_set_formatter_stag_functions
ppf
{
mark_open_stag = (fun _ -> "");
mark_close_stag = (fun _ -> "");
print_open_stag =
(function
| Format.String_tag "title" -> Format.fprintf ppf "\003h3\004"
| Format.String_tag "commandline" ->
Format.fprintf ppf "\003div class='cmdline'\004@[<h>"
| Format.String_tag "commanddoc" ->
Format.fprintf ppf "\003div class='cmddoc'\004"
| Format.String_tag "opt" ->
Format.fprintf ppf "\003span class='opt'\004"
| Format.String_tag "arg" ->
Format.fprintf ppf "\003span class='arg'\004"
| Format.String_tag "kwd" ->
Format.fprintf ppf "\003span class='kwd'\004"
| Format.String_tag "hilight" -> ()
| Format.String_tag "error" -> ()
| Format.String_tag "warning" -> ()
| Format.String_tag "list" -> Format.fprintf ppf "\003ul\004@\n"
| Format.String_tag "command" -> Format.fprintf ppf "\003li\004@\n"
| Format.String_tag "document" ->
Format.fprintf
ppf
"@[<v 0>\003style\004.cmdline { font-family: monospace \
}.cmddoc { white-space: pre-wrap ; font-family: monospace; \
line-height: 170%%; margin: 0 0 20px 0 }.cmdline { \
background: #343131; padding: 2px 8px; border-radius:10px; \
color: white; margin: 5px; }.cmdline+.cmddoc { margin: \
-5px 5px 0 20px; padding: 5px }.opt,.arg { background: \
#343131; font-weight: bold; padding: 2px 4px; \
border-radius:5px; }.kwd { font-weight: bold; } .opt { \
color:#CF0; background: #460; } .arg { color: #CEF; \
background: #369; }\003/style\004@\n"
| other -> push_level_tag other);
print_close_stag =
(function
| Format.String_tag "title" -> Format.fprintf ppf "\003/h3\004@\n"
| Format.String_tag "commandline" ->
Format.fprintf ppf "@]\003/div\004@\n"
| Format.String_tag "commanddoc" ->
Format.fprintf ppf "\003/div\004@\n"
| Format.String_tag "opt"
| Format.String_tag "arg"
| Format.String_tag "kwd" ->
Format.fprintf ppf "\003/span\004"
| Format.String_tag "error"
| Format.String_tag "warning"
| Format.String_tag "hilight" ->
()
| Format.String_tag "list" -> Format.fprintf ppf "\003/ul\004@\n"
| Format.String_tag "command" ->
Format.fprintf ppf "\003/li\004@\n"
| Format.String_tag "document" -> Format.fprintf ppf "@]"
| other -> pop_level_tag other);
} ;
let orig_out_functions = Format.pp_get_formatter_out_functions ppf () in
Format.pp_set_formatter_out_functions
ppf
{
orig_out_functions with
out_string =
(fun s i j ->
let buf = Buffer.create (j - i) in
for n = i to j - 1 do
match s.[n] with
| '\003' -> Buffer.add_char buf '<'
| '\004' -> Buffer.add_char buf '>'
| '>' -> Buffer.add_string buf ">"
| '<' -> Buffer.add_string buf "<"
| c -> Buffer.add_char buf c
done ;
let s' = Buffer.contents buf in
orig_out_functions.out_string s' 0 (String.length s'));
} ;
Format.pp_set_print_tags ppf true) ;
orig_state
let restore_formatter ppf (out_functions, tag_functions, tags) =
Format.pp_print_flush ppf () ;
Format.pp_set_formatter_out_functions ppf out_functions ;
Format.pp_set_formatter_stag_functions ppf tag_functions ;
Format.pp_set_print_tags ppf tags
let usage_internal ppf ~executable_name ~global_options ?(highlights = [])
commands =
let by_group = group_commands commands in
let print_groups =
Format.pp_print_list
~pp_sep:(fun ppf () -> Format.fprintf ppf "@,@,")
(print_group (fun ppf (Ex command) ->
print_command ?prefix:None ~highlights ppf command))
in
Format.fprintf
ppf
"@{<document>@{<title>Usage@}@,\
@{<list>@{<command>@{<commandline>%s [@{<opt>global options@}] \
@{<kwd>command@} [@{<opt>command options@}]@}@}@,\
@{<command>@{<commandline>%s @{<opt>--help@} (for global options)@}@}@,\
@{<command>@{<commandline>%s [@{<opt>global options@}] @{<kwd>command@} \
@{<opt>--help@} (for command options)@}@}@,\
@{<command>@{<commandline>%s @{<opt>--version@} (for version \
information)@}@}@}@,\
@,\
@{<title>To browse the documentation@}@,\
@{<list>@{<command>@{<commandline>%s [@{<opt>global options@}] \
@{<kwd>man@} (for a list of commands)@}@}@,\
@{<command>@{<commandline>%s [@{<opt>global options@}] @{<kwd>man@} \
@{<opt>-v 3@} (for the full manual)@}@}@}@,\
@,\
@{<title>Global options (must come before the command)@}@,\
@{<commanddoc>%a@}%a%a@}@."
executable_name
executable_name
executable_name
executable_name
executable_name
executable_name
print_options_detailed
global_options
(fun ppf () -> if by_group <> [] then Format.fprintf ppf "@,@,")
()
print_groups
by_group
let constant c = Constant c
let arg ~doc ?short ~long ~placeholder kind =
Arg {doc; label = {long; short}; placeholder; kind}
let default_arg ~doc ?short ~long ~placeholder ~default kind =
DefArg {doc; placeholder; label = {long; short}; kind; default}
let map_arg ~f:converter spec = Map {spec; converter}
let args1 a = a
let args2 a b = Pair (a, b)
let args3 a b c =
map_arg
~f:(fun _ ((a, b), c) -> Lwt_result_syntax.return (a, b, c))
(args2 (args2 a b) c)
let args4 a b c d =
map_arg
~f:(fun _ ((a, b), (c, d)) -> Lwt_result_syntax.return (a, b, c, d))
(args2 (args2 a b) (args2 c d))
let args5 a b c d e =
map_arg
~f:(fun _ ((a, b, c, d), e) -> Lwt_result_syntax.return (a, b, c, d, e))
(args2 (args4 a b c d) e)
let args6 a b c d e f =
map_arg
~f:(fun _ ((a, b, c, d), (e, f)) ->
Lwt_result_syntax.return (a, b, c, d, e, f))
(args2 (args4 a b c d) (args2 e f))
let args7 a b c d e f g =
map_arg
~f:(fun _ ((a, b, c, d), (e, f, g)) ->
Lwt_result_syntax.return (a, b, c, d, e, f, g))
(args2 (args4 a b c d) (args3 e f g))
let args8 a b c d e f g h =
map_arg
~f:(fun _ ((a, b, c, d), (e, f, g, h)) ->
Lwt_result_syntax.return (a, b, c, d, e, f, g, h))
(args2 (args4 a b c d) (args4 e f g h))
let args9 a b c d e f g h i =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h), i) ->
Lwt_result_syntax.return (a, b, c, d, e, f, g, h, i))
(args2 (args8 a b c d e f g h) i)
let args10 a b c d e f g h i j =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h), (i, j)) ->
Lwt_result_syntax.return (a, b, c, d, e, f, g, h, i, j))
(args2 (args8 a b c d e f g h) (args2 i j))
let args11 a b c d e f g h i j k =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h), (i, j, k)) ->
Lwt_result_syntax.return (a, b, c, d, e, f, g, h, i, j, k))
(args2 (args8 a b c d e f g h) (args3 i j k))
let args12 a b c d e f g h i j k l =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h), (i, j, k, l)) ->
Lwt_result_syntax.return (a, b, c, d, e, f, g, h, i, j, k, l))
(args2 (args8 a b c d e f g h) (args4 i j k l))
let args13 a b c d e f g h i j k l m =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h), (i, j, k, l, m)) ->
Lwt_result_syntax.return (a, b, c, d, e, f, g, h, i, j, k, l, m))
(args2 (args8 a b c d e f g h) (args5 i j k l m))
let args14 a b c d e f g h i j k l m n =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h), (i, j, k, l, m, n)) ->
Lwt_result_syntax.return (a, b, c, d, e, f, g, h, i, j, k, l, m, n))
(args2 (args8 a b c d e f g h) (args6 i j k l m n))
let args15 a b c d e f g h i j k l m n o =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h), (i, j, k, l, m, n, o)) ->
Lwt_result_syntax.return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o))
(args2 (args8 a b c d e f g h) (args7 i j k l m n o))
let args16 a b c d e f g h i j k l m n o p =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h), (i, j, k, l, m, n, o, p)) ->
Lwt_result_syntax.return (a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p))
(args2 (args8 a b c d e f g h) (args8 i j k l m n o p))
let args17 a b c d e f g h i j k l m n o p q =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p), q) ->
Lwt_result_syntax.return
(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q))
(args2 (args16 a b c d e f g h i j k l m n o p) q)
let args18 a b c d e f g h i j k l m n o p q r =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p), (q, r)) ->
Lwt_result_syntax.return
(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r))
(args2 (args16 a b c d e f g h i j k l m n o p) (args2 q r))
let args19 a b c d e f g h i j k l m n o p q r s =
map_arg
~f:(fun _ ((a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p), (q, r, s)) ->
Lwt_result_syntax.return
(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s))
(args2 (args16 a b c d e f g h i j k l m n o p) (args3 q r s))
let switch ~doc ?short ~long () = Switch {doc; label = {long; short}}
(* Argument parsing *)
let rec parse_arg :
type a ctx.
?command:_ command ->
(a, ctx) arg ->
string list StringMap.t ->
ctx ->
a tzresult Lwt.t =
fun ?command spec args_dict ctx ->
let open Lwt_result_syntax in
match spec with
| Arg {label = {long; short = _}; kind = {converter; _}; _} -> (
match StringMap.find_opt long args_dict with
| None | Some [] -> return_none
| Some [s] ->
let+ x =
trace_eval (fun () -> Bad_option_argument ("--" ^ long, command))
@@ converter ctx s
in
Some x
| Some (_ :: _) -> tzfail (Multiple_occurrences ("--" ^ long, command)))
| DefArg {label = {long; short = _}; kind = {converter; _}; default; _} -> (
let*! r = converter ctx default in
match r with
| Error _ ->
invalid_arg
(Format.sprintf
"Value provided as default for '%s' could not be parsed by \
converter function."
long)
| Ok default -> (
match StringMap.find_opt long args_dict with
| None | Some [] -> return default
| Some [s] ->
trace (Bad_option_argument (long, command)) (converter ctx s)
| Some (_ :: _) -> tzfail (Multiple_occurrences (long, command))))
| Switch {label = {long; short = _}; _} -> (
match StringMap.find_opt long args_dict with
| None | Some [] -> return_false
| Some [_] -> return_true
| Some (_ :: _) -> tzfail (Multiple_occurrences (long, command)))
| Constant c -> return c
| Pair (speca, specb) ->
let* arga = parse_arg ?command speca args_dict ctx in
let* argb = parse_arg ?command specb args_dict ctx in
return (arga, argb)
| Map {spec; converter} ->
let* arg = parse_arg ?command spec args_dict ctx in
converter ctx arg
let empty_args_dict = StringMap.empty
let rec make_arities_dict :
type a b.
(a, b) arg -> (int * string) StringMap.t -> (int * string) StringMap.t =
fun arg acc ->
let add {long; short} num =
(match short with
| None -> acc
| Some c -> StringMap.add ("-" ^ String.make 1 c) (num, long) acc)
|> StringMap.add ("-" ^ long) (num, long)
|> StringMap.add ("--" ^ long) (num, long)
in
match arg with
| Arg {label; _} -> add label 1
| DefArg {label; _} -> add label 1
| Switch {label; _} -> add label 0
| Constant _c -> acc
| Pair (speca, specb) ->
let acc = make_arities_dict speca acc in
let acc = make_arities_dict specb acc in
acc
| Map {spec; _} -> make_arities_dict spec acc
type error += Version : error
type error += Help : 'a command option -> error
let check_help_flag ?command =
let open Lwt_result_syntax in
function ("-h" | "--help") :: _ -> tzfail (Help command) | _ -> return_unit
let check_version_flag =
let open Lwt_result_syntax in
function
(* No "-v", it is taken by man output verbosity *)
| "--version" :: _ -> tzfail Version
| _ -> return_unit
let add_occurrence long value acc =
match StringMap.find_opt long acc with
| Some v -> StringMap.add long v acc
| None -> StringMap.add long [value] acc
let make_args_dict_consume ?command spec args =
let open Lwt_result_syntax in
let rec make_args_dict completing arities acc args =
let* () = check_help_flag ?command args in
let* () = check_version_flag args in
match args with
| [] -> return (acc, [])
| arg :: tl ->
if String.length arg > 0 && arg.[0] = '-' then
match StringMap.find arg arities with
| Some (arity, long) -> (
let* () = check_help_flag ?command tl in
match (arity, tl) with
| 0, tl' ->
make_args_dict
completing
arities
(add_occurrence long "" acc)
tl'
| 1, value :: tl' ->
make_args_dict
completing
arities
(add_occurrence long value acc)
tl'
| 1, [] when completing -> return (acc, [])
| 1, [] -> tzfail (Option_expected_argument (arg, None))
| _, _ ->
Stdlib.failwith
"cli_entries: Arguments with arity not equal to 1 or 0 \
unsupported")
| None -> tzfail (Unknown_option (arg, None))
else return (acc, args)
in
make_args_dict
false
(make_arities_dict spec StringMap.empty)
StringMap.empty
args
let make_args_dict_filter ?command spec args =
let open Lwt_result_syntax in
let rec make_args_dict arities (dict, other_args) args =
let* () = check_help_flag ?command args in
match args with
| [] -> return (dict, other_args)
| arg :: tl -> (
match StringMap.find arg arities with
| Some (arity, long) -> (
let* () = check_help_flag ?command tl in
match (arity, tl) with
| 0, tl ->
make_args_dict
arities
(add_occurrence long "" dict, other_args)
tl
| 1, value :: tl' ->
make_args_dict
arities
(add_occurrence long value dict, other_args)
tl'
| 1, [] -> tzfail (Option_expected_argument (arg, command))
| _, _ ->
Stdlib.failwith
"cli_entries: Arguments with arity not equal to 1 or 0 \
unsupported")
| None -> make_args_dict arities (dict, arg :: other_args) tl)
in
let+ dict, remaining =
make_args_dict
(make_arities_dict spec StringMap.empty)
(StringMap.empty, [])
args
in
(dict, List.rev remaining)
(* Some combinators for writing commands concisely. *)
let param ~name ~desc kind next = Param (name, desc, kind, next)
let seq_of_param param =
match param Stop with
| Param (n, desc, parameter, Stop) -> Seq (n, desc, parameter)
| _ -> invalid_arg "Tezos_clic.seq_of_param"
let non_terminal_seq ~suffix param next =
match (suffix, param Stop) with
| [], _ -> invalid_arg "Tezos_clic.non_terminal_seq: empty suffix"
| _, Param (n, desc, parameter, Stop) ->
NonTerminalSeq (n, desc, parameter, suffix, next)
| _ -> invalid_arg "Tezos_clic.non_terminal_seq"
let prefix keyword next = Prefix (keyword, next)
let rec fixed = function [] -> Stop | n :: r -> Prefix (n, fixed r)
let rec prefixes p next =
match p with [] -> next | n :: r -> Prefix (n, prefixes r next)
let stop = Stop
let no_options = Constant ()
let command ?group ~desc options params handler =
Command {params; options; handler; desc; group; conv = (fun x -> x)}
(* Param combinators *)
let string ~name ~desc next =
param
~name
~desc
{converter = (fun _ s -> Lwt.return_ok s); autocomplete = None}
next
let string_contains ~needle ~haystack =
Option.catch
~catch_only:(function Not_found -> true | _ -> false)
(fun () -> Re.Str.search_forward (Re.Str.regexp_string needle) haystack 0)
let rec search_params_prefix : type a arg. string -> (a, arg) params -> bool =
fun prefix -> function
| Prefix (keyword, next) -> (
match string_contains ~needle:prefix ~haystack:keyword with
| None -> search_params_prefix prefix next
| Some _ -> true)
| Param (_, _, _, next) -> search_params_prefix prefix next
| Stop -> false
| Seq _ -> false
| NonTerminalSeq (_, _, _, suffix, next) ->
List.exists
(fun keyword ->
match string_contains ~needle:prefix ~haystack:keyword with
| None -> false
| Some _ -> true)
suffix
|| search_params_prefix prefix next
let search_command keyword (Command {params; _}) =
search_params_prefix keyword params
(* Command execution *)
let exec (type ctx)
(Command {options = options_spec; params = spec; handler; conv; _} as
command) (ctx : ctx) params args_dict =
let open Lwt_result_syntax in
let rec exec :
type ctx a.
int -> ctx -> (a, ctx) params -> a -> string list -> unit tzresult Lwt.t =
fun i ctx spec cb params ->
match (spec, params) with
| Stop, _ -> cb ctx
| Seq (_, _, {converter; _}), seq ->
let rec do_seq i acc = function
| [] -> return (List.rev acc)
| p :: rest ->
let* v =
Error_monad.catch_es (fun () -> converter ctx p)
|> trace (Bad_argument (i, p))
in
do_seq (succ i) (v :: acc) rest
in
let* parsed = do_seq i [] seq in
cb parsed ctx
| NonTerminalSeq (_, _, {converter; _}, suffix, next), seq ->
let rec do_seq i acc = function
| [] -> return (List.rev acc, [])
| p :: rest as params ->
(* try to match suffix first *)
let rec match_suffix = function
| param :: params, suffix :: suffixes when param = suffix ->
match_suffix (params, suffixes)
| params, [] ->
(* all of the suffix parts have been matched *)
(params, true)
| _, _ -> (params, false)
in
let unmatched_rest, matched = match_suffix (params, suffix) in
if matched then return (List.rev acc, unmatched_rest)
else
(* if suffix is not match, try to continue with the sequence *)
Error_monad.catch_es (fun () ->
let* v = converter ctx p in
do_seq (succ i) (v :: acc) rest)
in
let* parsed, rest = do_seq i [] seq in
exec (succ i) ctx next (cb parsed) rest
| Prefix (n, next), p :: rest when n = p -> exec (succ i) ctx next cb rest
| Param (_, _, {converter; _}, next), p :: rest ->
let* v =
Error_monad.catch_es (fun () -> converter ctx p)
|> trace (Bad_argument (i, p))
in
exec (succ i) ctx next (cb v) rest
| _ -> Stdlib.failwith "cli_entries internal error: exec no case matched"
in
let ctx = conv ctx in
let* parsed_options = parse_arg ~command options_spec args_dict ctx in
exec 1 ctx spec (handler parsed_options) params
[@@@ocaml.warning "-30"]
(* Command dispatch tree *)
type 'arg level = {
stop : 'arg command option;
prefix : (string * 'arg tree) list;
}
and 'arg param_level = {
stop : 'arg command option;
autocomplete : ('arg -> string list tzresult Lwt.t) option;
tree : 'arg tree;
}
and 'arg non_terminal_seq_level = {
stop : 'arg command option;
autocomplete : ('arg -> string list tzresult Lwt.t) option;
tree : 'arg tree;
name : string;
desc : string;
suffix : string list;
}
and 'ctx tree =
| TPrefix : 'ctx level -> 'ctx tree
| TParam : 'ctx param_level -> 'ctx tree
| TStop : 'ctx command -> 'ctx tree
| TSeq :
'ctx command * ('ctx -> string list tzresult Lwt.t) option
-> 'ctx tree
| TNonTerminalSeq : 'ctx non_terminal_seq_level -> 'ctx tree
| TEmpty : 'ctx tree
let insert_in_dispatch_tree : type ctx. ctx tree -> ctx command -> ctx tree =
fun root (Command {params; conv; _} as command) ->
let rec insert_tree :
type a ictx. (ctx -> ictx) -> ctx tree -> (a, ictx) params -> ctx tree =
fun conv t c ->
let insert_tree t c = insert_tree conv t c in
let rec suffix_to_params suffix next =
match suffix with
| suffix :: suffixes -> Prefix (suffix, suffix_to_params suffixes next)
| [] -> next
in
let suffix_to_tree suffix next =
insert_tree TEmpty (suffix_to_params suffix next)
in
let conv_autocomplete = Option.map (fun a c -> a (conv c)) in
match (t, c) with
| TEmpty, Stop -> TStop command
| TEmpty, Seq (_, _, {autocomplete; _}) ->
TSeq (command, conv_autocomplete autocomplete)
| TEmpty, Param (_, _, {autocomplete; _}, next) ->
let autocomplete = conv_autocomplete autocomplete in
TParam {tree = insert_tree TEmpty next; stop = None; autocomplete}
| TEmpty, NonTerminalSeq (name, desc, {autocomplete; _}, suffix, next) ->
let autocomplete = conv_autocomplete autocomplete in
let tree = suffix_to_tree suffix next in
TNonTerminalSeq {stop = None; tree; autocomplete; suffix; name; desc}
| TEmpty, Prefix (n, next) ->
TPrefix {stop = None; prefix = [(n, insert_tree TEmpty next)]}
| TStop cmd, Param (_, _, {autocomplete; _}, next) ->
let autocomplete = conv_autocomplete autocomplete in
TParam {tree = insert_tree TEmpty next; stop = Some cmd; autocomplete}
| TStop cmd, Prefix (n, next) ->
TPrefix {stop = Some cmd; prefix = [(n, insert_tree TEmpty next)]}
| TStop cmd, NonTerminalSeq (name, desc, {autocomplete; _}, suffix, next) ->
let autocomplete = conv_autocomplete autocomplete in
let tree = suffix_to_tree suffix next in
TNonTerminalSeq
{stop = Some cmd; tree; autocomplete; suffix; name; desc}
| TParam t, Param (_, _, _, next) ->
TParam {t with tree = insert_tree t.tree next}
| TPrefix ({prefix; _} as l), Prefix (n, next) ->
let rec insert_prefix = function
| [] -> [(n, insert_tree TEmpty next)]
| (n', t) :: rest when n = n' -> (n, insert_tree t next) :: rest
| item :: rest -> item :: insert_prefix rest
in
TPrefix {l with prefix = insert_prefix prefix}
| TPrefix ({stop = None; _} as l), Stop ->
TPrefix {l with stop = Some command}
| TParam ({stop = None; _} as l), Stop ->
TParam {l with stop = Some command}
| TParam t, Prefix (_n, next) ->
TParam {t with tree = insert_tree t.tree next}
| TNonTerminalSeq t, NonTerminalSeq (n, desc, _, suffix, next) ->
if
n <> t.name || desc <> t.desc || t.suffix <> suffix
(* we should match the parameter too but this would require a bit of refactoring*)
then
Stdlib.failwith
"Command cannot have different non_terminal_seq_level at the same \
position"
else
let params = suffix_to_params suffix next in
TNonTerminalSeq {t with tree = insert_tree t.tree params}
| _, _ ->
Stdlib.failwith
(Format.asprintf
"Tezos_clic.Command_tree.insert: conflicting commands \"%a\""
(fun ppf (Command {params; options; _}) ->
print_commandline ppf ([], options, params))
command)
in
insert_tree conv root params
let make_dispatch_tree commands =
List.fold_left insert_in_dispatch_tree TEmpty commands
let rec gather_commands ?(acc = []) tree =
match tree with
| TEmpty -> acc
| TSeq (c, _) | TStop c -> c :: acc
| TPrefix {stop; prefix} ->
gather_assoc
~acc:(match stop with None -> acc | Some c -> c :: acc)
prefix
| TParam {tree; stop; _} | TNonTerminalSeq {tree; stop; _} ->
gather_commands
tree
~acc:(match stop with None -> acc | Some c -> c :: acc)
and gather_assoc ?(acc = []) trees =
List.fold_left (fun acc (_, tree) -> gather_commands tree ~acc) acc trees
let find_command tree initial_arguments =
let open Lwt_result_syntax in
let is_short_option s =
String.length s = 2
&& s.[0] = '-'
&& match s.[1] with 'a' .. 'z' | 'A' .. 'Z' -> true | _ -> false
in
let is_long_option s = String.length s >= 2 && s.[0] = '-' && s.[1] = '-' in
let is_option s = is_short_option s || is_long_option s in
let rec traverse tree arguments acc =
match (tree, arguments) with
| ( ( TStop _ | TSeq _
| TNonTerminalSeq {stop = Some _; _}
| TPrefix {stop = Some _; _}
| TParam {stop = Some _; _} ),
("-h" | "--help") :: _ ) -> (
match gather_commands tree with
| [] -> assert false
| [command] -> tzfail (Help (Some command))
| more -> tzfail (Unterminated_command (initial_arguments, more)))
| TStop c, [] -> return (c, empty_args_dict, initial_arguments)
| TStop (Command {options; _} as command), remaining -> (
let* args_dict, unparsed =
make_args_dict_filter ~command options remaining
in
match unparsed with
| [] -> return (command, args_dict, initial_arguments)
| hd :: _ ->
if String.length hd > 0 && hd.[0] = '-' then
tzfail (Unknown_option (hd, Some command))
else tzfail (Extra_arguments (unparsed, command)))
| TSeq ((Command {options; _} as command), _), remaining ->
if
List.exists
(function "-h" | "--help" -> true | _ -> false)
remaining
then tzfail (Help (Some command))
else
let+ dict, remaining =
make_args_dict_filter ~command options remaining
in
(command, dict, List.rev_append acc remaining)
| TNonTerminalSeq {stop = None; _}, ([] | ("-h" | "--help") :: _) ->
tzfail (Unterminated_command (initial_arguments, gather_commands tree))
| TNonTerminalSeq {stop = Some c; _}, [] ->
return (c, empty_args_dict, initial_arguments)
| ( (TNonTerminalSeq {tree; suffix; _} as nts),
(parameter :: arguments' as remaining) ) ->
(* try to match suffix first *)
let rec match_suffix matched_acc = function
| param :: params, suffix :: suffixes when param = suffix ->
match_suffix (param :: matched_acc) (params, suffixes)
| _, [] ->
(* all of the suffix parts have been matched *)
true
| _, _ -> false
in
let matched = match_suffix [] (remaining, suffix) in
if matched then
(* continue with the nested tree *)
traverse tree remaining acc
else
(* continue traversing with the current node (non-terminal sequence) *)
traverse nts arguments' (parameter :: acc)
| TPrefix {stop = Some cmd; _}, [] ->
return (cmd, empty_args_dict, initial_arguments)
| TPrefix {stop = None; prefix}, ([] | ("-h" | "--help") :: _) ->
tzfail (Unterminated_command (initial_arguments, gather_assoc prefix))
| TPrefix {prefix; _}, hd_arg :: tl -> (
match List.assoc ~equal:String.equal hd_arg prefix with
| None -> tzfail (Command_not_found (List.rev acc, gather_assoc prefix))
| Some tree' -> traverse tree' tl (hd_arg :: acc))
| TParam {stop = None; _}, ([] | ("-h" | "--help") :: _) ->
tzfail (Unterminated_command (initial_arguments, gather_commands tree))
| TParam {stop = Some c; _}, hd :: _ when is_option hd ->
(* If the argument looks like an option, we choose the "stop"
command. *)
traverse (TStop c) arguments acc
| TParam {stop = Some c; _}, [] ->
return (c, empty_args_dict, initial_arguments)
| TParam {tree; _}, parameter :: arguments' ->
traverse tree arguments' (parameter :: acc)
| TEmpty, _ -> tzfail (Command_not_found (List.rev acc, []))
in
traverse tree initial_arguments []
let get_arg {long; short} =
("--" ^ long)
:: (match short with None -> [] | Some c -> ["-" ^ String.make 1 c])
let rec list_args : type a ctx. (a, ctx) arg -> string list = function
| Constant _ -> []
| Arg {label; _} | DefArg {label; _} | Switch {label; _} -> get_arg label
| Pair (speca, specb) -> list_args speca @ list_args specb
| Map {spec; _} -> list_args spec
let complete_func autocomplete cctxt =
let open Lwt_result_syntax in
match autocomplete with
| None -> return_nil
| Some autocomplete -> autocomplete cctxt
let list_command_args (Command {options; _}) = list_args options
let rec remaining_spec : type a ctx. StringSet.t -> (a, ctx) arg -> string list
=
fun seen -> function
| Constant _ -> []
| Arg {label; _} | DefArg {label; _} | Switch {label; _} ->
if StringSet.mem label.long seen then [] else get_arg label
| Pair (speca, specb) -> remaining_spec seen speca @ remaining_spec seen specb
| Map {spec; _} -> remaining_spec seen spec
let complete_options (type ctx) continuation args args_spec ind (ctx : ctx) =
let arities = make_arities_dict args_spec StringMap.empty in
let rec complete_spec :
type a. string -> (a, ctx) arg -> string list option tzresult Lwt.t =
fun name ->
let open Lwt_result_syntax in
function
| Constant _ -> return_none
| DefArg {kind = {autocomplete; _}; label; _}
| Arg {kind = {autocomplete; _}; label; _}
when label.long = name ->
let* p = complete_func autocomplete ctx in
return_some p
| Switch {label; _} when label.long = name -> return_some []
| Arg _ | DefArg _ | Switch _ -> return_none
| Pair (speca, specb) -> (
let* resa = complete_spec name speca in
match resa with
| Some _ -> return resa
| None -> complete_spec name specb)
| Map {spec; _} -> complete_spec name spec
in
let rec help args ind seen =
let open Lwt_result_syntax in
match args with
| _ when ind = 0 ->
let+ cont_args = continuation args 0 in
cont_args @ remaining_spec seen args_spec
| [] -> Stdlib.failwith "cli_entries internal autocomplete error"
| arg :: tl -> (
match StringMap.find arg arities with
| Some (arity, long) -> (
let seen = StringSet.add long seen in
match (arity, tl) with
| 0, args when ind = 0 ->
let+ cont_args = continuation args 0 in
remaining_spec seen args_spec @ cont_args
| 0, args -> help args (ind - 1) seen
| 1, _ when ind = 1 ->
let* res = complete_spec arg args_spec in
return (Option.value ~default:[] res)
| 1, _ :: tl -> help tl (ind - 2) seen
| _ -> Stdlib.failwith "cli_entries internal error, invalid arity")
| None -> continuation args ind)
in
help args ind StringSet.empty
let complete_next_tree cctxt =
let open Lwt_result_syntax in
function
| TPrefix {stop; prefix} ->
return
((match stop with
| None -> []
| Some command -> list_command_args command)
@ List.map fst prefix)
| TSeq (command, autocomplete) ->
let+ completions = complete_func autocomplete cctxt in
completions @ list_command_args command
| TNonTerminalSeq {autocomplete; suffix; _} ->
let+ completions = complete_func autocomplete cctxt in
completions @ [WithExceptions.Option.get ~loc:__LOC__ @@ List.hd suffix]
| TParam {autocomplete; _} -> complete_func autocomplete cctxt
| TStop command -> return (list_command_args command)
| TEmpty -> return_nil
let rec args_starting_from_suffix original_suffix ind matched_args = function
| (s :: s_rest as suffix), a :: a_rest ->
if s = a then
args_starting_from_suffix
original_suffix
ind
(matched_args @ [a])
(s_rest, a_rest)
else if matched_args = [] then
(* Suffix not found on args head, check the rest of the args *)
args_starting_from_suffix
original_suffix
(ind - 1)
matched_args
(suffix, a_rest)
else
(* After there is a suffix match, the rest of the suffix has
to be matched in the following args, unless it's empty. *)
None
| unmatched_suffix, args
(* Partial or full suffix match found *)
when Compare.List_lengths.(unmatched_suffix < original_suffix) ->
Some (matched_args @ args, ind)
| _ -> None
let complete_tree cctxt tree index args =
let rec help tree args ind =
let open Lwt_result_syntax in
if ind = 0 then complete_next_tree cctxt tree
else
match (tree, args) with
| TSeq _, _ -> complete_next_tree cctxt tree
| (TNonTerminalSeq {tree; suffix; _} as this_tree), _ :: _tl -> (
match args_starting_from_suffix suffix ind [] (suffix, args) with
| Some (args, ind) -> help tree args ind
| _ -> complete_next_tree cctxt this_tree)
| TPrefix {prefix; _}, hd :: tl -> (
match List.assoc ~equal:String.equal hd prefix with
| None -> return_nil
| Some p -> help p tl (ind - 1))
| TParam {tree; _}, _ :: tl -> help tree tl (ind - 1)
| TStop (Command {options; conv; _}), args ->
complete_options (fun _ _ -> return_nil) args options ind (conv cctxt)
| (TParam _ | TPrefix _ | TNonTerminalSeq _), [] | TEmpty, _ -> return_nil
in
help tree args index
let autocompletion ~script ~cur_arg ~prev_arg ~args ~global_options commands
cctxt =
let open Lwt_result_syntax in
let tree = make_dispatch_tree commands in
let rec ind n = function
| [] -> None
| hd :: tl ->
if hd = prev_arg then
Some (Option.value ~default:(n + 1) (ind (n + 1) tl))
else ind (n + 1) tl
in
let+ completions =
if prev_arg = script then
let+ command_completions = complete_next_tree cctxt tree in
list_args global_options @ command_completions
else
match ind 0 args with
| None -> return_nil
| Some index ->
complete_options
(fun args ind -> complete_tree cctxt tree ind args)
args
global_options
index
cctxt
in
List.filter
(fun completion ->
Re.Str.(string_match (regexp_string cur_arg) completion 0))
completions
let parse_global_options global_options ctx args =
let open Lwt_result_syntax in
let* dict, remaining = make_args_dict_consume global_options args in
let* nested = parse_arg global_options dict ctx in
return (nested, remaining)
let dispatch commands ctx args =
let open Lwt_result_syntax in
let tree = make_dispatch_tree commands in
match args with
| []
when match tree with
| TPrefix {stop; _} -> stop = None
| TParam {stop; _} -> stop = None
| TStop _ -> false
| TSeq (_, _) -> false
| TNonTerminalSeq {stop; _} -> stop = None
| TEmpty -> true ->
tzfail (Help None)
| [("-h" | "--help")] -> tzfail (Help None)
| _ ->
let* command, args_dict, filtered_args = find_command tree args in
exec command ctx filtered_args args_dict
type error += No_manual_entry of string list
let manual_group = {name = "man"; title = "Access the documentation"}
let add_manual ~executable_name ~global_options format ppf commands =
let rec with_manual =
lazy
(commands
@ [
command
~group:manual_group
~desc:
"Print documentation of commands.\n\
Add search keywords to narrow list.\n\
Will display only the commands by default, unless [-verbosity \
<2|3>] is passed or the list of matching commands if less than \
3."
(args2
(arg
~doc:
"level of details\n\
0. Only shows command mnemonics, without documentation.\n\
1. Shows command mnemonics with short descriptions.\n\
2. Show commands and arguments with short descriptions\n\
3. Show everything"
~long:"verbosity"
~short:'v'
~placeholder:"0|1|2|3"
(parameter
~autocomplete:(fun _ -> Lwt.return_ok ["0"; "1"; "2"; "3"])
(fun _ arg ->
let open Lwt_result_syntax in
match arg with
| "0" -> return Terse
| "1" -> return Short
| "2" -> return Details
| "3" -> return Full
| _ -> failwith "Level of details out of range")))
(default_arg
~doc:"the manual's output format"
~placeholder:"plain|colors|html"
~long:"format"
~default:
(match format with
| Ansi -> "colors"
| Plain -> "plain"
| Html -> "html")
(parameter
~autocomplete:(fun _ ->
Lwt.return_ok ["colors"; "plain"; "html"])
(fun _ arg ->
let open Lwt_result_syntax in
match arg with
| "colors" -> return Ansi
| "plain" -> return Plain
| "html" -> return Html
| _ -> failwith "Unknown manual format"))))
(prefix
"man"
(seq_of_param
(string
~name:"keyword"
~desc:
"keyword to search for\n\
If several are given they must all appear in the \
command.")))
(fun (verbosity, format) keywords _ ->
let commands =
List.fold_left
(fun commands keyword ->
List.filter (search_command keyword) commands)
(Lazy.force with_manual)
keywords
in
let verbosity =
match verbosity with
| Some verbosity -> verbosity
| None when Compare.List_length_with.(commands <= 3) -> Full
| None -> Short
in
let open Lwt_result_syntax in
match commands with
| [] -> tzfail (No_manual_entry keywords)
| _ ->
let state = setup_formatter ppf format verbosity in
let commands = List.map (fun c -> Ex c) commands in
usage_internal
ppf
~executable_name
~global_options
~highlights:keywords
commands ;
restore_formatter ppf state ;
return_unit);
])
in
Lazy.force with_manual
let pp_cli_errors ppf ~executable_name ~global_options ~default errs =
let pp_one = function
| Bad_argument (i, v) ->
Format.fprintf ppf "Erroneous command line argument %d (%s)." i v ;
Some []
| Option_expected_argument (arg, command) ->
Format.fprintf
ppf
"Command line option @{<opt>%s@} expects an argument."
arg ;
Some (Option.fold ~some:(fun command -> [Ex command]) ~none:[] command)
| Bad_option_argument (arg, command) ->
Format.fprintf
ppf
"Wrong value for command line option @{<opt>%s@}."
arg ;
Some (Option.fold ~some:(fun command -> [Ex command]) ~none:[] command)
| Multiple_occurrences (arg, command) ->
Format.fprintf
ppf
"Command line option @{<opt>%s@} appears multiple times."
arg ;
Some (Option.fold ~some:(fun command -> [Ex command]) ~none:[] command)
| No_manual_entry [keyword] ->
Format.fprintf ppf "No manual entry that match @{<hilight>%s@}." keyword ;
Some []
| No_manual_entry (keyword :: keywords) ->
Format.fprintf
ppf
"No manual entry that match %a and @{<hilight>%s@}."
(Format.pp_print_list
~pp_sep:(fun ppf () -> Format.fprintf ppf ", ")
(fun ppf keyword -> Format.fprintf ppf "@{<hilight>%s@}" keyword))
keywords
keyword ;
Some []
| Unknown_option (option, command) ->
Format.fprintf ppf "Unexpected command line option @{<opt>%s@}." option ;
Some (Option.fold ~some:(fun command -> [Ex command]) ~none:[] command)
| Extra_arguments (extra, command) ->
Format.(
fprintf
ppf
"Extra command line arguments:@, @[<h>%a@]."
(pp_print_list ~pp_sep:pp_print_space pp_print_string)
extra) ;
Some [Ex command]
| Unterminated_command (_, commands) ->
Format.fprintf
ppf
"@[<v 2>Unterminated command, here are possible completions.@,%a@]"
(Format.pp_print_list (fun ppf (Command {params; options; _}) ->
print_commandline ppf ([], options, params)))
commands ;
Some (List.map (fun c -> Ex c) commands)
| Command_not_found ([], _all_commands) ->
Format.fprintf
ppf
"@[<v 0>Unrecognized command.@,\
Try using the @{<kwd>man@} command to get more information.@]" ;
Some []
| Command_not_found (_, commands) ->
Format.fprintf
ppf
"@[<v 0>Unrecognized command.@,\
Did you mean one of the following?@,\
\ @[<v 0>%a@]@]"
(Format.pp_print_list (fun ppf (Command {params; options; _}) ->
print_commandline ppf ([], options, params)))
commands ;
Some (List.map (fun c -> Ex c) commands)
| err ->
default ppf err ;
None
in
let rec pp acc errs =
let return command =
match (command, acc) with
| None, _ -> acc
| Some command, Some commands -> Some (command @ commands)
| Some command, None -> Some command
in
match errs with
| [] -> None
| [last] -> return (pp_one last)
| err :: errs ->
let acc = return (pp_one err) in
Format.fprintf ppf "@," ;
pp acc errs
in
Format.fprintf ppf "@[<v 2>@{<error>@{<title>Error@}@}@," ;
match pp None errs with
| None -> Format.fprintf ppf "@]@\n"
| Some commands ->
Format.fprintf
ppf
"@]@\n@\n@[<v 0>%a@]"
(fun ppf commands ->
usage_internal ppf ~executable_name ~global_options commands)
commands
let usage ppf ~executable_name ~global_options commands =
usage_internal
ppf
~executable_name
~global_options
(List.map (fun c -> Ex c) commands)
let map_command f (Command c) = Command {c with conv = (fun x -> c.conv (f x))}