1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
|
(* Copyright (C) 2013, Thomas Leonard
* See the README file for details, or visit http://0install.net.
*)
(** Parsing command-line arguments *)
open Common
exception Usage_error of int (* exit code: e.g. 0 for success, 1 for error *)
type raw_option = (string * string list)
let starts_with = XString.starts_with
class type ['b] option_reader =
object
method read : string -> string option -> string Stream.t -> completion:(int option) -> string list
method get_arg_types : int -> 'b list
end
class type ['a,'b] option_parser =
object
method get_reader : 'b option_reader
method parse : string list -> 'a
end
type ('a,'b) opt_spec = (string list * int * string * ('a,'b) option_parser)
let is_empty stream = None = Stream.peek stream
type 'a parsed_options = (string * 'a) list
type ('a,'b) argparse_spec = {
options_spec : ('a,'b) opt_spec list;
(* We've just read an argument; should any futher options be treated as arguments? *)
no_more_options : string list -> bool
}
let re_equals = Str.regexp_string "="
type 'b complete =
| CompleteNothing (** There are no possible completions *)
| CompleteOptionName of string (** Complete this partial option name *)
| CompleteOption of (string * 'b option_reader * string list * int) (* option, reader, values, completion arg *)
| CompleteArg of int
| CompleteLiteral of string (** This is the single possible completion *)
let make_option_map options_spec =
let map = ref XString.Map.empty in
let add (names, _nargs, _help, handler) =
ListLabels.iter names ~f:(fun name ->
if XString.Map.mem name !map then (
let reader = handler#get_reader in
if reader != (XString.Map.find_safe name !map)#get_reader then
failwith ("Option '" ^ name ^ "' has two different readers")
) else (
map := XString.Map.add name handler !map
)
) in
List.iter add options_spec;
!map
let read_args ?(cword) (spec : ('a,'b) argparse_spec) input_args =
let options = ref [] in
let args = ref [] in
let complete = ref CompleteNothing in
let options_map = make_option_map spec.options_spec in
let lookup_option x =
XString.Map.find_opt x options_map |> pipe_some (fun r -> Some r#get_reader) in
let allow_options = ref true in
let stream = Stream.of_list input_args in
(* 0 if the next item we will read is the completion word, etc.
None if we're not doing completion. *)
let args_to_cword () =
match cword with
| None -> None
| Some i -> Some (i - Stream.count stream) in
(* Read from [stream] all the values needed by option [opt] and add to [options].
[carg] is the argument to complete, if in range. -1 to complete the option itself. *)
let handle_option stream opt ~carg =
match lookup_option opt with
| None ->
if carg = Some (-1) then (
(* We are completing this option *)
complete := CompleteOptionName opt
) else if cword <> None then (
(* We are completing elsewhere; just skip unknown options *)
()
) else (
Safe_exn.failf "Unknown option '%s'" opt
)
| Some handler ->
let command = match !args with
| command :: _ -> Some command
| _ -> None in
let values = handler#read opt command stream ~completion:carg in
options := (opt, values) :: !options;
match carg with
| None -> ()
| Some -1 ->
(* Even with an exact match, there may be a longer option *)
complete := CompleteOptionName opt
| Some carg ->
if carg >= 0 && carg < List.length values then (
complete := CompleteOption (opt, handler, values, carg)
)
in
let handle_long_option opt =
match Str.bounded_split_delim re_equals opt 2 with
| [key; value] ->
let consumed_value = ref false in
let value_stream _ =
if !consumed_value then (
Some (Stream.next stream)
) else (
consumed_value := true;
Some value
) in
(* If the arg being completed contains an "=", we're always completing the value part *)
let carg = match args_to_cword () with
| None -> None
| Some 0 -> Some 1
| Some _ -> None in
handle_option (Stream.from value_stream) key ~carg;
if cword = None && not !consumed_value then
Safe_exn.failf "Option does not take an argument in '%s'" opt
| _ -> handle_option stream opt ~carg:(args_to_cword ()) in
let handle_short_option opt =
let do_completion = args_to_cword () = Some (-1) in
let is_valid = ref true in
let i = ref 1 in
while !i < String.length opt do
let opt_stream : string Stream.t =
if !i + 1 = String.length opt then (
(* If we're on the last character, any value comes from the next argument *)
stream
) else (
(* If [handle_option] needs an argument, get it from the rest of this
option and stop processing after that. *)
let get_value _ =
let start = !i + 1 in
let value = String.sub opt start (String.length opt - start) in
i := String.length opt;
Some value in
Stream.from get_value
) in
let opt_name = "-" ^ (String.make 1 @@ opt.[!i]) in
let carg = if do_completion then Some (-1) else None in
handle_option opt_stream opt_name ~carg;
i := !i + 1;
if do_completion && !is_valid && not (XString.Map.mem opt_name options_map) then
is_valid := false;
done;
if do_completion then (
if !is_valid then
complete := CompleteLiteral opt
else
complete := CompleteNothing
)
in
while not (is_empty stream) do
let completing_this = args_to_cword () = Some 0 in
match Stream.next stream with
| "-" when completing_this ->
complete := CompleteOptionName "--"
| "--" when !allow_options ->
if completing_this then
handle_long_option "--" (* start of option being completed *)
else
allow_options := false (* end of options marker *)
| opt when !allow_options && starts_with opt "--" -> handle_long_option opt
| opt when !allow_options && starts_with opt "-" -> handle_short_option opt
| arg ->
if completing_this && !complete = CompleteNothing then (
complete := CompleteArg (List.length !args);
);
args := arg :: !args;
if !allow_options && spec.no_more_options !args then allow_options := false
done;
(List.rev !options, List.rev !args, !complete)
let parse_options valid_options raw_options =
let map = make_option_map valid_options in
let parse_option = function
| (name, values) ->
match XString.Map.find_opt name map with
| None -> Safe_exn.failf "Option '%s' is not valid here" name
| Some reader ->
try (name, reader#parse values)
with Safe_exn.T _ as ex -> Safe_exn.reraise_with ex "... processing option '%s'" name in
List.map parse_option raw_options
let iter_options options fn =
let process (actual_opt, value) =
try fn value
with Safe_exn.T _ as ex -> Safe_exn.reraise_with ex "... processing option '%s'" actual_opt
in List.iter process options
(** {2 Handy wrappers for option handlers} *)
let no_arg_reader =
object
method read _name _command _stream ~completion:_ = []
method get_arg_types _ = []
end
class ['a,'b] no_arg (value : 'a) =
object (_ : ('a,'b) #option_parser)
method parse = function
| [] -> value
| _ -> failwith "Expected no arguments!"
method get_reader = no_arg_reader
end
class ['a,'b] one_arg arg_type (fn : string -> 'a) =
object (_ : ('a,'b) #option_parser)
method parse = function
| [item] -> fn item
| _ -> failwith "Expected a single item!"
method get_reader =
object
method get_arg_types _ = [arg_type]
method read opt_name _command stream ~completion =
match Stream.peek stream with
| None when completion <> None -> [""]
| None -> Safe_exn.failf "Missing value for option %s" opt_name
| Some next -> Stream.junk stream; [next]
end
end
class ['a,'b] two_arg arg1_type arg2_type (fn : string -> string -> 'a) =
object (_ : ('a,'b) #option_parser)
method parse = function
| [a; b] -> fn a b
| _ -> failwith "Expected a pair of items!"
method get_reader =
object
method read opt_name _command stream ~completion =
match Stream.npeek 2 stream with
| [_; _] as pair -> Stream.junk stream; Stream.junk stream; pair
| _ when completion = None -> Safe_exn.failf "Missing value for option %s" opt_name
| [x] -> Stream.junk stream; [x; ""]
| _ -> [""; ""]
method get_arg_types _ = [arg1_type; arg2_type]
end
end
let pp_options format_type fmt opts =
let display_options =
opts |> List.filter_map (fun (names, (nargs:int), help, p) ->
match help with
| "" -> None
| help ->
let types = p#get_reader#get_arg_types nargs in
let format_opt name =
let sep = if not (starts_with name "--") then " " else "=" in
name ^ match types with
| [] -> ""
| [x] -> sep ^ format_type x
| xs -> sep ^ String.concat " " (List.map format_type xs) in
let arg_strs = String.concat ", " (List.map format_opt names) in
Some (arg_strs, help)) in
let col1_width = 2 + (min 20 @@ List.fold_left (fun w (syn, _help) -> max (String.length syn) w) 0 display_options) in
let spaces n = String.make n ' ' in
let pp_items fmt items =
let need_cut = ref false in
items |> List.iter (fun (syn, help) ->
if !need_cut then Format.pp_print_cut fmt ()
else need_cut := true;
let padding = col1_width - String.length syn in
if padding > 0 then
Format.fprintf fmt "%s%s%s" syn (spaces padding) help
else
Format.fprintf fmt "%s@\n%s%s" syn (spaces col1_width) help
) in
Format.fprintf fmt "Options:@,@[<v2> %a@]" pp_items display_options
|