1363 lines
57 KiB
OCaml
1363 lines
57 KiB
OCaml
(*****************************************************************************)
|
|
(* *)
|
|
(* 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
|
|
|
|
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' } =
|
|
{ converter = (fun ctx s ->
|
|
c1 ctx s >>= function
|
|
| 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 ->
|
|
a1 ctx >>=? fun r1 ->
|
|
a2 ctx >>=? fun r2 ->
|
|
return (List.concat [r1; r2]))
|
|
}
|
|
|
|
let map_parameter ~f { converter; autocomplete } =
|
|
{ converter = (fun ctx s -> converter ctx s >>|? f);
|
|
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
|
|
|
|
type ('a, 'arg) args =
|
|
| NoArgs : (unit, 'args) args
|
|
| AddArg : ('a, 'args) arg * ('b, 'args) args ->
|
|
('a * 'b, 'args) args
|
|
|
|
(* 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
|
|
|
|
type (_, _) options =
|
|
Argument : { spec : ('a, 'arg) args ;
|
|
converter : 'a -> 'b } -> ('b, 'arg) options
|
|
|
|
(* 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_occurences : string * 'ctx command option -> error
|
|
type error += Extra_arguments : string list * 'ctx command -> error
|
|
|
|
let trim s = (* config-file wokaround *)
|
|
TzString.split '\n' s |>
|
|
List.map String.trim |>
|
|
String.concat "\n"
|
|
|
|
let print_desc ppf doc =
|
|
let short, long = try
|
|
let len = String.index doc '\n' in
|
|
String.sub doc 0 len,
|
|
Some (String.sub doc (len + 1) (String.length doc - len - 1))
|
|
with _ -> doc, None 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 print_options_detailed (type ctx) =
|
|
let help_option : type a.Format.formatter -> (a, ctx) arg -> 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 _ -> () in
|
|
let rec help : type b. Format.formatter -> (b, ctx) args -> unit =
|
|
fun ppf -> function
|
|
| NoArgs -> ()
|
|
| AddArg (arg, NoArgs) ->
|
|
Format.fprintf ppf "%a"
|
|
help_option arg
|
|
| AddArg (arg, rest) ->
|
|
Format.fprintf ppf "%a@,%a"
|
|
help_option arg help rest
|
|
in help
|
|
|
|
let has_args : type a ctx. (a, ctx) args -> bool = function
|
|
| NoArgs -> false
|
|
| AddArg (_,_) -> true
|
|
|
|
let print_options_brief (type ctx) =
|
|
let help_option :
|
|
type 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 _ -> ()
|
|
in let rec help : type b. Format.formatter -> (b, ctx) args -> unit =
|
|
fun ppf -> function
|
|
| NoArgs -> ()
|
|
| AddArg (arg, NoArgs) ->
|
|
Format.fprintf ppf "%a" help_option arg
|
|
| AddArg (arg, rest) ->
|
|
Format.fprintf ppf "%a@ %a"
|
|
help_option arg help rest
|
|
in help
|
|
|
|
let print_highlight highlight_strings formatter str =
|
|
let rec print_string = function
|
|
| [] -> Format.fprintf formatter "%s" str
|
|
| regex :: tl ->
|
|
begin 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
|
|
end
|
|
in print_string (List.map Re.Str.regexp_string highlight_strings)
|
|
|
|
let print_commandline ppf (highlights, options, args) =
|
|
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
|
|
| 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) args -> 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) ;
|
|
begin match spec with
|
|
| NoArgs -> ()
|
|
| _ -> Format.fprintf ppf "@,%a" print_options_detailed spec
|
|
end
|
|
| Prefix (_, next) ->
|
|
print_params_detailed spec ppf next
|
|
| Param (n, desc, _, Stop) ->
|
|
Format.fprintf ppf "@{<arg>%s@}: %a"
|
|
n print_desc (trim desc);
|
|
begin match spec with
|
|
| NoArgs -> ()
|
|
| _ -> Format.fprintf ppf "@,%a" print_options_detailed spec
|
|
end
|
|
| 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 arg ctx. (arg, ctx) params -> (_, ctx) args -> bool
|
|
= fun params args ->
|
|
let rec help : (arg, ctx) params -> bool = function
|
|
| Stop -> has_args args
|
|
| Seq (_, _, _) -> 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 = Argument { spec ; _ } ; _ }) ->
|
|
if contains_params_args params spec
|
|
then
|
|
Format.fprintf ppf "@{<command>%a%a@{<short>@,@{<commanddoc>%a@,%a@}@}@}"
|
|
prefix ()
|
|
print_commandline (highlights, spec, params)
|
|
print_desc desc
|
|
(print_params_detailed spec) params
|
|
else
|
|
Format.fprintf ppf "@{<command>%a%a@{<short>@,@{<commanddoc>%a@}@}@}"
|
|
prefix ()
|
|
print_commandline (highlights, spec, 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 "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_tag_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_tag_functions ppf (),
|
|
Format.pp_get_print_tags ppf () in
|
|
begin
|
|
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
|
|
| [ _ ] | [] -> Pervasives.failwith "Clic: unclosed verbosity tag" in
|
|
push_level (Terse, (<=)) ;
|
|
let push_level_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 -> Pervasives.failwith ("Clic: invalid semantic tag <" ^ tag ^ ">") in
|
|
if String.length tag > 0 && String.get tag 0 = '=' then
|
|
push (=) (String.sub tag 1 (String.length tag - 1))
|
|
else if String.length tag > 0 && String.get tag 0 = '-' then
|
|
push (>) (String.sub tag 1 (String.length tag - 1))
|
|
else push (<=) tag in
|
|
let pop_level_tag = function
|
|
| "full" | "details" | "short" | "terse"
|
|
| "-full" | "-details" | "-short" | "-terse"
|
|
| "=full" | "=details" | "=short" | "=terse" -> pop_level ()
|
|
| tag -> Pervasives.failwith ("Clic: invalid semantic tag <" ^ 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.unopt ~default: pfg fg,
|
|
Option.unopt ~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
|
|
| [ _ ] | [] -> Pervasives.failwith "Clic: unclosed ansi format" in
|
|
Format.pp_set_formatter_tag_functions ppf
|
|
{ mark_open_tag = (fun _ -> "") ;
|
|
mark_close_tag = (fun _ -> "") ;
|
|
print_open_tag = begin function
|
|
| "title" -> push_ansi_format (None, None, true, true)
|
|
| "commandline" -> Format.fprintf ppf "@[<hov 4>"
|
|
| "commanddoc" -> Format.fprintf ppf " @[<v 0>"
|
|
| "opt" -> push_ansi_format (Some `Green, None, false, false)
|
|
| "arg" -> push_ansi_format (Some `Yellow, None, false, false) ; Format.fprintf ppf "<"
|
|
| "kwd" -> push_ansi_format (None, None, false, true)
|
|
| "error" -> push_ansi_format (Some `Red, None, true, true)
|
|
| "warning" -> push_ansi_format (Some `Yellow, None, true, true)
|
|
| "hilight" -> push_ansi_format (Some `White, Some `Yellow, true, true)
|
|
| "list" -> Format.fprintf ppf " @[<v 0>"
|
|
| "command" -> Format.fprintf ppf "@[<v 0>"
|
|
| "document" -> Format.fprintf ppf "@[<v 0>"
|
|
| other -> push_level_tag other
|
|
end ;
|
|
print_close_tag = begin function
|
|
| "title" -> Format.fprintf ppf ":" ; pop_ansi_format ()
|
|
| "commandline" -> Format.fprintf ppf "@]"
|
|
| "commanddoc" -> Format.fprintf ppf "@]"
|
|
| "opt" -> pop_ansi_format ()
|
|
| "arg" -> Format.fprintf ppf ">" ; pop_ansi_format ()
|
|
| "kwd" -> pop_ansi_format ()
|
|
| "error" -> pop_ansi_format ()
|
|
| "warning" -> pop_ansi_format ()
|
|
| "hilight" -> pop_ansi_format ()
|
|
| "command" | "list" -> Format.fprintf ppf "@]"
|
|
| "document" -> Format.fprintf ppf "@]"
|
|
| other -> pop_level_tag other
|
|
end } ;
|
|
Format.pp_set_print_tags ppf true
|
|
| Plain ->
|
|
Format.pp_set_formatter_tag_functions ppf
|
|
{ mark_open_tag = (fun _ -> "") ;
|
|
mark_close_tag = (fun _ -> "") ;
|
|
print_open_tag = begin function
|
|
| "title" -> ()
|
|
| "commandline" -> Format.fprintf ppf "@[<hov 4>"
|
|
| "commanddoc" -> Format.fprintf ppf " @[<v 0>"
|
|
| "opt" -> ()
|
|
| "arg" -> Format.fprintf ppf "<"
|
|
| "kwd" -> ()
|
|
| "hilight" -> ()
|
|
| "error" -> ()
|
|
| "warning" -> ()
|
|
| "list" -> Format.fprintf ppf " @[<v 0>"
|
|
| "command" -> Format.fprintf ppf "@[<v 0>"
|
|
| "document" -> Format.fprintf ppf "@[<v 0>"
|
|
| other -> push_level_tag other
|
|
end ;
|
|
print_close_tag = begin function
|
|
| "title" -> Format.fprintf ppf ":"
|
|
| "commandline" -> Format.fprintf ppf "@]"
|
|
| "commanddoc" -> Format.fprintf ppf "@]"
|
|
| "opt" -> ()
|
|
| "arg" -> Format.fprintf ppf ">"
|
|
| "kwd" -> ()
|
|
| "error" -> ()
|
|
| "warning" -> ()
|
|
| "hilight" -> ()
|
|
| "command" | "list" -> Format.fprintf ppf "@]"
|
|
| "document" -> Format.fprintf ppf "@]"
|
|
| other -> pop_level_tag other
|
|
end } ;
|
|
Format.pp_set_print_tags ppf true
|
|
| Html ->
|
|
Format.pp_set_formatter_tag_functions ppf
|
|
{ mark_open_tag = (fun _ -> "") ;
|
|
mark_close_tag = (fun _ -> "") ;
|
|
print_open_tag = begin function
|
|
| "title" -> Format.fprintf ppf "\003h3\004"
|
|
| "commandline" -> Format.fprintf ppf "\003div class='cmdline'\004@[<h>"
|
|
| "commanddoc" -> Format.fprintf ppf "\003div class='cmddoc'\004"
|
|
| "opt" -> Format.fprintf ppf "\003span class='opt'\004"
|
|
| "arg" -> Format.fprintf ppf "\003span class='arg'\004"
|
|
| "kwd" -> Format.fprintf ppf "\003span class='kwd'\004"
|
|
| "hilight" -> ()
|
|
| "error" -> ()
|
|
| "warning" -> ()
|
|
| "list" -> Format.fprintf ppf "\003ul\004@\n"
|
|
| "command" -> Format.fprintf ppf "\003li\004@\n"
|
|
| "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
|
|
end ;
|
|
print_close_tag = begin function
|
|
| "title" -> Format.fprintf ppf "\003/h3\004@\n"
|
|
| "commandline" -> Format.fprintf ppf "@]\003/div\004@\n"
|
|
| "commanddoc" -> Format.fprintf ppf "\003/div\004@\n"
|
|
| "opt" | "arg" | "kwd" -> Format.fprintf ppf "\003/span\004"
|
|
| "error" | "warning" | "hilight" -> ()
|
|
| "list" -> Format.fprintf ppf "\003/ul\004@\n"
|
|
| "command" -> Format.fprintf ppf "\003/li\004@\n"
|
|
| "document" -> Format.fprintf ppf "@]"
|
|
| other -> pop_level_tag other
|
|
end } ;
|
|
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 String.get 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
|
|
end ;
|
|
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_tag_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 (Argument { spec ; _ }) = global_options 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)@}@}\
|
|
@}@,@,\
|
|
@{<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
|
|
print_options_detailed spec
|
|
(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 switch ~doc ?short ~long () =
|
|
Switch { doc ; label = { long ; short } }
|
|
|
|
let parse_arg :
|
|
type a ctx. ?command:_ command -> (a, ctx) arg -> string list TzString.Map.t -> ctx -> a tzresult Lwt.t =
|
|
fun ?command spec args_dict ctx ->
|
|
match spec with
|
|
| Arg { label = { long ; short = _ } ; kind = { converter ; _ } ; _ } ->
|
|
begin match TzString.Map.find_opt long args_dict with
|
|
| None
|
|
| Some [] -> return_none
|
|
| Some [ s ] ->
|
|
(trace
|
|
(Bad_option_argument ("--" ^ long, command))
|
|
(converter ctx s)) >>|? fun x ->
|
|
Some x
|
|
| Some (_ :: _) ->
|
|
fail (Multiple_occurences ("--" ^ long, command))
|
|
end
|
|
| DefArg { label = { long ; short = _ } ; kind = { converter ; _ } ; default ; _ } ->
|
|
converter ctx default >>= fun default ->
|
|
begin match default with
|
|
| Ok x -> return x
|
|
| Error _ ->
|
|
invalid_arg
|
|
(Format.sprintf
|
|
"Value provided as default for '%s' could not be parsed by converter function."
|
|
long) end >>=? fun default ->
|
|
begin match TzString.Map.find_opt long args_dict with
|
|
| None
|
|
| Some [] -> return default
|
|
| Some [ s ] ->
|
|
(trace
|
|
(Bad_option_argument (long, command))
|
|
(converter ctx s))
|
|
| Some (_ :: _) ->
|
|
fail (Multiple_occurences (long, command))
|
|
end
|
|
| Switch { label = { long ; short = _ } ; _ } ->
|
|
begin match TzString.Map.find_opt long args_dict with
|
|
| None
|
|
| Some [] -> return_false
|
|
| Some [ _ ] -> return_true
|
|
| Some (_ :: _) -> fail (Multiple_occurences (long, command))
|
|
end
|
|
| Constant c -> return c
|
|
|
|
(* Argument parsing *)
|
|
let rec parse_args :
|
|
type a ctx. ?command:_ command -> (a, ctx) args -> string list TzString.Map.t -> ctx -> a tzresult Lwt.t =
|
|
fun ?command spec args_dict ctx ->
|
|
match spec with
|
|
| NoArgs -> return_unit
|
|
| AddArg (arg, rest) ->
|
|
parse_arg ?command arg args_dict ctx >>=? fun arg ->
|
|
parse_args ?command rest args_dict ctx >>|? fun rest ->
|
|
(arg, rest)
|
|
|
|
let empty_args_dict = TzString.Map.empty
|
|
|
|
let rec make_arities_dict :
|
|
type a b. (a, b) args -> (int * string) TzString.Map.t -> (int * string) TzString.Map.t =
|
|
fun args acc -> match args with
|
|
| NoArgs -> acc
|
|
| AddArg (arg, rest) ->
|
|
let recur { long ; short } num =
|
|
(match short with
|
|
| None -> acc
|
|
| Some c -> TzString.Map.add ("-" ^ String.make 1 c) (num, long) acc) |>
|
|
TzString.Map.add ("-" ^ long) (num, long) |>
|
|
TzString.Map.add ("--" ^ long) (num, long) |>
|
|
make_arities_dict rest in
|
|
match arg with
|
|
| Arg { label ; _ } -> recur label 1
|
|
| DefArg { label ; _ } -> recur label 1
|
|
| Switch { label ; _ } -> recur label 0
|
|
| Constant _c -> make_arities_dict rest acc
|
|
|
|
type error += Help : 'a command option -> error
|
|
|
|
let check_help_flag ?command = function
|
|
| ("-h" | "--help") :: _ -> fail (Help command)
|
|
| _ -> return_unit
|
|
|
|
let add_occurrence long value acc =
|
|
match TzString.Map.find_opt long acc with
|
|
| Some v -> TzString.Map.add long v acc
|
|
| None -> TzString.Map.add long [ value ] acc
|
|
|
|
let make_args_dict_consume ?command spec args =
|
|
let rec make_args_dict completing arities acc args =
|
|
check_help_flag ?command args >>=? fun () ->
|
|
match args with
|
|
| [] -> return (acc, [])
|
|
| arg :: tl ->
|
|
if String.length arg > 0 && String.get arg 0 = '-' then
|
|
if TzString.Map.mem arg arities then
|
|
let arity, long = TzString.Map.find arg arities in
|
|
check_help_flag ?command tl >>=? fun () ->
|
|
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, [] ->
|
|
fail (Option_expected_argument (arg, None))
|
|
| _, _ ->
|
|
raise (Failure "cli_entries: Arguments with arity not equal to 1 or 0 not supported")
|
|
else
|
|
fail (Unknown_option (arg, None))
|
|
else return (acc, args)
|
|
in make_args_dict false (make_arities_dict spec TzString.Map.empty) TzString.Map.empty args
|
|
|
|
let make_args_dict_filter ?command spec args =
|
|
let rec make_args_dict arities (dict, other_args) args =
|
|
check_help_flag ?command args >>=? fun () ->
|
|
match args with
|
|
| [] -> return (dict, other_args)
|
|
| arg :: tl ->
|
|
if TzString.Map.mem arg arities
|
|
then let arity, long = TzString.Map.find arg arities in
|
|
check_help_flag ?command tl >>=? fun () ->
|
|
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, [] -> fail (Option_expected_argument (arg, command))
|
|
| _, _ ->
|
|
raise (Failure "cli_entries: Arguments with arity not equal to 1 or 0 not supported")
|
|
else make_args_dict arities (dict, arg :: other_args) tl
|
|
in make_args_dict
|
|
(make_arities_dict spec TzString.Map.empty)
|
|
(TzString.Map.empty, [])
|
|
args >>|? fun (dict, remaining) ->
|
|
(dict, List.rev remaining)
|
|
|
|
let (>>) arg1 arg2 = AddArg (arg1, arg2)
|
|
let args1 spec =
|
|
Argument { spec = spec >> NoArgs;
|
|
converter = fun (arg, ()) -> arg }
|
|
let args2 spec1 spec2 =
|
|
Argument { spec = spec1 >> (spec2 >> NoArgs) ;
|
|
converter = fun (arg1, (arg2, ())) -> arg1, arg2 }
|
|
let args3 spec1 spec2 spec3 =
|
|
Argument { spec = spec1 >> (spec2 >> (spec3 >> NoArgs)) ;
|
|
converter = fun (arg1, (arg2, (arg3, ()))) -> arg1, arg2, arg3 }
|
|
let args4 spec1 spec2 spec3 spec4 =
|
|
Argument { spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> NoArgs))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, ())))) -> arg1, arg2, arg3, arg4 }
|
|
let args5 spec1 spec2 spec3 spec4 spec5 =
|
|
Argument { spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> NoArgs)))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, ()))))) -> arg1, arg2, arg3, arg4, arg5 }
|
|
let args6 spec1 spec2 spec3 spec4 spec5 spec6 =
|
|
Argument { spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> NoArgs))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, ())))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6 }
|
|
let args7 spec1 spec2 spec3 spec4 spec5 spec6 spec7 =
|
|
Argument { spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> (spec7 >> NoArgs)))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, (spec7, ()))))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6, spec7 }
|
|
let args8 spec1 spec2 spec3 spec4 spec5 spec6 spec7 spec8 =
|
|
Argument { spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> (spec7 >> (spec8 >> NoArgs))))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, (spec7, (spec8, ())))))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6, spec7, spec8 }
|
|
let args9 spec1 spec2 spec3 spec4 spec5 spec6 spec7 spec8 spec9 =
|
|
Argument
|
|
{ spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> (spec7 >> (spec8 >> (spec9 >> NoArgs)))))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, (spec7, (spec8, (spec9, ()))))))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6, spec7, spec8, spec9 }
|
|
let args10 spec1 spec2 spec3 spec4 spec5 spec6 spec7 spec8 spec9 spec10 =
|
|
Argument
|
|
{ spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> (spec7 >> (spec8 >> (spec9 >> (spec10 >> NoArgs))))))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, (spec7, (spec8, (spec9, (spec10, ())))))))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6, spec7, spec8, spec9, spec10 }
|
|
let args11 spec1 spec2 spec3 spec4 spec5 spec6 spec7 spec8 spec9 spec10 spec11 =
|
|
Argument
|
|
{ spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> (spec7 >> (spec8 >> (spec9 >> (spec10 >> (spec11 >> NoArgs)))))))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, (spec7, (spec8, (spec9, (spec10, (spec11, ()))))))))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6, spec7, spec8, spec9, spec10, spec11 }
|
|
|
|
let args12 spec1 spec2 spec3 spec4 spec5 spec6 spec7 spec8 spec9 spec10 spec11 spec12 =
|
|
Argument
|
|
{ spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> (spec7 >> (spec8 >> (spec9 >> (spec10 >> (spec11 >> (spec12 >> NoArgs))))))))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, (spec7, (spec8, (spec9, (spec10, (spec11, (spec12, ())))))))))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6, spec7, spec8, spec9, spec10, spec11, spec12 }
|
|
|
|
let args13 spec1 spec2 spec3 spec4 spec5 spec6 spec7 spec8 spec9 spec10 spec11 spec12 spec13 =
|
|
Argument
|
|
{ spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> (spec7 >> (spec8 >> (spec9 >> (spec10 >> (spec11 >> (spec12 >> (spec13 >> NoArgs)))))))))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, (spec7, (spec8, (spec9, (spec10, (spec11, (spec12, (spec13, ()))))))))))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6, spec7, spec8, spec9, spec10, spec11, spec12, spec13 }
|
|
|
|
let args14 spec1 spec2 spec3 spec4 spec5 spec6 spec7 spec8 spec9 spec10 spec11 spec12 spec13 spec14 =
|
|
Argument
|
|
{ spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> (spec7 >> (spec8 >> (spec9 >> (spec10 >> (spec11 >> (spec12 >> (spec13 >> (spec14 >> NoArgs))))))))))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, (spec7, (spec8, (spec9, (spec10, (spec11, (spec12, (spec13, (spec14, ())))))))))))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6, spec7, spec8, spec9, spec10, spec11, spec12, spec13, spec14 }
|
|
|
|
let args15 spec1 spec2 spec3 spec4 spec5 spec6 spec7 spec8 spec9 spec10 spec11 spec12 spec13 spec14 spec15 =
|
|
Argument
|
|
{ spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> (spec7 >> (spec8 >> (spec9 >> (spec10 >> (spec11 >> (spec12 >> (spec13 >> (spec14 >> (spec15 >> NoArgs)))))))))))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, (spec7, (spec8, (spec9, (spec10, (spec11, (spec12, (spec13, (spec14, (spec15, ()))))))))))))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6, spec7, spec8, spec9, spec10, spec11, spec12, spec13, spec14, spec15 }
|
|
|
|
let args16 spec1 spec2 spec3 spec4 spec5 spec6 spec7 spec8 spec9 spec10 spec11 spec12 spec13 spec14 spec15 spec16 =
|
|
Argument
|
|
{ spec = spec1 >> (spec2 >> (spec3 >> (spec4 >> (spec5 >> (spec6 >> (spec7 >> (spec8 >> (spec9 >> (spec10 >> (spec11 >> (spec12 >> (spec13 >> (spec14 >> (spec15 >> (spec16 >> NoArgs))))))))))))))) ;
|
|
converter = fun (arg1, (arg2, (arg3, (arg4, (arg5, (spec6, (spec7, (spec8, (spec9, (spec10, (spec11, (spec12, (spec13, (spec14, (spec15, (spec16, ())))))))))))))))) ->
|
|
arg1, arg2, arg3, arg4, arg5, spec6, spec7, spec8, spec9, spec10, spec11, spec12, spec13, spec14, spec15, spec16 }
|
|
|
|
(* 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 "Clic.seq_of_param"
|
|
|
|
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 = Argument { spec=NoArgs ; converter=fun () -> () }
|
|
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 -> return s) ; autocomplete=None } next
|
|
|
|
let string_contains ~needle ~haystack =
|
|
try
|
|
Some (Re.Str.search_forward (Re.Str.regexp_string needle) haystack 0)
|
|
with Not_found ->
|
|
None
|
|
|
|
let rec search_params_prefix : type a arg. string -> (a, arg) params -> bool =
|
|
fun prefix -> function
|
|
| Prefix (keyword, next) ->
|
|
begin
|
|
match string_contains ~needle:prefix ~haystack:keyword with
|
|
| None -> search_params_prefix prefix next
|
|
| Some _ -> true
|
|
end
|
|
| Param (_, _, _, next) -> search_params_prefix prefix next
|
|
| Stop -> false
|
|
| Seq _ -> false
|
|
|
|
let search_command keyword (Command { params ; _ }) =
|
|
search_params_prefix keyword params
|
|
|
|
|
|
(* Command execution *)
|
|
let exec
|
|
(type ctx)
|
|
(Command { options = (Argument { converter ; spec = options_spec }) ;
|
|
params = spec ; handler ; conv ; _ } as command)
|
|
(ctx : ctx) params args_dict =
|
|
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 ->
|
|
Lwt.catch
|
|
(fun () -> converter ctx p)
|
|
(function
|
|
| Failure msg -> Error_monad.failwith "%s" msg
|
|
| exn -> fail (Exn exn))
|
|
|> trace (Bad_argument (i, p)) >>=? fun v ->
|
|
do_seq (succ i) (v :: acc) rest in
|
|
do_seq i [] seq >>=? fun parsed ->
|
|
cb parsed ctx
|
|
| Prefix (n, next), p :: rest when n = p ->
|
|
exec (succ i) ctx next cb rest
|
|
| Param (_, _, { converter ; _ }, next), p :: rest ->
|
|
Lwt.catch
|
|
(fun () -> converter ctx p)
|
|
(function
|
|
| Failure msg -> Error_monad.failwith "%s" msg
|
|
| exn -> fail (Exn exn))
|
|
|> trace (Bad_argument (i, p)) >>=? fun v ->
|
|
exec (succ i) ctx next (cb v) rest
|
|
| _ -> raise (Failure ("cli_entries internal error: exec no case matched"))
|
|
in
|
|
let ctx = conv ctx in
|
|
parse_args ~command options_spec args_dict ctx >>=? fun parsed_options ->
|
|
exec 1 ctx spec (handler (converter parsed_options)) params
|
|
|
|
(* 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 '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
|
|
| TEmpty : 'ctx tree
|
|
|
|
let has_options : type ctx. ctx command -> bool =
|
|
fun (Command { options = Argument { spec ; _ } ; _ }) ->
|
|
let args_help : type a ctx. (a, ctx) args -> bool = function
|
|
| NoArgs -> false
|
|
| AddArg (_, _) -> true
|
|
in args_help spec
|
|
|
|
let insert_in_dispatch_tree :
|
|
type ctx. ctx tree -> ctx command -> ctx tree =
|
|
fun root (Command { params ; conv ; _ } as command) ->
|
|
let access_autocomplete :
|
|
type p ctx. (p, ctx) parameter -> (ctx -> string list tzresult Lwt.t) option =
|
|
fun { autocomplete ; _ } -> autocomplete in
|
|
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
|
|
match t, c with
|
|
| TEmpty, Stop -> TStop command
|
|
| TEmpty, Seq (_, _, { autocomplete ; _ }) ->
|
|
TSeq (command,
|
|
Option.map autocomplete ~f:(fun a c -> a (conv c)))
|
|
| TEmpty, Param (_, _, param, next) ->
|
|
let autocomplete = access_autocomplete param in
|
|
let autocomplete = Option.map autocomplete ~f:(fun a c -> a (conv c)) in
|
|
TParam { tree = insert_tree TEmpty next ; stop = None ; autocomplete}
|
|
| TEmpty, Prefix (n, next) ->
|
|
TPrefix { stop = None ; prefix = [ (n, insert_tree TEmpty next) ] }
|
|
| TStop cmd, Param (_, _, param, next) ->
|
|
let autocomplete = access_autocomplete param in
|
|
let autocomplete = Option.map autocomplete ~f:(fun a c -> a (conv c)) in
|
|
if not (has_options cmd)
|
|
then TParam { tree = insert_tree TEmpty next ;
|
|
stop = Some cmd ;
|
|
autocomplete }
|
|
else raise (Failure "Command cannot have both prefix and options")
|
|
| TStop cmd, Prefix (n, next) ->
|
|
TPrefix { stop = Some cmd ;
|
|
prefix = [ (n, insert_tree TEmpty next) ] }
|
|
| 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 }
|
|
| _, _ ->
|
|
Pervasives.failwith
|
|
"Clic.Command_tree.insert: conflicting commands" 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 ; _ } ->
|
|
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 rec traverse tree arguments acc =
|
|
match tree, arguments with
|
|
| (TStop _ | TSeq _
|
|
| TPrefix { stop = Some _ ; _ }
|
|
| TParam { stop = Some _ ; _}), ("-h" | "--help") :: _ ->
|
|
begin match gather_commands tree with
|
|
| [] -> assert false
|
|
| [ command ] -> fail (Help (Some command))
|
|
| more -> fail (Unterminated_command (initial_arguments, more))
|
|
end
|
|
| TStop c, [] -> return (c, empty_args_dict, initial_arguments)
|
|
| TStop (Command { options = Argument { spec ; _ } ; _ } as command), remaining ->
|
|
make_args_dict_filter ~command spec remaining >>=? fun (args_dict, unparsed) ->
|
|
begin match unparsed with
|
|
| [] -> return (command, args_dict, initial_arguments)
|
|
| hd :: _ ->
|
|
if String.length hd > 0 && String.get hd 0 = '-' then
|
|
fail (Unknown_option (hd, Some command))
|
|
else
|
|
fail (Extra_arguments (unparsed, command))
|
|
end
|
|
| TSeq (Command { options = Argument { spec ; _ } ; _ } as command, _), remaining ->
|
|
if List.exists (function "-h" | "--help" -> true | _ -> false) remaining then
|
|
fail (Help (Some command))
|
|
else
|
|
make_args_dict_filter ~command spec remaining >>|? fun (dict, remaining) ->
|
|
(command, dict, List.rev_append acc remaining)
|
|
| TPrefix { stop = Some cmd ; _ }, [] ->
|
|
return (cmd, empty_args_dict, initial_arguments)
|
|
| TPrefix { stop = None ; prefix }, ([] | ("-h" | "--help") :: _) ->
|
|
fail (Unterminated_command (initial_arguments, gather_assoc prefix))
|
|
| TPrefix { prefix ; _ }, hd_arg :: tl ->
|
|
begin
|
|
try
|
|
return (List.assoc hd_arg prefix)
|
|
with Not_found -> fail (Command_not_found (List.rev acc, gather_assoc prefix))
|
|
end >>=? fun tree' ->
|
|
traverse tree' tl (hd_arg :: acc)
|
|
| TParam { stop = None ; _ }, ([] | ("-h" | "--help") :: _) ->
|
|
fail (Unterminated_command (initial_arguments, gather_commands tree))
|
|
| TParam { stop = Some c ; _ }, [] ->
|
|
return (c, empty_args_dict, initial_arguments)
|
|
| TParam { tree ; _ }, parameter :: arguments' ->
|
|
traverse tree arguments' (parameter :: acc)
|
|
| TEmpty, _ ->
|
|
fail (Command_not_found (List.rev acc, []))
|
|
in traverse tree initial_arguments []
|
|
|
|
|
|
let get_arg_label (type a) (arg : (a, _) arg) =
|
|
match arg with
|
|
| Arg { label ; _ } -> label
|
|
| DefArg { label ; _ } -> label
|
|
| Switch { label ; _ } -> label
|
|
| Constant _ -> assert false
|
|
|
|
let get_arg
|
|
: type a ctx. (a, ctx) arg -> string list
|
|
= fun arg ->
|
|
let { long ; short } = get_arg_label arg in
|
|
("--" ^ long) :: match short with None -> [] | Some c -> [ "-" ^ String.make 1 c ]
|
|
|
|
let rec list_args : type arg ctx. (arg, ctx) args -> string list = function
|
|
| NoArgs -> []
|
|
| AddArg (Constant _, args) -> list_args args
|
|
| AddArg (arg, args) -> get_arg arg @ list_args args
|
|
|
|
let complete_func autocomplete cctxt =
|
|
match autocomplete with
|
|
| None -> return_nil
|
|
| Some autocomplete -> autocomplete cctxt
|
|
|
|
let list_command_args (Command { options = Argument { spec ; _ } ; _ }) =
|
|
list_args spec
|
|
|
|
let complete_arg : type a ctx. ctx -> (a, ctx) arg -> string list tzresult Lwt.t =
|
|
fun ctx -> function
|
|
| Arg { kind = { autocomplete ; _ } ; _ } -> complete_func autocomplete ctx
|
|
| DefArg { kind = { autocomplete ; _ } ; _ } -> complete_func autocomplete ctx
|
|
| Switch _ -> return_nil
|
|
| Constant _ -> return_nil
|
|
|
|
let rec remaining_spec :
|
|
type a ctx. TzString.Set.t -> (a, ctx) args -> string list =
|
|
fun seen -> function
|
|
| NoArgs -> []
|
|
| AddArg (Constant _, rest) ->
|
|
remaining_spec seen rest
|
|
| AddArg (arg, rest) ->
|
|
let { long ; _ } = get_arg_label arg in
|
|
if TzString.Set.mem long seen
|
|
then remaining_spec seen rest
|
|
else get_arg arg @ remaining_spec seen rest
|
|
|
|
let complete_options (type ctx) continuation args args_spec ind (ctx : ctx) =
|
|
let arities = make_arities_dict args_spec TzString.Map.empty in
|
|
let rec complete_spec : type a. string -> (a, ctx) args -> string list tzresult Lwt.t =
|
|
fun name -> function
|
|
| NoArgs -> return_nil
|
|
| AddArg (Constant _, rest) ->
|
|
complete_spec name rest
|
|
| AddArg (arg, rest) ->
|
|
if (get_arg_label arg).long = name
|
|
then complete_arg ctx arg
|
|
else complete_spec name rest in
|
|
let rec help args ind seen =
|
|
match args with
|
|
| _ when ind = 0 ->
|
|
continuation args 0 >>|? fun cont_args ->
|
|
cont_args @ remaining_spec seen args_spec
|
|
| [] ->
|
|
Pervasives.failwith
|
|
"cli_entries internal autocomplete error"
|
|
| arg :: tl ->
|
|
if TzString.Map.mem arg arities
|
|
then
|
|
let arity, long = TzString.Map.find arg arities in
|
|
let seen = TzString.Set.add long seen in
|
|
match arity, tl with
|
|
| 0, args when ind = 0 ->
|
|
continuation args 0 >>|? fun cont_args ->
|
|
remaining_spec seen args_spec @ cont_args
|
|
| 0, args -> help args (ind - 1) seen
|
|
| 1, _ when ind = 1 -> complete_spec arg args_spec
|
|
| 1, _ :: tl -> help tl (ind - 2) seen
|
|
| _ -> Pervasives.failwith "cli_entries internal error, invalid arity"
|
|
else continuation args ind
|
|
in help args ind TzString.Set.empty
|
|
|
|
let complete_next_tree cctxt = function
|
|
| TPrefix { stop; prefix } ->
|
|
return
|
|
((match stop with
|
|
| None -> []
|
|
| Some command -> list_command_args command)
|
|
@ (List.map fst prefix))
|
|
| TSeq (command, autocomplete) ->
|
|
complete_func autocomplete cctxt >>|? fun completions ->
|
|
completions @ (list_command_args command)
|
|
| TParam { autocomplete ; _ } ->
|
|
complete_func autocomplete cctxt
|
|
| TStop command -> return (list_command_args command)
|
|
| TEmpty -> return_nil
|
|
|
|
let complete_tree cctxt tree index args =
|
|
let rec help tree args ind =
|
|
if ind = 0
|
|
then complete_next_tree cctxt tree
|
|
else
|
|
match tree, args with
|
|
| TSeq _, _ -> complete_next_tree cctxt tree
|
|
| TPrefix { prefix ; _ }, hd :: tl ->
|
|
begin
|
|
try help (List.assoc hd prefix) tl (ind - 1)
|
|
with Not_found -> return_nil
|
|
end
|
|
| TParam { tree ; _ }, _ :: tl ->
|
|
help tree tl (ind - 1)
|
|
| TStop Command { options = Argument { spec ; _ } ; conv ;_ }, args ->
|
|
complete_options (fun _ _ -> return_nil) args spec ind (conv cctxt)
|
|
| (TParam _ | TPrefix _), []
|
|
| TEmpty, _ -> return_nil
|
|
in help tree args index
|
|
|
|
let autocompletion ~script ~cur_arg ~prev_arg ~args ~global_options commands cctxt =
|
|
let tree = make_dispatch_tree commands in
|
|
let rec ind n = function
|
|
| [] -> None
|
|
| hd :: tl ->
|
|
if hd = prev_arg
|
|
then Some (Option.unopt ~default:(n + 1) (ind (n + 1) tl))
|
|
else (ind (n + 1) tl) in
|
|
begin
|
|
if prev_arg = script
|
|
then complete_next_tree cctxt tree >>|? fun command_completions ->
|
|
begin
|
|
let (Argument { spec ; _ }) = global_options in
|
|
list_args spec @ command_completions
|
|
end
|
|
else
|
|
match ind 0 args with
|
|
| None ->
|
|
return_nil
|
|
| Some index ->
|
|
begin
|
|
let Argument { spec ; _ } = global_options in
|
|
complete_options
|
|
(fun args ind -> complete_tree cctxt tree ind args)
|
|
args spec index cctxt
|
|
end
|
|
end >>|? fun completions ->
|
|
List.filter
|
|
(fun completion -> Re.Str.(string_match (regexp_string cur_arg) completion 0))
|
|
completions
|
|
|
|
let parse_global_options global_options ctx args =
|
|
let Argument { spec ; converter } = global_options in
|
|
make_args_dict_consume spec args >>=? fun (dict, remaining) ->
|
|
parse_args spec dict ctx >>=? fun nested ->
|
|
return (converter nested, remaining)
|
|
|
|
let dispatch commands ctx args =
|
|
let tree = make_dispatch_tree commands in
|
|
match args with
|
|
| [] | [ "-h" | "--help" ] ->
|
|
fail (Help None)
|
|
| _ ->
|
|
find_command tree args >>=? fun (command, args_dict, filtered_args) ->
|
|
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 _ -> return [ "0" ; "1" ; "2" ; "3" ])
|
|
(fun _ arg -> 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 _ -> return [ "colors" ; "plain" ; "html" ])
|
|
(fun _ arg -> 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 List.length commands <= 3 -> Full
|
|
| None -> Short in
|
|
match commands with
|
|
| [] -> fail (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.unopt_map ~f:(fun command -> [ Ex command ]) ~default:[] command)
|
|
| Bad_option_argument (arg, command) ->
|
|
Format.fprintf ppf
|
|
"Wrong value for command line option @{<opt>%s@}." arg ;
|
|
Some (Option.unopt_map ~f:(fun command -> [ Ex command ]) ~default:[] command)
|
|
| Multiple_occurences (arg, command) ->
|
|
Format.fprintf ppf
|
|
"Command line option @{<opt>%s@} appears multiple times." arg ;
|
|
Some (Option.unopt_map ~f:(fun command -> [ Ex command ]) ~default:[] 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.unopt_map ~f:(fun command -> [ Ex command ]) ~default:[] command)
|
|
| Extra_arguments (extra, command) ->
|
|
Format.fprintf ppf
|
|
"Extra command line arguments:@, @[<h>%a@]."
|
|
(Format.pp_print_list (fun ppf -> Format.fprintf ppf "%s")) 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 = Argument { spec ; _ } ; _ }) ->
|
|
print_commandline ppf ([], spec, 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 = Argument { spec ; _ } ; _ }) ->
|
|
print_commandline ppf ([], spec, 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)) })
|