]>
Commit | Line | Data |
---|---|---|
b7329c8a DM |
1 | use failure::*; |
2 | use std::collections::HashMap; | |
f46403cc | 3 | use std::collections::HashSet; |
698d9d44 | 4 | use serde_json::Value; |
4968bc3a | 5 | |
ef2f2efb | 6 | use crate::api_schema::*; |
dc9a007b DM |
7 | use crate::api_schema::router::*; |
8 | //use crate::api_schema::config::*; | |
0f253593 | 9 | use super::environment::CliEnvironment; |
6049b71f | 10 | |
4de0e142 | 11 | use super::getopts; |
6049b71f | 12 | |
6adb40ee DM |
13 | #[derive(Copy, Clone)] |
14 | enum ParameterDisplayStyle { | |
15 | //Config, | |
16 | //SonfigSub, | |
17 | Arg, | |
18 | Fixed, | |
19 | } | |
20 | ||
2f3f2bb7 DM |
21 | /// CLI usage information format |
22 | #[derive(Copy, Clone, PartialEq)] | |
23 | enum DocumentationFormat { | |
24 | /// text, command line only (one line) | |
25 | Short, | |
26 | /// text, list all options | |
27 | Long, | |
28 | /// text, include description | |
29 | Full, | |
8b6dd224 DM |
30 | /// like full, but in reStructuredText format |
31 | ReST, | |
2f3f2bb7 DM |
32 | } |
33 | ||
34 | fn get_schema_type_text(schema: &Schema, _style: ParameterDisplayStyle) -> String { | |
6adb40ee | 35 | |
2f3f2bb7 | 36 | let type_text = match schema { |
6adb40ee DM |
37 | Schema::Null => String::from("<null>"), // should not happen |
38 | Schema::String(_) => String::from("<string>"), | |
39 | Schema::Boolean(_) => String::from("<boolean>"), | |
c4c51740 DM |
40 | Schema::Integer(integer_schema) => { |
41 | if integer_schema.minimum != None && integer_schema.maximum != None { | |
42 | format!("<integer> ({} - {})", integer_schema.minimum.unwrap(), integer_schema.maximum.unwrap()) | |
43 | } else if integer_schema.minimum != None { | |
44 | format!("<integer> ({} - N)", integer_schema.minimum.unwrap()) | |
45 | } else if integer_schema.maximum != None { | |
46 | format!("<integer> (-N - {})", integer_schema.maximum.unwrap()) | |
47 | } else { | |
48 | String::from("<integer>") | |
49 | } | |
50 | }, | |
6adb40ee DM |
51 | Schema::Object(_) => String::from("<object>"), |
52 | Schema::Array(_) => String::from("<array>"), | |
53 | }; | |
54 | ||
55 | type_text | |
56 | } | |
57 | ||
58 | fn get_property_description( | |
59 | name: &str, | |
2f3f2bb7 DM |
60 | schema: &Schema, |
61 | style: ParameterDisplayStyle, | |
62 | format: DocumentationFormat, | |
6adb40ee DM |
63 | ) -> String { |
64 | ||
65 | let type_text = get_schema_type_text(schema, style); | |
66 | ||
2f3f2bb7 DM |
67 | let (descr, default) = match schema { |
68 | Schema::Null => ("null", None), | |
69 | Schema::String(ref schema) => (schema.description, schema.default.map(|v| v.to_owned())), | |
70 | Schema::Boolean(ref schema) => (schema.description, schema.default.map(|v| v.to_string())), | |
71 | Schema::Integer(ref schema) => (schema.description, schema.default.map(|v| v.to_string())), | |
72 | Schema::Object(ref schema) => (schema.description, None), | |
73 | Schema::Array(ref schema) => (schema.description, None), | |
6adb40ee DM |
74 | }; |
75 | ||
698d9d44 DM |
76 | let default_text = match default { |
77 | Some(text) => format!(" (default={})", text), | |
78 | None => String::new(), | |
79 | }; | |
80 | ||
8b6dd224 DM |
81 | if format == DocumentationFormat::ReST { |
82 | ||
83 | let mut text = match style { | |
84 | ParameterDisplayStyle::Arg => { | |
698d9d44 | 85 | format!(":``--{} {}{}``: ", name, type_text, default_text) |
8b6dd224 DM |
86 | } |
87 | ParameterDisplayStyle::Fixed => { | |
698d9d44 | 88 | format!(":``<{}> {}{}``: ", name, type_text, default_text) |
8b6dd224 DM |
89 | } |
90 | }; | |
91 | ||
92 | text.push_str(descr); | |
93 | text.push('\n'); | |
94 | text.push('\n'); | |
95 | ||
96 | text | |
2f3f2bb7 DM |
97 | |
98 | } else { | |
99 | ||
2f3f2bb7 DM |
100 | let display_name = match style { |
101 | ParameterDisplayStyle::Arg => { | |
102 | format!("--{}", name) | |
103 | } | |
104 | ParameterDisplayStyle::Fixed => { | |
105 | format!("<{}>", name) | |
106 | } | |
107 | }; | |
108 | ||
109 | // fixme: wrap text | |
110 | let mut text = format!(" {:-10} {}{}", display_name, type_text, default_text); | |
111 | let indent = " "; | |
112 | text.push('\n'); | |
113 | text.push_str(indent); | |
114 | text.push_str(descr); | |
115 | text.push('\n'); | |
116 | text.push('\n'); | |
117 | ||
118 | text | |
119 | } | |
6adb40ee DM |
120 | } |
121 | ||
2f3f2bb7 DM |
122 | fn generate_usage_str( |
123 | prefix: &str, | |
124 | cli_cmd: &CliCommand, | |
125 | format: DocumentationFormat, | |
126 | indent: &str) -> String { | |
6adb40ee DM |
127 | |
128 | let arg_param = &cli_cmd.arg_param; | |
129 | let fixed_param = &cli_cmd.fixed_param; | |
130 | let properties = &cli_cmd.info.parameters.properties; | |
2f3f2bb7 | 131 | let description = &cli_cmd.info.parameters.description; |
6adb40ee DM |
132 | |
133 | let mut done_hash = HashSet::<&str>::new(); | |
134 | let mut args = String::new(); | |
135 | ||
136 | for positional_arg in arg_param { | |
f33fa273 | 137 | match properties.get(positional_arg) { |
9bc3ddb8 | 138 | Some((optional, schema)) => { |
f33fa273 | 139 | args.push(' '); |
9bc3ddb8 DM |
140 | |
141 | let is_array = if let Schema::Array(_) = schema.as_ref() { true } else { false }; | |
f33fa273 | 142 | if *optional { args.push('['); } |
9bc3ddb8 | 143 | if is_array { args.push('{'); } |
f33fa273 | 144 | args.push('<'); args.push_str(positional_arg); args.push('>'); |
9bc3ddb8 | 145 | if is_array { args.push('}'); } |
f33fa273 DM |
146 | if *optional { args.push(']'); } |
147 | ||
148 | done_hash.insert(positional_arg); | |
149 | } | |
150 | None => panic!("no such property '{}' in schema", positional_arg), | |
151 | } | |
6adb40ee DM |
152 | } |
153 | ||
2f3f2bb7 DM |
154 | let mut arg_descr = String::new(); |
155 | for positional_arg in arg_param { | |
156 | let (_optional, schema) = properties.get(positional_arg).unwrap(); | |
157 | let param_descr = get_property_description( | |
158 | positional_arg, &schema, ParameterDisplayStyle::Fixed, format); | |
159 | arg_descr.push_str(¶m_descr); | |
160 | } | |
161 | ||
6adb40ee DM |
162 | let mut options = String::new(); |
163 | ||
164 | let mut prop_names: Vec<&str> = properties.keys().map(|v| *v).collect(); | |
165 | prop_names.sort(); | |
166 | ||
167 | for prop in prop_names { | |
168 | let (optional, schema) = properties.get(prop).unwrap(); | |
169 | if done_hash.contains(prop) { continue; } | |
170 | if fixed_param.contains(&prop) { continue; } | |
b7329c8a | 171 | |
2f3f2bb7 | 172 | let type_text = get_schema_type_text(&schema, ParameterDisplayStyle::Arg); |
6adb40ee DM |
173 | |
174 | if *optional { | |
175 | ||
b55cee92 | 176 | if options.len() > 0 { options.push('\n'); } |
2f3f2bb7 | 177 | options.push_str(&get_property_description(prop, &schema, ParameterDisplayStyle::Arg, format)); |
6adb40ee DM |
178 | |
179 | } else { | |
180 | args.push_str("--"); args.push_str(prop); | |
181 | args.push(' '); | |
182 | args.push_str(&type_text); | |
183 | } | |
184 | ||
185 | done_hash.insert(prop); | |
186 | } | |
187 | ||
698d9d44 DM |
188 | let option_indicator = if options.len() > 0 { " [OPTIONS]" } else { "" }; |
189 | ||
8b6dd224 | 190 | let mut text = match format { |
2f3f2bb7 | 191 | DocumentationFormat::Short => { |
698d9d44 | 192 | return format!("{}{}{}{}\n\n", indent, prefix, args, option_indicator); |
2f3f2bb7 DM |
193 | } |
194 | DocumentationFormat::Long => { | |
698d9d44 | 195 | format!("{}{}{}{}\n\n", indent, prefix, args, option_indicator) |
2f3f2bb7 DM |
196 | } |
197 | DocumentationFormat::Full => { | |
698d9d44 | 198 | format!("{}{}{}{}\n\n{}\n\n", indent, prefix, args, option_indicator, description) |
2f3f2bb7 | 199 | } |
8b6dd224 | 200 | DocumentationFormat::ReST => { |
698d9d44 | 201 | format!("``{} {}{}``\n\n{}\n\n", prefix, args.trim(), option_indicator, description) |
2f3f2bb7 | 202 | } |
8b6dd224 DM |
203 | }; |
204 | ||
205 | if arg_descr.len() > 0 { | |
8b6dd224 | 206 | text.push_str(&arg_descr); |
698d9d44 | 207 | text.push('\n'); |
8b6dd224 DM |
208 | } |
209 | if options.len() > 0 { | |
8b6dd224 | 210 | text.push_str(&options); |
698d9d44 | 211 | text.push('\n'); |
2f3f2bb7 | 212 | } |
8b6dd224 | 213 | text |
6adb40ee DM |
214 | } |
215 | ||
216 | fn print_simple_usage_error(prefix: &str, cli_cmd: &CliCommand, err: Error) { | |
217 | ||
2f3f2bb7 | 218 | let usage = generate_usage_str(prefix, cli_cmd, DocumentationFormat::Long, ""); |
8b6dd224 | 219 | eprint!("Error: {}\nUsage: {}", err, usage); |
b7329c8a DM |
220 | } |
221 | ||
698d9d44 DM |
222 | fn print_help( |
223 | top_def: &CommandLineInterface, | |
224 | mut prefix: String, | |
225 | args: &Vec<String>, | |
226 | verbose: Option<bool>, | |
227 | ) { | |
228 | let mut iface = top_def; | |
229 | ||
230 | for cmd in args { | |
231 | if let CommandLineInterface::Nested(map) = iface { | |
232 | if let Some(subcmd) = find_command(map, cmd) { | |
233 | iface = subcmd; | |
234 | prefix.push(' '); | |
235 | prefix.push_str(cmd); | |
236 | continue; | |
237 | } | |
238 | } | |
239 | eprintln!("no such command '{}'", cmd); | |
240 | std::process::exit(-1); | |
241 | } | |
242 | ||
243 | let format = match verbose.unwrap_or(false) { | |
244 | true => DocumentationFormat::Full, | |
245 | false => DocumentationFormat::Short, | |
246 | }; | |
247 | ||
248 | match iface { | |
249 | CommandLineInterface::Nested(map) => { | |
250 | println!("Usage:\n\n{}", generate_nested_usage(&prefix, map, format)); | |
251 | } | |
252 | CommandLineInterface::Simple(cli_cmd) => { | |
253 | println!("Usage: {}", generate_usage_str(&prefix, cli_cmd, format, "")); | |
254 | } | |
255 | } | |
256 | } | |
257 | ||
258 | fn handle_simple_command( | |
259 | top_def: &CommandLineInterface, | |
260 | prefix: &str, | |
261 | cli_cmd: &CliCommand, | |
262 | args: Vec<String>, | |
263 | ) { | |
4968bc3a | 264 | |
33256db6 DM |
265 | let (params, rest) = match getopts::parse_arguments( |
266 | &args, &cli_cmd.arg_param, &cli_cmd.info.parameters) { | |
267 | Ok((p, r)) => (p, r), | |
268 | Err(err) => { | |
6adb40ee | 269 | print_simple_usage_error(prefix, cli_cmd, err.into()); |
33256db6 DM |
270 | std::process::exit(-1); |
271 | } | |
272 | }; | |
b7329c8a | 273 | |
698d9d44 DM |
274 | if (cli_cmd.info.handler as *const fn()) == (dummy_help as *const fn()) { |
275 | let prefix = prefix.split(' ').next().unwrap().to_string(); | |
276 | print_help(top_def, prefix, &rest, params["verbose"].as_bool()); | |
277 | return; | |
278 | } | |
279 | ||
b7329c8a | 280 | if !rest.is_empty() { |
33256db6 | 281 | let err = format_err!("got additional arguments: {:?}", rest); |
6adb40ee | 282 | print_simple_usage_error(prefix, cli_cmd, err); |
33256db6 | 283 | std::process::exit(-1); |
b7329c8a DM |
284 | } |
285 | ||
33256db6 DM |
286 | let mut rpcenv = CliEnvironment::new(); |
287 | ||
288 | match (cli_cmd.info.handler)(params, &cli_cmd.info, &mut rpcenv) { | |
289 | Ok(value) => { | |
290 | println!("Result: {}", serde_json::to_string_pretty(&value).unwrap()); | |
291 | } | |
292 | Err(err) => { | |
293 | eprintln!("Error: {}", err); | |
294 | } | |
295 | } | |
b7329c8a DM |
296 | } |
297 | ||
6460764d | 298 | fn find_command<'a>(def: &'a CliCommandMap, name: &str) -> Option<&'a CommandLineInterface> { |
baed30b7 | 299 | |
6460764d | 300 | if let Some(sub_cmd) = def.commands.get(name) { |
baed30b7 DM |
301 | return Some(sub_cmd); |
302 | }; | |
303 | ||
304 | let mut matches: Vec<&str> = vec![]; | |
305 | ||
6460764d | 306 | for cmd in def.commands.keys() { |
baed30b7 DM |
307 | if cmd.starts_with(name) { |
308 | matches.push(cmd); } | |
309 | } | |
310 | ||
311 | if matches.len() != 1 { return None; } | |
312 | ||
6460764d | 313 | if let Some(sub_cmd) = def.commands.get(matches[0]) { |
baed30b7 DM |
314 | return Some(sub_cmd); |
315 | }; | |
316 | ||
317 | None | |
318 | } | |
319 | ||
6adb40ee DM |
320 | fn print_nested_usage_error(prefix: &str, def: &CliCommandMap, err: Error) { |
321 | ||
698d9d44 | 322 | let usage = generate_nested_usage(prefix, def, DocumentationFormat::Short); |
6adb40ee | 323 | |
698d9d44 | 324 | eprintln!("Error: {}\n\nUsage:\n\n{}", err, usage); |
6adb40ee | 325 | } |
33256db6 | 326 | |
8b6dd224 | 327 | fn generate_nested_usage(prefix: &str, def: &CliCommandMap, format: DocumentationFormat) -> String { |
6adb40ee DM |
328 | |
329 | let mut cmds: Vec<&String> = def.commands.keys().collect(); | |
330 | cmds.sort(); | |
331 | ||
8b6dd224 DM |
332 | let mut usage = String::new(); |
333 | ||
6adb40ee DM |
334 | for cmd in cmds { |
335 | let new_prefix = format!("{} {}", prefix, cmd); | |
336 | ||
337 | match def.commands.get(cmd).unwrap() { | |
338 | CommandLineInterface::Simple(cli_cmd) => { | |
8b6dd224 DM |
339 | if usage.len() > 0 && format == DocumentationFormat::ReST { |
340 | usage.push_str("----\n\n"); | |
341 | } | |
342 | usage.push_str(&generate_usage_str(&new_prefix, cli_cmd, format, "")); | |
6adb40ee DM |
343 | } |
344 | CommandLineInterface::Nested(map) => { | |
8b6dd224 | 345 | usage.push_str(&generate_nested_usage(&new_prefix, map, format)); |
6adb40ee DM |
346 | } |
347 | } | |
348 | } | |
33256db6 | 349 | |
8b6dd224 | 350 | usage |
33256db6 DM |
351 | } |
352 | ||
698d9d44 DM |
353 | fn handle_nested_command( |
354 | top_def: &CommandLineInterface, | |
355 | prefix: &str, | |
356 | def: &CliCommandMap, | |
357 | mut args: Vec<String>, | |
358 | ) { | |
b7329c8a DM |
359 | |
360 | if args.len() < 1 { | |
6460764d | 361 | let mut cmds: Vec<&String> = def.commands.keys().collect(); |
b7329c8a DM |
362 | cmds.sort(); |
363 | ||
364 | let list = cmds.iter().fold(String::new(),|mut s,item| { | |
365 | if !s.is_empty() { s+= ", "; } | |
366 | s += item; | |
367 | s | |
368 | }); | |
369 | ||
33256db6 | 370 | let err = format_err!("no command specified.\nPossible commands: {}", list); |
6adb40ee | 371 | print_nested_usage_error(prefix, def, err); |
33256db6 | 372 | std::process::exit(-1); |
b7329c8a DM |
373 | } |
374 | ||
375 | let command = args.remove(0); | |
376 | ||
baed30b7 | 377 | let sub_cmd = match find_command(def, &command) { |
b7329c8a | 378 | Some(cmd) => cmd, |
33256db6 DM |
379 | None => { |
380 | let err = format_err!("no such command '{}'", command); | |
6adb40ee | 381 | print_nested_usage_error(prefix, def, err); |
33256db6 DM |
382 | std::process::exit(-1); |
383 | } | |
b7329c8a DM |
384 | }; |
385 | ||
33256db6 DM |
386 | let new_prefix = format!("{} {}", prefix, command); |
387 | ||
b7329c8a | 388 | match sub_cmd { |
9f6ab1fc | 389 | CommandLineInterface::Simple(cli_cmd) => { |
698d9d44 | 390 | handle_simple_command(top_def, &new_prefix, cli_cmd, args); |
b7329c8a | 391 | } |
9f6ab1fc | 392 | CommandLineInterface::Nested(map) => { |
698d9d44 | 393 | handle_nested_command(top_def, &new_prefix, map, args); |
b7329c8a DM |
394 | } |
395 | } | |
b7329c8a DM |
396 | } |
397 | ||
30d2e99c DM |
398 | fn print_property_completion( |
399 | schema: &Schema, | |
400 | name: &str, | |
401 | completion_functions: &HashMap<String, CompletionFunction>, | |
402 | arg: &str) | |
403 | { | |
404 | if let Some(callback) = completion_functions.get(name) { | |
244d9b17 | 405 | let list = (callback)(arg); |
30d2e99c DM |
406 | for value in list { |
407 | if value.starts_with(arg) { | |
408 | println!("{}", value); | |
409 | } | |
410 | } | |
411 | return; | |
412 | } | |
413 | ||
38555b50 DM |
414 | if let Schema::String(StringSchema { format: Some(format), ..} ) = schema { |
415 | if let ApiStringFormat::Enum(list) = format.as_ref() { | |
416 | for value in list { | |
417 | if value.starts_with(arg) { | |
418 | println!("{}", value); | |
419 | } | |
420 | } | |
421 | return; | |
422 | } | |
423 | } | |
f46403cc DM |
424 | println!(""); |
425 | } | |
b6e8dd39 | 426 | |
f46403cc DM |
427 | fn record_done_arguments(done: &mut HashSet<String>, parameters: &ObjectSchema, list: &[String]) { |
428 | ||
429 | for arg in list { | |
430 | if arg.starts_with("--") && arg.len() > 2 { | |
431 | let prop_name = arg[2..].to_owned(); | |
379ea0ed | 432 | if let Some((_, schema)) = parameters.properties.get::<str>(&prop_name) { |
f46403cc DM |
433 | match schema.as_ref() { |
434 | Schema::Array(_) => { /* do nothing */ } | |
435 | _ => { done.insert(prop_name); } | |
436 | } | |
437 | } | |
438 | } | |
439 | } | |
440 | } | |
441 | ||
442 | fn print_simple_completion( | |
443 | cli_cmd: &CliCommand, | |
ca9caffa | 444 | done: &mut HashSet<String>, |
f46403cc | 445 | arg_param: &[&str], |
6949d915 | 446 | args: &[String], |
f46403cc DM |
447 | ) { |
448 | // fixme: arg_param, fixed_param | |
449 | //eprintln!("COMPL: {:?} {:?} {}", arg_param, args, args.len()); | |
450 | ||
451 | if !arg_param.is_empty() { | |
452 | let prop_name = arg_param[0]; | |
453 | done.insert(prop_name.into()); | |
454 | if args.len() > 1 { | |
6949d915 | 455 | print_simple_completion(cli_cmd, done, &arg_param[1..], &args[1..]); |
f46403cc | 456 | return; |
7f4e639b | 457 | } else if args.len() == 1 { |
379ea0ed | 458 | if let Some((_, schema)) = cli_cmd.info.parameters.properties.get(prop_name) { |
30d2e99c | 459 | print_property_completion(schema, prop_name, &cli_cmd.completion_functions, &args[0]); |
b6e8dd39 | 460 | } |
f46403cc DM |
461 | } |
462 | return; | |
463 | } | |
464 | if args.is_empty() { return; } | |
465 | ||
ca9caffa | 466 | record_done_arguments(done, &cli_cmd.info.parameters, &args); |
2f025895 | 467 | |
6949d915 | 468 | let prefix = &args[args.len()-1]; // match on last arg |
f46403cc | 469 | |
2f025895 | 470 | // complete option-name or option-value ? |
6949d915 DM |
471 | if !prefix.starts_with("-") && args.len() > 1 { |
472 | let last = &args[args.len()-2]; | |
2f025895 | 473 | if last.starts_with("--") && last.len() > 2 { |
ca9caffa | 474 | let prop_name = &last[2..]; |
379ea0ed | 475 | if let Some((_, schema)) = cli_cmd.info.parameters.properties.get(prop_name) { |
30d2e99c | 476 | print_property_completion(schema, prop_name, &cli_cmd.completion_functions, &prefix); |
2f025895 DM |
477 | } |
478 | return; | |
479 | } | |
480 | } | |
f46403cc | 481 | |
379ea0ed | 482 | for (name, (_optional, _schema)) in &cli_cmd.info.parameters.properties { |
f46403cc | 483 | if done.contains(*name) { continue; } |
2f025895 | 484 | let option = String::from("--") + name; |
6949d915 | 485 | if option.starts_with(prefix) { |
2f025895 | 486 | println!("{}", option); |
f46403cc DM |
487 | } |
488 | } | |
489 | } | |
490 | ||
9d78d579 DM |
491 | fn print_help_completion(def: &CommandLineInterface, help_cmd: &CliCommand, args: &[String]) { |
492 | ||
793b0f4d DM |
493 | let mut done = HashSet::new(); |
494 | ||
9d78d579 DM |
495 | match def { |
496 | CommandLineInterface::Simple(_) => { | |
6949d915 | 497 | print_simple_completion(help_cmd, &mut done, &help_cmd.arg_param, args); |
9d78d579 DM |
498 | return; |
499 | } | |
500 | CommandLineInterface::Nested(map) => { | |
501 | if args.is_empty() { | |
502 | for cmd in map.commands.keys() { | |
503 | println!("{}", cmd); | |
504 | } | |
505 | return; | |
506 | } | |
793b0f4d | 507 | |
9d78d579 | 508 | let first = &args[0]; |
793b0f4d DM |
509 | |
510 | if first.starts_with("-") { | |
511 | print_simple_completion(help_cmd, &mut done, &help_cmd.arg_param, args); | |
512 | return; | |
513 | } | |
514 | ||
9d78d579 DM |
515 | if let Some(sub_cmd) = map.commands.get(first) { |
516 | print_help_completion(sub_cmd, help_cmd, &args[1..]); | |
517 | return; | |
518 | } | |
793b0f4d | 519 | |
9d78d579 DM |
520 | for cmd in map.commands.keys() { |
521 | if cmd.starts_with(first) { | |
522 | println!("{}", cmd); | |
523 | } | |
524 | } | |
525 | } | |
526 | } | |
527 | } | |
528 | ||
6949d915 | 529 | fn print_nested_completion(def: &CommandLineInterface, args: &[String]) { |
f46403cc DM |
530 | |
531 | match def { | |
532 | CommandLineInterface::Simple(cli_cmd) => { | |
533 | let mut done = HashSet::new(); | |
534 | let fixed: Vec<String> = cli_cmd.fixed_param.iter().map(|s| s.to_string()).collect(); | |
535 | record_done_arguments(&mut done, &cli_cmd.info.parameters, &fixed); | |
536 | print_simple_completion(cli_cmd, &mut done, &cli_cmd.arg_param, args); | |
b6e8dd39 DM |
537 | return; |
538 | } | |
539 | CommandLineInterface::Nested(map) => { | |
540 | if args.is_empty() { | |
541 | for cmd in map.commands.keys() { | |
542 | println!("{}", cmd); | |
543 | } | |
544 | return; | |
545 | } | |
6949d915 | 546 | let first = &args[0]; |
f4e4d583 DM |
547 | if args.len() > 1 { |
548 | if let Some(sub_cmd) = map.commands.get(first) { | |
549 | print_nested_completion(sub_cmd, &args[1..]); | |
550 | return; | |
551 | } | |
b6e8dd39 DM |
552 | } |
553 | for cmd in map.commands.keys() { | |
6949d915 | 554 | if cmd.starts_with(first) { |
b6e8dd39 DM |
555 | println!("{}", cmd); |
556 | } | |
557 | } | |
558 | } | |
559 | } | |
560 | } | |
561 | ||
562 | pub fn print_bash_completion(def: &CommandLineInterface) { | |
563 | ||
564 | let comp_point: usize = match std::env::var("COMP_POINT") { | |
565 | Ok(val) => { | |
566 | match usize::from_str_radix(&val, 10) { | |
567 | Ok(i) => i, | |
a27a3ee4 | 568 | Err(_) => return, |
b6e8dd39 DM |
569 | } |
570 | } | |
a27a3ee4 | 571 | Err(_) => return, |
b6e8dd39 DM |
572 | }; |
573 | ||
574 | let cmdline = match std::env::var("COMP_LINE") { | |
575 | Ok(val) => val[0..comp_point].to_owned(), | |
a27a3ee4 | 576 | Err(_) => return, |
b6e8dd39 DM |
577 | }; |
578 | ||
579 | let mut args = match shellwords::split(&cmdline) { | |
580 | Ok(v) => v, | |
581 | Err(_) => return, | |
582 | }; | |
583 | ||
25e77d38 DM |
584 | if args.len() == 0 { return; } |
585 | ||
b6e8dd39 DM |
586 | args.remove(0); //no need for program name |
587 | ||
f46403cc DM |
588 | if cmdline.ends_with(char::is_whitespace) { |
589 | //eprintln!("CMDLINE {:?}", cmdline); | |
590 | args.push("".into()); | |
591 | } | |
592 | ||
9d78d579 DM |
593 | if !args.is_empty() && args[0] == "help" { |
594 | print_help_completion(def, &help_command_def(), &args[1..]); | |
595 | } else { | |
6949d915 | 596 | print_nested_completion(def, &args); |
9d78d579 | 597 | } |
b6e8dd39 DM |
598 | } |
599 | ||
9d78d579 DM |
600 | fn help_command_def() -> CliCommand { |
601 | CliCommand::new( | |
698d9d44 DM |
602 | ApiMethod::new( |
603 | dummy_help, | |
604 | ObjectSchema::new("Get help about specified command.") | |
605 | .optional("verbose", BooleanSchema::new("Verbose help.")) | |
606 | ) | |
9d78d579 DM |
607 | ) |
608 | } | |
609 | ||
610 | pub fn run_cli_command(def: CommandLineInterface) { | |
698d9d44 DM |
611 | |
612 | let def = match def { | |
613 | CommandLineInterface::Simple(cli_cmd) => CommandLineInterface::Simple(cli_cmd), | |
9d78d579 DM |
614 | CommandLineInterface::Nested(map) => |
615 | CommandLineInterface::Nested(map.insert("help", help_command_def().into())), | |
698d9d44 DM |
616 | }; |
617 | ||
618 | let top_def = &def; // we pass this to the help function ... | |
33256db6 DM |
619 | |
620 | let mut args = std::env::args(); | |
b7329c8a | 621 | |
33256db6 DM |
622 | let prefix = args.next().unwrap(); |
623 | let prefix = prefix.rsplit('/').next().unwrap(); // without path | |
624 | ||
625 | let args: Vec<String> = args.collect(); | |
b7329c8a | 626 | |
8b6dd224 DM |
627 | if !args.is_empty() { |
628 | if args[0] == "bashcomplete" { | |
698d9d44 | 629 | print_bash_completion(&def); |
8b6dd224 DM |
630 | return; |
631 | } | |
632 | ||
633 | if args[0] == "printdoc" { | |
634 | let usage = match def { | |
635 | CommandLineInterface::Simple(cli_cmd) => { | |
698d9d44 | 636 | generate_usage_str(&prefix, &cli_cmd, DocumentationFormat::ReST, "") |
8b6dd224 DM |
637 | } |
638 | CommandLineInterface::Nested(map) => { | |
698d9d44 | 639 | generate_nested_usage(&prefix, &map, DocumentationFormat::ReST) |
8b6dd224 DM |
640 | } |
641 | }; | |
642 | println!("{}", usage); | |
643 | return; | |
644 | } | |
c21de379 DM |
645 | } |
646 | ||
33256db6 | 647 | match def { |
698d9d44 DM |
648 | CommandLineInterface::Simple(ref cli_cmd) => handle_simple_command(top_def, &prefix, &cli_cmd, args), |
649 | CommandLineInterface::Nested(ref map) => handle_nested_command(top_def, &prefix, &map, args), | |
4968bc3a | 650 | }; |
b7329c8a DM |
651 | } |
652 | ||
244d9b17 | 653 | pub type CompletionFunction = fn(&str) -> Vec<String>; |
30d2e99c | 654 | |
b7329c8a DM |
655 | pub struct CliCommand { |
656 | pub info: ApiMethod, | |
657 | pub arg_param: Vec<&'static str>, | |
658 | pub fixed_param: Vec<&'static str>, | |
30d2e99c | 659 | pub completion_functions: HashMap<String, CompletionFunction>, |
b7329c8a DM |
660 | } |
661 | ||
bf7f1039 DM |
662 | impl CliCommand { |
663 | ||
664 | pub fn new(info: ApiMethod) -> Self { | |
30d2e99c DM |
665 | Self { |
666 | info, arg_param: vec![], | |
667 | fixed_param: vec![], | |
668 | completion_functions: HashMap::new(), | |
669 | } | |
bf7f1039 DM |
670 | } |
671 | ||
672 | pub fn arg_param(mut self, names: Vec<&'static str>) -> Self { | |
673 | self.arg_param = names; | |
674 | self | |
675 | } | |
676 | ||
677 | pub fn fixed_param(mut self, args: Vec<&'static str>) -> Self { | |
678 | self.fixed_param = args; | |
679 | self | |
680 | } | |
30d2e99c DM |
681 | |
682 | pub fn completion_cb(mut self, param_name: &str, cb: CompletionFunction) -> Self { | |
683 | self.completion_functions.insert(param_name.into(), cb); | |
684 | self | |
685 | } | |
bf7f1039 DM |
686 | } |
687 | ||
6460764d DM |
688 | pub struct CliCommandMap { |
689 | pub commands: HashMap<String, CommandLineInterface>, | |
690 | } | |
691 | ||
698d9d44 DM |
692 | fn dummy_help(_param: Value, _info: &ApiMethod, _rpcenv: &mut RpcEnvironment) -> Result<Value, Error> { |
693 | panic!("internal error"); // this is just a place holder - never call this | |
694 | } | |
695 | ||
6460764d DM |
696 | impl CliCommandMap { |
697 | ||
698 | pub fn new() -> Self { | |
699 | Self { commands: HashMap:: new() } | |
700 | } | |
701 | ||
702 | pub fn insert<S: Into<String>>(mut self, name: S, cli: CommandLineInterface) -> Self { | |
703 | self.commands.insert(name.into(), cli); | |
704 | self | |
705 | } | |
706 | } | |
707 | ||
9f6ab1fc | 708 | pub enum CommandLineInterface { |
b7329c8a | 709 | Simple(CliCommand), |
6460764d | 710 | Nested(CliCommandMap), |
b7329c8a | 711 | } |
8f62336b DM |
712 | |
713 | impl From<CliCommand> for CommandLineInterface { | |
714 | fn from(cli_cmd: CliCommand) -> Self { | |
715 | CommandLineInterface::Simple(cli_cmd) | |
716 | } | |
717 | } | |
718 | ||
6460764d DM |
719 | impl From<CliCommandMap> for CommandLineInterface { |
720 | fn from(list: CliCommandMap) -> Self { | |
721 | CommandLineInterface::Nested(list) | |
8f62336b DM |
722 | } |
723 | } |