You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1167 lines
43 KiB

(*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*)
(** Definition and parsing of command line arguments *)
open! IStd
module F = Format
module YBU = Yojson.Basic.Util
module L = Die
open PolyVariantEqual
let ( = ) = String.equal
let manpage_s_notes = "NOTES"
let is_env_var_set v = Option.value (Option.map (Sys.getenv v) ~f:(String.equal "1")) ~default:false
(** The working directory of the initial invocation of infer, to which paths passed as command line
options are relative. *)
let init_work_dir, is_originator =
match Sys.getenv "INFER_CWD" with
| Some dir ->
(dir, false)
| None ->
let real_cwd = Utils.realpath (Sys.getcwd ()) in
Unix.putenv ~key:"INFER_CWD" ~data:real_cwd ;
(real_cwd, true)
let strict_mode_env_var = "INFER_STRICT_MODE"
let strict_mode = is_env_var_set strict_mode_env_var
let warnf =
if strict_mode then fun fmt -> L.(die UserError) fmt
else if not is_originator then fun fmt -> F.ifprintf F.err_formatter fmt
else F.eprintf
(** This is the subset of Arg.spec that we actually use. What's important is that all these specs
call back functions. We use this to mark deprecated arguments. What's not important is that, eg,
Arg.Float is missing. *)
type spec =
| Unit of (unit -> unit)
| String of (string -> unit)
| Symbol of string list * (string -> unit)
let to_arg_spec = function
| Unit f ->
Arg.Unit f
| String f ->
Arg.String f
| Symbol (symbols, f) ->
Arg.Symbol (symbols, f)
let to_arg_spec_triple (x, spec, y) = (x, to_arg_spec spec, y)
let to_arg_speclist = List.map ~f:to_arg_spec_triple
type parse_mode = InferCommand | Javac | NoParse [@@deriving compare, enumerate]
let equal_parse_mode = [%compare.equal: parse_mode]
type anon_arg_action =
{parse_subcommands: bool; parse_argfiles: bool; on_unknown: [`Add | `Reject | `Skip]}
let anon_arg_action_of_parse_mode parse_mode =
let parse_subcommands, parse_argfiles, on_unknown =
match parse_mode with
| InferCommand ->
(true, true, `Reject)
| Javac ->
(false, true, `Skip)
| NoParse ->
(false, false, `Skip)
in
{parse_subcommands; parse_argfiles; on_unknown}
type command_doc =
{ title: Cmdliner.Manpage.title
; manual_before_options: Cmdliner.Manpage.block list
; manual_options:
[`Prepend of Cmdliner.Manpage.block list | `Replace of Cmdliner.Manpage.block list]
; manual_after_options: Cmdliner.Manpage.block list }
type desc =
{ long: string
; short: string
; meta: string
; doc: string
; default_string: string
; spec: spec
; decode_json: inferconfig_dir:string -> Yojson.Basic.t -> string list
(** how to go from an option in the json config file to a list of command-line options *) }
let dashdash ?short long =
match (long, short) with
| "", (None | Some "") | "--", _ ->
long
| "", Some short ->
"-" ^ short
| _ ->
"--" ^ long
let xdesc {long; short; spec} =
let key long short =
match (long, short) with
| "", "" ->
""
| "--", _ ->
"--"
| "", _ ->
"-" ^ short
| _ ->
"--" ^ long
in
let xspec =
match spec with
(* translate Symbol to String for better formatting of --help messages *)
| Symbol (symbols, action) ->
String
(fun arg ->
if List.mem ~equal:String.equal symbols arg then action arg
else
raise
(Arg.Bad
(F.sprintf "wrong argument '%s'; option '%s' expects one of: %s" arg
(dashdash ~short long)
(String.concat ~sep:" | " symbols))) )
| _ ->
spec
in
(* Arg doesn't need to know anything about documentation since we generate our own *)
(key long short, xspec, "")
let check_no_duplicates desc_list =
let rec check_for_duplicates_ = function
| [] | [_] ->
true
| (x, _, _) :: (y, _, _) :: _ when (not (String.is_empty x)) && x = y ->
L.(die InternalError) "Multiple definitions of command line option: %s" x
| _ :: tl ->
check_for_duplicates_ tl
in
check_for_duplicates_
(List.sort ~compare:(fun (x, _, _) (y, _, _) -> String.compare x y) desc_list)
let parse_mode_desc_lists = List.map ~f:(fun parse_mode -> (parse_mode, ref [])) all_of_parse_mode
module SectionMap = Caml.Map.Make (struct
type t = String.t
(* this must be the reverse of the order in which we want the sections to appear in the
manual *)
let compare s1 s2 =
if String.equal s1 s2 then (* this simplifies the next two cases *)
0
else if String.equal s1 Cmdliner.Manpage.s_options then
(* ensure OPTIONS section is last (hence first in the manual) *)
1
else if String.equal s2 Cmdliner.Manpage.s_options then (* same as above *)
-1
else (* reverse order *)
String.compare s2 s1
end)
let help_sections_desc_lists =
List.map InferCommand.all_commands ~f:(fun command -> (command, ref SectionMap.empty))
let visible_descs_list = ref []
let hidden_descs_list = ref []
(** add [desc] to the one relevant parse_tag_desc_lists for the purposes of parsing, and, in the
case of InferCommand, include [desc] in --help only for the relevant sections. *)
let add parse_mode sections desc =
let desc_list = List.Assoc.find_exn ~equal:equal_parse_mode parse_mode_desc_lists parse_mode in
desc_list := desc :: !desc_list ;
let add_to_section (command, section) =
let sections = List.Assoc.find_exn ~equal:InferCommand.equal help_sections_desc_lists command in
let prev_contents = try SectionMap.find section !sections with Caml.Not_found -> [] in
sections := SectionMap.add section (desc :: prev_contents) !sections
in
List.iter sections ~f:add_to_section ;
if List.is_empty sections then hidden_descs_list := desc :: !hidden_descs_list
else
let desc_infer =
if String.equal "" desc.doc then desc
else
let oxford_comma l =
let rec aux acc l =
match (l, acc) with
| [], _ ->
assert false
| [x], [] ->
x
| [x; y], [] ->
Printf.sprintf "%s and %s" x y
| [x; y], acc ->
Printf.sprintf "%s, %s, and %s" (String.concat ~sep:", " (List.rev acc)) x y
| x :: tl, acc ->
aux (x :: acc) tl
in
aux [] l
in
(* in the help of `infer` itself, show in which specific commands the option is used *)
let commands =
List.map ~f:fst sections
|> List.sort ~compare:InferCommand.compare
|> List.remove_consecutive_duplicates ~equal:InferCommand.equal
|> List.map ~f:(fun cmd ->
let exe = InferCommand.to_exe_name cmd in
Printf.sprintf "$(b,%s)(1)" (Cmdliner.Manpage.escape exe) )
|> oxford_comma
in
{desc with doc= Printf.sprintf "%s\nSee also %s." desc.doc commands}
in
visible_descs_list := desc_infer :: !visible_descs_list ;
()
let deprecate_desc parse_mode ~long ~short ~deprecated doc desc =
let warn source =
let source_s =
match source with
| `CLI ->
""
| `Inferconfig root ->
Printf.sprintf " in %s/.inferconfig:" root
in
match parse_mode with
| Javac | NoParse ->
()
| InferCommand when not (String.is_empty long) ->
warnf "WARNING:%s '-%s' is deprecated. Use '--%s'%s instead.@." source_s deprecated long
(if short = "" then "" else Printf.sprintf " or '-%s'" short)
| InferCommand ->
warnf "WARNING:%s '-%s' is deprecated. Here is its documentation:@\n%s@." source_s
deprecated doc
in
let warn_then_f f x =
warn `CLI ;
f x
in
let deprecated_spec =
match desc.spec with
| Unit f ->
Unit (warn_then_f f)
| String f ->
String (warn_then_f f)
| Symbol (symbols, f) ->
Symbol (symbols, warn_then_f f)
in
let deprecated_decode_json ~inferconfig_dir j =
warn (`Inferconfig inferconfig_dir) ;
desc.decode_json ~inferconfig_dir j
in
{ long= ""
; short= deprecated
; meta= ""
; doc= ""
; default_string= ""
; spec= deprecated_spec
; decode_json= deprecated_decode_json }
let mk ?(deprecated = []) ?(parse_mode = InferCommand) ?(in_help = []) ~long ?short:short0 ~default
~meta doc ~default_to_string ~decode_json ~mk_setter ~mk_spec =
(* check that *some* flag exists for the option, otherwise report an error *)
if String.is_empty long && Option.is_none short0 && List.for_all ~f:String.is_empty deprecated
then
L.die InternalError
"No command line flag corresponds to this option, please give it at least one of 1) a long \
form, 2) a short form, or 3) a non-empty deprecated form. The documentation for this option \
is '%s'."
doc ;
let variable = ref default in
let closure = mk_setter variable in
let setter str =
try closure str
with exc ->
raise (Arg.Bad (F.sprintf "bad value %s for flag %s (%s)" str long (Exn.to_string exc)))
in
let spec = mk_spec setter in
let default_string = default_to_string default in
let short = match short0 with Some c -> String.of_char c | None -> "" in
let desc = {long; short; meta; doc; default_string; spec; decode_json} in
(* add desc for long option, with documentation (which includes any short option) for exes *)
if not (String.is_empty long) then add parse_mode in_help desc ;
(* add desc for short option only for parsing, without documentation *)
if not (String.is_empty short) then add parse_mode [] {desc with long= ""; meta= ""; doc= ""} ;
(* add desc for deprecated options only for parsing, without documentation *)
List.iter deprecated ~f:(fun deprecated ->
deprecate_desc parse_mode ~long ~short ~deprecated doc desc |> add parse_mode [] ) ;
variable
(* begin parsing state *)
(* arguments passed to Arg.parse_argv_dynamic, susceptible to be modified on the fly when parsing *)
let args_to_parse : string array ref = ref (Array.of_list [])
(* reference used by Arg.parse_argv_dynamic to track the index of the argument being parsed *)
let arg_being_parsed : int ref = ref 0
(* list of arg specifications currently being used by Arg.parse_argv_dynamic *)
let curr_speclist : (Arg.key * Arg.spec * Arg.doc) list ref = ref []
let anon_arg_action = ref (anon_arg_action_of_parse_mode InferCommand)
let subcommands = ref []
let subcommand_actions = ref []
let rev_anon_args = ref []
(* keep track of the current active command to drive the remainder of the program *)
let curr_command = ref None
(* end parsing state *)
type 'a t =
?deprecated:string list
-> long:Arg.key
-> ?short:char
-> ?parse_mode:parse_mode
-> ?in_help:(InferCommand.t * string) list
-> ?meta:string
-> Arg.doc
-> 'a
let int_json_decoder ~flag ~inferconfig_dir json =
let int_as_string =
match json with
| `String s ->
warnf "WARNING: in %s/.inferconfig for option '%s', use an integer instead of a string.@."
inferconfig_dir flag ;
s
| json ->
string_of_int (YBU.to_int json)
in
[flag; int_as_string]
let string_json_decoder ~flag ~inferconfig_dir:_ json = [flag; YBU.to_string json]
let path_json_decoder ~flag ~inferconfig_dir json =
let abs_path =
let path = YBU.to_string json in
if Filename.is_relative path then inferconfig_dir ^/ path else path
in
[flag; abs_path]
let list_json_decoder json_decoder ~inferconfig_dir json =
List.concat (YBU.convert_each (json_decoder ~inferconfig_dir) json)
(* selects "--long" if not empty, or some non-empty "-deprecated" or "-short" *)
let mk_flag ~deprecated ?short ~long =
if String.is_empty long then
match short with
| Some c ->
Printf.sprintf "-%c" c
| None -> (
match deprecated with
| s :: _ ->
"-" ^ s
| [] ->
L.die InternalError "Option has no corresponding flag that is not empty" )
else dashdash long
let mk_set var value ?(deprecated = []) ~long ?short ?parse_mode ?in_help ?(meta = "") doc =
let setter () = var := value in
let flag = mk_flag ~deprecated ?short ~long in
ignore
(mk ~deprecated ~long ?short ~default:() ?parse_mode ?in_help ~meta doc
~default_to_string:(fun () -> "")
~decode_json:(string_json_decoder ~flag)
~mk_setter:(fun _ _ -> setter ())
~mk_spec:(fun _ -> Unit setter))
let mk_with_reset value ~reset_doc ?deprecated ~long ?parse_mode mk =
let var = mk () in
if not (String.equal "" long) then
(* Do not pass any ~in_help value so that the reset options only show up in --help-full and do
not clutter --help. *)
mk_set var value ?deprecated ~long:(long ^ "-reset") ?parse_mode reset_doc ;
var
let reset_doc_opt ~long = Printf.sprintf "Cancel the effect of $(b,%s)." (dashdash long)
let reset_doc_list ~long = Printf.sprintf "Set $(b,%s) to the empty list." (dashdash long)
let mk_option ?(default = None) ?(default_to_string = fun _ -> "") ~f ?(mk_reset = true)
?(deprecated = []) ~long ?short ?parse_mode ?in_help ?(meta = "string") doc =
let flag = mk_flag ~deprecated ?short ~long in
let mk () =
mk ~deprecated ~long ?short ~default ?parse_mode ?in_help ~meta doc ~default_to_string
~decode_json:(string_json_decoder ~flag)
~mk_setter:(fun var str -> var := f str)
~mk_spec:(fun set -> String set)
in
if mk_reset then
let reset_doc = reset_doc_opt ~long in
mk_with_reset None ~reset_doc ~long ?parse_mode mk
else mk ()
let mk_bool ?(deprecated_no = []) ?(default = false) ?(f = fun b -> b) ?(deprecated = []) ~long
?short ?parse_mode ?in_help ?(meta = "") doc0 =
let nolong =
let len = String.length long in
let is_already_no = len > 3 && String.sub long ~pos:0 ~len:3 = "no-" in
if is_already_no then String.sub long ~pos:3 ~len:(len - 3)
else if Int.equal len 0 then ""
else "no-" ^ long
and noshort =
Option.map
~f:(fun short ->
if Char.is_lowercase short then Char.uppercase short else Char.lowercase short )
short
in
let doc long short =
if String.is_empty long then doc0
else
match short with
| Some short ->
doc0 ^ " (Conversely: $(b,--" ^ long ^ ") | $(b,-" ^ String.of_char short ^ "))"
| None ->
doc0 ^ " (Conversely: $(b,--" ^ long ^ "))"
in
let doc, nodoc =
if String.equal doc0 "" then ("", "")
else if not default then ("Activates: " ^ doc nolong noshort, "")
else ("", "Deactivates: " ^ doc long short)
in
let default_to_string _ = "" in
let mk_spec set = Unit (fun () -> set "") in
let best_nonempty_enable, best_nonempty_disable =
let mk_best_non_empty long short_opt deprecated =
if String.is_empty long then
match short_opt with
| Some short ->
"-" ^ String.of_char short
| None -> (
match deprecated with
| [] ->
(* [mk] will fail in this case but with a non-informative message if this is the
auto-generated negated form of the option *)
L.die InternalError
"No command line flag can be given to enable this option or to disable it (did you \
forget to give it a `~deprecated_no` form?). The documentation for this option is \
'%s'."
doc0
| first_deprecated :: _ ->
"-" ^ first_deprecated )
else "--" ^ long
in
(mk_best_non_empty long short deprecated, mk_best_non_empty nolong noshort deprecated_no)
in
let var =
mk ~long ?short ~deprecated ~default ?parse_mode ?in_help ~meta doc ~default_to_string
~mk_setter:(fun var _ -> var := f true)
~decode_json:(fun ~inferconfig_dir:_ json ->
[(if YBU.to_bool json then best_nonempty_enable else best_nonempty_disable)] )
~mk_spec
in
ignore
(mk ~long:nolong ?short:noshort ~deprecated:deprecated_no ~default:(not default) ?parse_mode
?in_help ~meta nodoc ~default_to_string
~mk_setter:(fun _ _ -> var := f false)
~decode_json:(fun ~inferconfig_dir:_ json ->
[(if YBU.to_bool json then best_nonempty_disable else best_nonempty_enable)] )
~mk_spec) ;
var
let mk_bool_group ?(deprecated_no = []) ?(default = false) ?f:(f0 = Fn.id) ?(deprecated = []) ~long
?short ?parse_mode ?in_help ?meta doc children no_children =
let f b =
List.iter ~f:(fun child -> child := b) children ;
List.iter ~f:(fun child -> child := not b) no_children ;
f0 b
in
mk_bool ~deprecated ~deprecated_no ~default ~long ?short ~f ?parse_mode ?in_help ?meta doc
let mk_int ~default ?(default_to_string = string_of_int) ?(f = Fn.id) ?(deprecated = []) ~long
?short ?parse_mode ?in_help ?(meta = "int") doc =
let flag = mk_flag ~deprecated ?short ~long in
mk ~deprecated ~long ?short ~default ?parse_mode ?in_help ~meta doc ~default_to_string
~mk_setter:(fun var str -> var := f (int_of_string str))
~decode_json:(int_json_decoder ~flag)
~mk_spec:(fun set -> String set)
let mk_int_opt ?default ?(default_to_string = Option.value_map ~default:"" ~f:string_of_int)
?f:(f0 = Fn.id) ?(deprecated = []) ~long ?short ?parse_mode ?in_help ?(meta = "int") doc =
let f s = Some (f0 (int_of_string s)) in
mk_option ~deprecated ~long ?short ~default ~default_to_string ~f ?parse_mode ?in_help ~meta doc
let mk_float_opt ?default ?(default_to_string = Option.value_map ~default:"" ~f:string_of_float)
?(deprecated = []) ~long ?short ?parse_mode ?in_help ?(meta = "float") doc =
let f s = Some (float_of_string s) in
mk_option ~deprecated ~long ?short ~default ~default_to_string ~f ?parse_mode ?in_help ~meta doc
let mk_string ~default ?(default_to_string = Fn.id) ?(f = fun s -> s) ?(deprecated = []) ~long
?short ?parse_mode ?in_help ?(meta = "string") doc =
let flag = mk_flag ~deprecated ?short ~long in
mk ~deprecated ~long ?short ~default ?parse_mode ?in_help ~meta doc ~default_to_string
~mk_setter:(fun var str -> var := f str)
~decode_json:(string_json_decoder ~flag)
~mk_spec:(fun set -> String set)
let mk_string_opt ?default ?(default_to_string = Option.value ~default:"") ?(f = fun s -> s)
?mk_reset ?(deprecated = []) ~long ?short ?parse_mode ?in_help ?(meta = "string") doc =
let f s = Some (f s) in
mk_option ~deprecated ~long ?short ~default ~default_to_string ~f ?mk_reset ?parse_mode ?in_help
~meta doc
let mk_string_list ?(default = []) ?(default_to_string = String.concat ~sep:",") ?(f = fun s -> s)
?(deprecated = []) ~long ?short ?parse_mode ?in_help ?(meta = "string") doc =
let flag = mk_flag ~deprecated ?short ~long in
let mk () =
mk ~deprecated ~long ?short ~default ?parse_mode ?in_help ~meta:("+" ^ meta) doc
~default_to_string
~mk_setter:(fun var str -> var := f str :: !var)
~decode_json:(list_json_decoder (string_json_decoder ~flag))
~mk_spec:(fun set -> String set)
in
let reset_doc = reset_doc_list ~long in
mk_with_reset [] ~reset_doc ~long ?parse_mode mk
let map_to_str map =
let pair_to_str (a, b) = a ^ "=" ^ b in
let list = Map.to_alist map |> List.map ~f:pair_to_str in
String.concat list ~sep:","
let mk_string_map ?(default = String.Map.empty) ?(default_to_string = map_to_str) ?(deprecated = [])
~long ?short ?parse_mode ?in_help ?(meta = "key=value") doc =
let flag = mk_flag ~deprecated ?short ~long in
let split_str str =
match String.lsplit2 str ~on:'=' with
| Some a ->
a
| None ->
raise (Arg.Bad "Expected format is <key>=<value>")
in
let add_to_map map ~key ~data =
match Map.add map ~key ~data with
| `Ok a ->
a
| `Duplicate ->
raise (Arg.Bad "Duplicate keys are not allowed")
in
mk ~deprecated ~long ?short ~default ?parse_mode ?in_help ~meta:("+" ^ meta) doc
~default_to_string
~mk_setter:(fun var str ->
let key, data = split_str str in
var := add_to_map !var ~key ~data )
~mk_spec:(fun set -> String set )
(* In spirit of JSON we could have presented json as list of key-value pairs
with e.g. "key" and "value" fields, but for simplicity let's present each key-value pair
as it is passed to command line, which is a <key>=<value> *)
~decode_json:(list_json_decoder (string_json_decoder ~flag))
let normalize_path_in_args_being_parsed ?(f = Fn.id) ~is_anon_arg str =
if Filename.is_relative str then (
(* Replace relative paths with absolute ones on the fly in the args being parsed. This assumes
that [!arg_being_parsed] points at either [str] (if [is_anon_arg]) or at the option name
position in [!args_to_parse], as is the case e.g. when calling
[Arg.parse_argv_dynamic ~current:arg_being_parsed !args_to_parse ...]. *)
let root = Unix.getcwd () in
let abs_path = Utils.filename_to_absolute ~root str in
!args_to_parse.(!arg_being_parsed + if is_anon_arg then 0 else 1) <- f abs_path ;
abs_path )
else str
let mk_path_helper ~setter ~default_to_string ~default ~deprecated ~long ~short ~parse_mode ~in_help
~meta ~decode_json doc =
mk ~deprecated ~long ?short ~default ?parse_mode ?in_help ~meta doc ~decode_json
~default_to_string
~mk_setter:(fun var str ->
let abs_path = normalize_path_in_args_being_parsed ~is_anon_arg:false str in
setter var abs_path )
~mk_spec:(fun set -> String set)
let mk_path ~default ?(default_to_string = Fn.id) ?(f = Fn.id) ?(deprecated = []) ~long ?short
?parse_mode ?in_help ?(meta = "path") =
let flag = mk_flag ~deprecated ?short ~long in
mk_path_helper
~setter:(fun var x -> var := f x)
~decode_json:(path_json_decoder ~flag) ~default_to_string ~default ~deprecated ~long ~short
~parse_mode ~in_help ~meta
let mk_path_opt ?default ?(default_to_string = Option.value ~default:"") ?(deprecated = []) ~long
?short ?parse_mode ?in_help ?(meta = "path") doc =
let mk () =
let flag = mk_flag ~deprecated ?short ~long in
mk_path_helper
~setter:(fun var x -> var := Some x)
~decode_json:(path_json_decoder ~flag) ~default_to_string ~default ~deprecated ~long ~short
~parse_mode ~in_help ~meta doc
in
let reset_doc = reset_doc_opt ~long in
mk_with_reset None ~reset_doc ~long ?parse_mode mk
let mk_path_list ?(default = []) ?(default_to_string = String.concat ~sep:", ") ?(deprecated = [])
~long ?short ?parse_mode ?in_help ?(meta = "path") doc =
let flag = mk_flag ~deprecated ?short ~long in
let mk () =
mk_path_helper
~setter:(fun var x -> var := x :: !var)
~decode_json:(list_json_decoder (path_json_decoder ~flag))
~default_to_string ~default ~deprecated ~long ~short ~parse_mode ~in_help ~meta:("+" ^ meta)
doc
in
let reset_doc = reset_doc_list ~long in
mk_with_reset [] ~reset_doc ~long ?parse_mode mk
let mk_symbols_meta symbols =
let strings = List.map ~f:fst symbols in
Printf.sprintf "{ %s }" (String.concat ~sep:" | " strings)
let mk_symbol ~default ~symbols ~eq ?(f = Fn.id) ?(deprecated = []) ~long ?short ?parse_mode
?in_help ?meta doc =
let strings = List.map ~f:fst symbols in
let sym_to_str = List.map ~f:(fun (x, y) -> (y, x)) symbols in
let of_string str = List.Assoc.find_exn ~equal:String.equal symbols str in
let to_string sym = List.Assoc.find_exn ~equal:eq sym_to_str sym in
let meta = Option.value meta ~default:(mk_symbols_meta symbols) in
let flag = mk_flag ~deprecated ?short ~long in
mk ~deprecated ~long ?short ~default ?parse_mode ?in_help ~meta doc
~default_to_string:(fun s -> to_string s)
~mk_setter:(fun var str -> var := of_string str |> f)
~decode_json:(string_json_decoder ~flag)
~mk_spec:(fun set -> Symbol (strings, set))
let mk_symbol_opt ~symbols ?(f = Fn.id) ?(mk_reset = true) ?(deprecated = []) ~long ?short
?parse_mode ?in_help ?meta doc =
let strings = List.map ~f:fst symbols in
let of_string str = List.Assoc.find_exn ~equal:String.equal symbols str in
let meta = Option.value meta ~default:(mk_symbols_meta symbols) in
let flag = mk_flag ~deprecated ?short ~long in
let mk () =
mk ~deprecated ~long ?short ~default:None ?parse_mode ?in_help ~meta doc
~default_to_string:(fun _ -> "")
~mk_setter:(fun var str -> var := Some (f (of_string str)))
~decode_json:(string_json_decoder ~flag)
~mk_spec:(fun set -> Symbol (strings, set))
in
if mk_reset then
let reset_doc = reset_doc_opt ~long in
mk_with_reset None ~reset_doc ~long ?parse_mode mk
else mk ()
let mk_symbol_seq ?(default = []) ~symbols ~eq ?(deprecated = []) ~long ?short ?parse_mode ?in_help
?meta doc =
let sym_to_str = List.map ~f:(fun (x, y) -> (y, x)) symbols in
let of_string str = List.Assoc.find_exn ~equal:String.equal symbols str in
let to_string sym = List.Assoc.find_exn ~equal:eq sym_to_str sym in
let meta = Option.value meta ~default:(",-separated sequence of " ^ mk_symbols_meta symbols) in
mk ~deprecated ~long ?short ~default ?parse_mode ?in_help ~meta doc
~default_to_string:(fun syms -> String.concat ~sep:" " (List.map ~f:to_string syms))
~mk_setter:(fun var str_seq -> var := List.map ~f:of_string (String.split ~on:',' str_seq))
~decode_json:(fun ~inferconfig_dir:_ json ->
[dashdash long; String.concat ~sep:"," (YBU.convert_each YBU.to_string json)] )
~mk_spec:(fun set -> String set)
let mk_json ?(deprecated = []) ~long ?short ?parse_mode ?in_help ?(meta = "json") doc =
mk ~deprecated ~long ?short ?parse_mode ?in_help ~meta doc ~default:(`List [])
~default_to_string:Yojson.Basic.to_string
~mk_setter:(fun var json -> var := Yojson.Basic.from_string json)
~decode_json:(fun ~inferconfig_dir:_ json -> [dashdash long; Yojson.Basic.to_string json])
~mk_spec:(fun set -> String set)
(** [mk_anon] always return the same ref. Anonymous arguments are only accepted if
[parse_action_accept_unknown_args] is true. *)
let mk_anon () = rev_anon_args
let normalize_desc_list speclist =
let norm k =
let remove_no s =
let len = String.length k in
if len > 3 && String.sub s ~pos:0 ~len:3 = "no-" then String.sub s ~pos:3 ~len:(len - 3)
else s
in
let remove_weird_chars =
String.filter ~f:(function 'a' .. 'z' | '0' .. '9' | '-' -> true | _ -> false)
in
remove_weird_chars @@ String.lowercase @@ remove_no k
in
let compare_specs {long= x} {long= y} =
match (x, y) with
| "--", "--" ->
0
| "--", _ ->
1
| _, "--" ->
-1
| _ ->
let lower_norm s = String.lowercase @@ norm s in
String.compare (lower_norm x) (lower_norm y)
in
let sort speclist = List.sort ~compare:compare_specs speclist in
sort speclist
let mk_command_doc ~title ~section ~version ~date ~short_description ~synopsis ~description ?options
?exit_status ?environment ?files ?notes ?bugs ?examples ?see_also command_str =
let add_if section blocks =
match blocks with None -> `Blocks [] | Some bs -> `Blocks (`S section :: bs)
in
let manual_before_options =
[ `S Cmdliner.Manpage.s_name
; (* the format of the following line is mandated by man(7) *)
`Pre (Printf.sprintf "%s - %s" command_str short_description)
; `S Cmdliner.Manpage.s_synopsis
; `Blocks synopsis
; `S Cmdliner.Manpage.s_description
; `Blocks description ]
in
let manual_options = Option.value ~default:(`Prepend []) options in
let manual_after_options =
[ add_if Cmdliner.Manpage.s_exit_status exit_status
; add_if Cmdliner.Manpage.s_environment environment
; add_if Cmdliner.Manpage.s_files files
; add_if manpage_s_notes notes
; add_if Cmdliner.Manpage.s_bugs bugs
; add_if Cmdliner.Manpage.s_examples examples
; add_if Cmdliner.Manpage.s_see_also see_also ]
in
let command_doc =
{ title= (command_str, section, date, version, title)
; manual_before_options
; manual_options
; manual_after_options }
in
command_doc
let set_curr_speclist_for_parse_mode ~usage parse_mode =
let curr_usage status =
prerr_endline (String.concat_array ~sep:" " !args_to_parse) ;
prerr_endline usage ;
Stdlib.exit status
in
(* "-help" and "--help" are automatically recognized by Arg.parse, so we have to give them special
treatment *)
let add_or_suppress_help speclist =
let unknown opt =
(opt, Unit (fun () -> raise (Arg.Bad ("unknown option '" ^ opt ^ "'"))), "")
in
let has_opt opt = List.exists ~f:(fun (o, _, _) -> String.equal opt o) speclist in
let add_unknown opt = if not (has_opt opt) then List.cons (unknown opt) else Fn.id in
add_unknown "-help" @@ add_unknown "--help" @@ speclist
in
let full_desc_list =
List.Assoc.find_exn ~equal:equal_parse_mode parse_mode_desc_lists parse_mode
in
curr_speclist :=
normalize_desc_list !full_desc_list
|> List.map ~f:xdesc |> add_or_suppress_help |> to_arg_speclist ;
assert (check_no_duplicates !curr_speclist) ;
curr_usage
let select_parse_mode ~usage parse_mode =
let print_usage = set_curr_speclist_for_parse_mode ~usage parse_mode in
anon_arg_action := anon_arg_action_of_parse_mode parse_mode ;
print_usage
let string_of_command command =
let _, s, _ = List.Assoc.find_exn !subcommands ~equal:InferCommand.equal command in
s
let mk_rest_actions ?(parse_mode = InferCommand) ?(in_help = []) doc ~usage decode_action =
let rest = ref [] in
let spec =
String
(fun arg ->
rest := List.rev (Array.to_list (Array.slice !args_to_parse (!arg_being_parsed + 1) 0)) ;
select_parse_mode ~usage (decode_action arg) |> ignore )
in
add parse_mode in_help
{ long= "--"
; short= ""
; meta= ""
; doc
; default_string= ""
; spec
; decode_json= (fun ~inferconfig_dir:_ _ -> []) } ;
rest
let mk_subcommand command ?on_unknown_arg:(on_unknown = `Reject) ~name ?deprecated_long ?parse_mode
?in_help command_doc =
let switch () =
curr_command := Some command ;
anon_arg_action := {!anon_arg_action with on_unknown}
in
( match deprecated_long with
| Some long ->
ignore
(mk ~long ~default:() ?parse_mode ?in_help ~meta:"" ""
~default_to_string:(fun () -> "")
~decode_json:(fun ~inferconfig_dir:_ _ ->
raise (Arg.Bad ("Bad option in config file: " ^ long)) )
~mk_setter:(fun _ _ ->
warnf "WARNING: '%s' is deprecated. Please use '%s' instead.@\n" (dashdash long) name ;
switch () )
~mk_spec:(fun set -> Unit (fun () -> set "")))
| None ->
() ) ;
subcommands := (command, (command_doc, name, in_help)) :: !subcommands ;
subcommand_actions := (name, switch) :: !subcommand_actions
let args_from_argfile arg =
let abs_fname =
let fname = String.slice arg 1 (String.length arg) in
normalize_path_in_args_being_parsed ~f:(fun s -> "@" ^ s) ~is_anon_arg:true fname
in
match In_channel.read_lines abs_fname with
| lines ->
let strip = Utils.strip_balanced_once ~drop:(function '"' | '\'' -> true | _ -> false) in
List.map ~f:strip lines
| exception e ->
raise (Arg.Bad ("Error reading argument file '" ^ abs_fname ^ "': " ^ Exn.to_string e))
exception SubArguments of string list
let anon_fun arg =
if !anon_arg_action.parse_argfiles && String.is_prefix arg ~prefix:"@" then
(* stop parsing the current args and go look in that argfile *)
raise (SubArguments (args_from_argfile arg))
else if
!anon_arg_action.parse_subcommands && List.Assoc.mem !subcommand_actions ~equal:String.equal arg
then
let command_switch = List.Assoc.find_exn !subcommand_actions ~equal:String.equal arg in
match (!curr_command, is_originator) with
| None, _ | Some _, false ->
command_switch ()
| Some command, true ->
raise
(Arg.Bad
(Printf.sprintf "More than one subcommand specified: '%s', '%s'"
(string_of_command command) arg))
else
match !anon_arg_action.on_unknown with
| `Add ->
rev_anon_args := arg :: !rev_anon_args
| `Skip ->
()
| `Reject ->
raise (Arg.Bad (Printf.sprintf "Unexpected anonymous argument: '%s'" arg))
let decode_inferconfig_to_argv path =
let json =
match Utils.read_json_file path with
| Ok json ->
json
| Error msg ->
warnf "WARNING: Could not read or parse Infer config in %s:@\n%s@." path msg ;
`Assoc []
in
let desc_list = List.Assoc.find_exn ~equal:equal_parse_mode parse_mode_desc_lists InferCommand in
let json_config = YBU.to_assoc json in
let inferconfig_dir = Filename.dirname path in
let one_config_item result (key, json_val) =
try
let {decode_json} =
List.find_exn
~f:(fun {long; short} ->
String.equal key long || String.equal key short
(* for deprecated options *)
|| (* for deprecated options that start with "-" *) String.equal ("-" ^ key) short )
!desc_list
in
decode_json ~inferconfig_dir json_val @ result
with
| Not_found_s _ | Caml.Not_found ->
warnf "WARNING: while reading config file %s:@\nUnknown option %s@." path key ;
result
| YBU.Type_error (msg, json) ->
warnf "WARNING: while reading config file %s:@\nIll-formed value %s for option %s: %s@."
path (Yojson.Basic.to_string json) key msg ;
result
in
List.fold ~f:one_config_item ~init:[] json_config
(** separator of argv elements when encoded into environment variables *)
let env_var_sep = '^'
let encode_argv_to_env argv =
String.concat ~sep:(String.make 1 env_var_sep)
(List.filter
~f:(fun arg ->
(not (String.contains arg env_var_sep))
||
( warnf "WARNING: Ignoring unsupported option containing '%c' character: %s@\n" env_var_sep
arg ;
false ) )
argv)
let decode_env_to_argv env =
String.split ~on:env_var_sep env |> List.filter ~f:(Fn.non String.is_empty)
(** [prefix_before_rest (prefix @ \["--" :: rest\])] is [prefix] where "--" is not in [prefix]. *)
let rev_prefix_before_rest args =
let rec rev_prefix_before_rest_ rev_keep = function
| [] | "--" :: _ ->
rev_keep
| keep :: args ->
rev_prefix_before_rest_ (keep :: rev_keep) args
in
rev_prefix_before_rest_ [] args
(** environment variable use to pass arguments from parent to child processes *)
let args_env_var = "INFER_ARGS"
let extra_env_args = ref []
let extend_env_args args = extra_env_args := List.rev_append args !extra_env_args
let parse_args ~usage initial_action ?initial_command args =
let exe_name = Sys.executable_name in
args_to_parse := Array.of_list (exe_name :: args) ;
arg_being_parsed := 0 ;
let curr_usage = select_parse_mode ~usage initial_action in
Option.iter initial_command ~f:(fun command ->
let switch =
List.Assoc.find_exn !subcommand_actions ~equal:String.equal (string_of_command command)
in
switch () ) ;
(* tests if msg indicates an unknown option, as opposed to a known option with bad argument *)
let is_unknown msg = String.is_substring msg ~substring:": unknown option" in
let rec parse_loop () =
try
Arg.parse_argv_dynamic ~current:arg_being_parsed !args_to_parse curr_speclist anon_fun usage
with
| SubArguments args ->
(* stop parsing the current arguments and parse [args] for a while *)
let saved_args = !args_to_parse in
let saved_current = !arg_being_parsed in
args_to_parse := Array.of_list (exe_name :: args) ;
arg_being_parsed := 0 ;
parse_loop () ;
(* resume argument parsing *)
args_to_parse := saved_args ;
arg_being_parsed := saved_current ;
parse_loop ()
| Arg.Bad usage_msg ->
if !anon_arg_action.on_unknown <> `Reject && is_unknown usage_msg then (
anon_fun !args_to_parse.(!arg_being_parsed) ;
parse_loop () )
else (
ANSITerminal.prerr_string L.(term_styles_of_style Fatal) usage_msg ;
Stdlib.exit 1 )
| Arg.Help _ ->
(* we handle --help by ourselves and error on -help, so Arg has no way to raise Help
anymore *)
assert false
in
parse_loop () ;
curr_usage
let keep_args_file = ref false
let parse ?config_file ~usage action initial_command =
let env_args = decode_env_to_argv (Option.value (Sys.getenv args_env_var) ~default:"") in
let inferconfig_args =
Option.map ~f:decode_inferconfig_to_argv config_file |> Option.value ~default:[]
in
let args_to_export = ref "" in
let add_parsed_args_to_args_to_export () =
(* reread args_to_parse instead of using all_args since mk_path_helper may have modified them *)
let prog_args =
List.rev_append
(rev_prefix_before_rest (Array.to_list !args_to_parse))
(List.rev !extra_env_args)
in
(* do not include program path in args passed via env var *)
let args = Option.value (List.tl prog_args) ~default:[] in
if not (List.is_empty args) then
let arg_string =
if String.equal !args_to_export "" then encode_argv_to_env args
else !args_to_export ^ String.of_char env_var_sep ^ encode_argv_to_env args
in
args_to_export := arg_string
in
(* read .inferconfig first, then env vars, then command-line options *)
parse_args ~usage InferCommand inferconfig_args |> ignore ;
(* NOTE: do not add the contents of .inferconfig to INFER_ARGS. This helps avoid hitting the
command line size limit. *)
parse_args ~usage InferCommand env_args |> ignore ;
add_parsed_args_to_args_to_export () ;
let curr_usage =
let cl_args = match Array.to_list (Sys.get_argv ()) with _ :: tl -> tl | [] -> [] in
let curr_usage = parse_args ~usage action ?initial_command cl_args in
add_parsed_args_to_args_to_export () ;
curr_usage
in
let to_export =
let argv_to_export = decode_env_to_argv !args_to_export in
if not (List.is_empty argv_to_export) then (
(* We have to be careful not to add too much data to the environment because the size of the
environment contributes to the length of the command to be run. If the environment + CLI is
too big, running any command will fail with a cryptic "exit code 127" error. Use an argfile
to prevent this from happening *)
let file = Filename.temp_file "args" "" in
Out_channel.with_file file ~f:(fun oc -> Out_channel.output_lines oc argv_to_export) ;
if not !keep_args_file then Utils.unlink_file_on_exit file ;
"@" ^ file )
else ""
in
Unix.putenv ~key:args_env_var ~data:to_export ;
(!curr_command, curr_usage)
let wrap_line indent_string wrap_length line0 =
let line = indent_string ^ line0 in
let indent_length = String.length indent_string in
let word_sep = ' ' in
let words = String.split ~on:word_sep line in
let word_sep_str = String.of_char word_sep in
let add_word_to_paragraph (rev_lines, non_empty, line, line_length) word =
let word_length =
let len = String.length word in
if String.is_prefix ~prefix:"$(b," word || String.is_prefix ~prefix:"$(i," word then
len - 4 (* length of formatting tag prefix *) - 1
(* APPROXIMATION: closing parenthesis that will come after the word, or maybe later *)
else len
in
let new_length = line_length + String.length word_sep_str + word_length in
let new_non_empty = non_empty || not (String.is_empty word) in
if new_length > wrap_length && non_empty then
(line :: rev_lines, true, indent_string ^ word, indent_length + word_length)
else
let sep = if Int.equal line_length indent_length then "" else word_sep_str in
let new_line = line ^ sep ^ word in
if new_length > wrap_length && new_non_empty then
(new_line :: rev_lines, false, indent_string, indent_length)
else (rev_lines, new_non_empty, new_line, new_length)
in
let rev_lines, _, line, _ = List.fold ~f:add_word_to_paragraph ~init:([], false, "", 0) words in
List.rev (line :: rev_lines)
let show_manual ?(scrub_defaults = false) ?internal_section format default_doc command_opt =
let command_doc =
match command_opt with
| None ->
default_doc
| Some command -> (
match List.Assoc.find_exn ~equal:InferCommand.equal !subcommands command with
| Some command_doc, _, _ ->
command_doc
| None, _, _ ->
L.(die InternalError) "No manual for internal command %s" (string_of_command command) )
in
let pp_meta f meta =
match meta with "" -> () | meta -> F.fprintf f " $(i,%s)" (Cmdliner.Manpage.escape meta)
in
let pp_short f = function "" -> () | s -> Format.fprintf f ",$(b,-%s)" s in
let block_of_desc {long; meta; short; doc; default_string} =
if String.equal doc "" then []
else
let doc =
if scrub_defaults || default_string = "" then doc
else
let doc_default_sep = if String.is_suffix ~suffix:"\n" doc then "" else " " in
Printf.sprintf "%s%s(default: %s)" doc doc_default_sep
(Cmdliner.Manpage.escape default_string)
in
let doc_first_line, doc_other_lines =
match String.split ~on:'\n' doc with first :: other -> (first, other) | [] -> ("", [])
in
(* Cmdline.Manpage does not format multi-paragraph documentation strings correctly for `I
blocks, so we do a bit of formatting by hand *)
let indent_string = " " in
let width =
77 (* Cmdliner.Manpage width limit it seems *) - 7
(* base indentation of documentation strings *)
in
`I (Format.asprintf "$(b,%s)%a%a" (dashdash long) pp_short short pp_meta meta, doc_first_line)
:: List.concat_map
(List.concat_map ~f:(wrap_line indent_string width) doc_other_lines)
~f:(fun s -> [`Noblank; `Pre s])
in
let option_blocks =
match command_doc.manual_options with
| `Replace blocks ->
`S Cmdliner.Manpage.s_options :: blocks
| `Prepend blocks -> (
let hidden =
match internal_section with
| Some section ->
`S section :: `P "Use at your own risk."
:: List.concat_map ~f:block_of_desc (normalize_desc_list !hidden_descs_list)
| None ->
[]
in
match command_opt with
| Some command ->
let sections =
List.Assoc.find_exn ~equal:InferCommand.equal help_sections_desc_lists command
in
SectionMap.fold
(fun section descs result ->
`S section
:: (if String.equal section Cmdliner.Manpage.s_options then blocks else [])
@ List.concat_map ~f:block_of_desc (normalize_desc_list descs)
@ result )
!sections hidden
| None ->
(`S Cmdliner.Manpage.s_options :: blocks)
@ List.concat_map ~f:block_of_desc (normalize_desc_list !visible_descs_list)
@ hidden )
in
let blocks =
[ `Blocks command_doc.manual_before_options
; `Blocks option_blocks
; `Blocks command_doc.manual_after_options ]
in
Cmdliner.Manpage.print format Format.std_formatter (command_doc.title, blocks) ;
()