]> git.proxmox.com Git - proxmox-backup.git/blob - src/cli/command.rs
5b4a321672f86a53425d238ed15a00673210bc2e
[proxmox-backup.git] / src / cli / command.rs
1 use failure::*;
2 use lazy_static::lazy_static;
3 use std::sync::Arc;
4 use serde_json::Value;
5
6 use std::collections::{HashMap, HashSet};
7
8 use crate::api_schema::*;
9 use crate::api_schema::router::*;
10 use crate::api_schema::format::*;
11 //use crate::api_schema::config::*;
12 use super::environment::CliEnvironment;
13
14 use super::getopts;
15
16 lazy_static!{
17
18 pub static ref OUTPUT_FORMAT: Arc<Schema> =
19 StringSchema::new("Output format.")
20 .format(Arc::new(ApiStringFormat::Enum(&["text", "json", "json-pretty"])))
21 .into();
22
23 }
24
25 /// Helper function to format and print result
26 ///
27 /// This is implemented for machine generatable formats 'json' and
28 /// 'json-pretty'. The 'text' format needs to be handled somewhere
29 /// else.
30 pub fn format_and_print_result(result: &Value, output_format: &str) {
31
32 if output_format == "json-pretty" {
33 println!("{}", serde_json::to_string_pretty(&result).unwrap());
34 } else if output_format == "json" {
35 println!("{}", serde_json::to_string(&result).unwrap());
36 } else {
37 unimplemented!();
38 }
39 }
40
41 fn generate_usage_str(
42 prefix: &str,
43 cli_cmd: &CliCommand,
44 format: DocumentationFormat,
45 indent: &str) -> String {
46
47 let arg_param = &cli_cmd.arg_param;
48 let fixed_param = &cli_cmd.fixed_param;
49 let properties = &cli_cmd.info.parameters.properties;
50 let description = &cli_cmd.info.parameters.description;
51
52 let mut done_hash = HashSet::<&str>::new();
53 let mut args = String::new();
54
55 for positional_arg in arg_param {
56 match properties.get(positional_arg) {
57 Some((optional, schema)) => {
58 args.push(' ');
59
60 let is_array = if let Schema::Array(_) = schema.as_ref() { true } else { false };
61 if *optional { args.push('['); }
62 if is_array { args.push('{'); }
63 args.push('<'); args.push_str(positional_arg); args.push('>');
64 if is_array { args.push('}'); }
65 if *optional { args.push(']'); }
66
67 done_hash.insert(positional_arg);
68 }
69 None => panic!("no such property '{}' in schema", positional_arg),
70 }
71 }
72
73 let mut arg_descr = String::new();
74 for positional_arg in arg_param {
75 let (_optional, schema) = properties.get(positional_arg).unwrap();
76 let param_descr = get_property_description(
77 positional_arg, &schema, ParameterDisplayStyle::Fixed, format);
78 arg_descr.push_str(&param_descr);
79 }
80
81 let mut options = String::new();
82
83 let mut prop_names: Vec<&str> = properties.keys().map(|v| *v).collect();
84 prop_names.sort();
85
86 for prop in prop_names {
87 let (optional, schema) = properties.get(prop).unwrap();
88 if done_hash.contains(prop) { continue; }
89 if fixed_param.contains_key(&prop) { continue; }
90
91 let type_text = get_schema_type_text(&schema, ParameterDisplayStyle::Arg);
92
93 if *optional {
94
95 if options.len() > 0 { options.push('\n'); }
96 options.push_str(&get_property_description(prop, &schema, ParameterDisplayStyle::Arg, format));
97
98 } else {
99 args.push_str(" --"); args.push_str(prop);
100 args.push(' ');
101 args.push_str(&type_text);
102 }
103
104 done_hash.insert(prop);
105 }
106
107 let option_indicator = if options.len() > 0 { " [OPTIONS]" } else { "" };
108
109 let mut text = match format {
110 DocumentationFormat::Short => {
111 return format!("{}{}{}{}\n\n", indent, prefix, args, option_indicator);
112 }
113 DocumentationFormat::Long => {
114 format!("{}{}{}{}\n\n", indent, prefix, args, option_indicator)
115 }
116 DocumentationFormat::Full => {
117 format!("{}{}{}{}\n\n{}\n\n", indent, prefix, args, option_indicator, description)
118 }
119 DocumentationFormat::ReST => {
120 format!("``{}{}{}``\n\n{}\n\n", prefix, args, option_indicator, description)
121 }
122 };
123
124 if arg_descr.len() > 0 {
125 text.push_str(&arg_descr);
126 text.push('\n');
127 }
128 if options.len() > 0 {
129 text.push_str(&options);
130 text.push('\n');
131 }
132 text
133 }
134
135 fn print_simple_usage_error(prefix: &str, cli_cmd: &CliCommand, err: Error) {
136
137 let usage = generate_usage_str(prefix, cli_cmd, DocumentationFormat::Long, "");
138 eprint!("Error: {}\nUsage: {}", err, usage);
139 }
140
141 fn print_help(
142 top_def: &CommandLineInterface,
143 mut prefix: String,
144 args: &Vec<String>,
145 verbose: Option<bool>,
146 ) {
147 let mut iface = top_def;
148
149 for cmd in args {
150 if let CommandLineInterface::Nested(map) = iface {
151 if let Some(subcmd) = find_command(map, cmd) {
152 iface = subcmd;
153 prefix.push(' ');
154 prefix.push_str(cmd);
155 continue;
156 }
157 }
158 eprintln!("no such command '{}'", cmd);
159 std::process::exit(-1);
160 }
161
162 let format = match verbose.unwrap_or(false) {
163 true => DocumentationFormat::Full,
164 false => DocumentationFormat::Short,
165 };
166
167 match iface {
168 CommandLineInterface::Nested(map) => {
169 println!("Usage:\n\n{}", generate_nested_usage(&prefix, map, format));
170 }
171 CommandLineInterface::Simple(cli_cmd) => {
172 println!("Usage: {}", generate_usage_str(&prefix, cli_cmd, format, ""));
173 }
174 }
175 }
176
177 fn handle_simple_command(
178 top_def: &CommandLineInterface,
179 prefix: &str,
180 cli_cmd: &CliCommand,
181 args: Vec<String>,
182 ) {
183
184 let (params, rest) = match getopts::parse_arguments(
185 &args, &cli_cmd.arg_param, &cli_cmd.info.parameters) {
186 Ok((p, r)) => (p, r),
187 Err(err) => {
188 print_simple_usage_error(prefix, cli_cmd, err.into());
189 std::process::exit(-1);
190 }
191 };
192
193 if cli_cmd.info.handler.is_none() {
194 let prefix = prefix.split(' ').next().unwrap().to_string();
195 print_help(top_def, prefix, &rest, params["verbose"].as_bool());
196 return;
197 }
198
199 if !rest.is_empty() {
200 let err = format_err!("got additional arguments: {:?}", rest);
201 print_simple_usage_error(prefix, cli_cmd, err);
202 std::process::exit(-1);
203 }
204
205 let mut rpcenv = CliEnvironment::new();
206
207 match (cli_cmd.info.handler.as_ref().unwrap())(params, &cli_cmd.info, &mut rpcenv) {
208 Ok(value) => {
209 if value != Value::Null {
210 println!("Result: {}", serde_json::to_string_pretty(&value).unwrap());
211 }
212 }
213 Err(err) => {
214 eprintln!("Error: {}", err);
215 std::process::exit(-1);
216 }
217 }
218 }
219
220 fn find_command<'a>(def: &'a CliCommandMap, name: &str) -> Option<&'a CommandLineInterface> {
221
222 if let Some(sub_cmd) = def.commands.get(name) {
223 return Some(sub_cmd);
224 };
225
226 let mut matches: Vec<&str> = vec![];
227
228 for cmd in def.commands.keys() {
229 if cmd.starts_with(name) {
230 matches.push(cmd); }
231 }
232
233 if matches.len() != 1 { return None; }
234
235 if let Some(sub_cmd) = def.commands.get(matches[0]) {
236 return Some(sub_cmd);
237 };
238
239 None
240 }
241
242 fn print_nested_usage_error(prefix: &str, def: &CliCommandMap, err: Error) {
243
244 let usage = generate_nested_usage(prefix, def, DocumentationFormat::Short);
245
246 eprintln!("Error: {}\n\nUsage:\n\n{}", err, usage);
247 }
248
249 fn generate_nested_usage(prefix: &str, def: &CliCommandMap, format: DocumentationFormat) -> String {
250
251 let mut cmds: Vec<&String> = def.commands.keys().collect();
252 cmds.sort();
253
254 let mut usage = String::new();
255
256 for cmd in cmds {
257 let new_prefix = format!("{} {}", prefix, cmd);
258
259 match def.commands.get(cmd).unwrap() {
260 CommandLineInterface::Simple(cli_cmd) => {
261 if usage.len() > 0 && format == DocumentationFormat::ReST {
262 usage.push_str("----\n\n");
263 }
264 usage.push_str(&generate_usage_str(&new_prefix, cli_cmd, format, ""));
265 }
266 CommandLineInterface::Nested(map) => {
267 usage.push_str(&generate_nested_usage(&new_prefix, map, format));
268 }
269 }
270 }
271
272 usage
273 }
274
275 fn handle_nested_command(
276 top_def: &CommandLineInterface,
277 prefix: &str,
278 def: &CliCommandMap,
279 mut args: Vec<String>,
280 ) {
281
282 if args.len() < 1 {
283 let mut cmds: Vec<&String> = def.commands.keys().collect();
284 cmds.sort();
285
286 let list = cmds.iter().fold(String::new(),|mut s,item| {
287 if !s.is_empty() { s+= ", "; }
288 s += item;
289 s
290 });
291
292 let err = format_err!("no command specified.\nPossible commands: {}", list);
293 print_nested_usage_error(prefix, def, err);
294 std::process::exit(-1);
295 }
296
297 let command = args.remove(0);
298
299 let sub_cmd = match find_command(def, &command) {
300 Some(cmd) => cmd,
301 None => {
302 let err = format_err!("no such command '{}'", command);
303 print_nested_usage_error(prefix, def, err);
304 std::process::exit(-1);
305 }
306 };
307
308 let new_prefix = format!("{} {}", prefix, command);
309
310 match sub_cmd {
311 CommandLineInterface::Simple(cli_cmd) => {
312 handle_simple_command(top_def, &new_prefix, cli_cmd, args);
313 }
314 CommandLineInterface::Nested(map) => {
315 handle_nested_command(top_def, &new_prefix, map, args);
316 }
317 }
318 }
319
320 fn print_property_completion(
321 schema: &Schema,
322 name: &str,
323 completion_functions: &HashMap<String, CompletionFunction>,
324 arg: &str,
325 param: &HashMap<String, String>,
326 ) {
327 if let Some(callback) = completion_functions.get(name) {
328 let list = (callback)(arg, param);
329 for value in list {
330 if value.starts_with(arg) {
331 println!("{}", value);
332 }
333 }
334 return;
335 }
336
337 if let Schema::String(StringSchema { format: Some(format), ..} ) = schema {
338 if let ApiStringFormat::Enum(list) = *format.as_ref() {
339 for value in list {
340 if value.starts_with(arg) {
341 println!("{}", value);
342 }
343 }
344 return;
345 }
346 }
347 println!();
348 }
349
350 fn record_done_argument(done: &mut HashMap<String, String>, parameters: &ObjectSchema, key: &str, value: &str) {
351
352 if let Some((_, schema)) = parameters.properties.get::<str>(key) {
353 match schema.as_ref() {
354 Schema::Array(_) => { /* do nothing ?? */ }
355 _ => { done.insert(key.to_owned(), value.to_owned()); }
356 }
357 }
358 }
359
360 fn print_simple_completion(
361 cli_cmd: &CliCommand,
362 done: &mut HashMap<String, String>,
363 all_arg_param: &[&str], // this is always the full list
364 arg_param: &[&str], // we remove done arguments
365 args: &[String],
366 ) {
367 // fixme: arg_param, fixed_param
368 //eprintln!("COMPL: {:?} {:?} {}", arg_param, args, args.len());
369
370 if !arg_param.is_empty() {
371 let prop_name = arg_param[0];
372 if args.len() > 1 {
373 record_done_argument(done, &cli_cmd.info.parameters, prop_name, &args[0]);
374 print_simple_completion(cli_cmd, done, arg_param, &arg_param[1..], &args[1..]);
375 return;
376 } else if args.len() == 1 {
377 record_done_argument(done, &cli_cmd.info.parameters, prop_name, &args[0]);
378 if let Some((_, schema)) = cli_cmd.info.parameters.properties.get(prop_name) {
379 print_property_completion(schema, prop_name, &cli_cmd.completion_functions, &args[0], done);
380 }
381 }
382 return;
383 }
384 if args.is_empty() { return; }
385
386 // Try to parse all argumnets but last, record args already done
387 if args.len() > 1 {
388 let mut errors = ParameterError::new(); // we simply ignore any parsing errors here
389 let (data, _rest) = getopts::parse_argument_list(&args[0..args.len()-1], &cli_cmd.info.parameters, &mut errors);
390 for (key, value) in &data {
391 record_done_argument(done, &cli_cmd.info.parameters, key, value);
392 }
393 }
394
395 let prefix = &args[args.len()-1]; // match on last arg
396
397 // complete option-name or option-value ?
398 if !prefix.starts_with("-") && args.len() > 1 {
399 let last = &args[args.len()-2];
400 if last.starts_with("--") && last.len() > 2 {
401 let prop_name = &last[2..];
402 if let Some((_, schema)) = cli_cmd.info.parameters.properties.get(prop_name) {
403 print_property_completion(schema, prop_name, &cli_cmd.completion_functions, &prefix, done);
404 }
405 return;
406 }
407 }
408
409 for (name, (_optional, _schema)) in &cli_cmd.info.parameters.properties {
410 if done.contains_key(*name) { continue; }
411 if all_arg_param.contains(name) { continue; }
412 let option = String::from("--") + name;
413 if option.starts_with(prefix) {
414 println!("{}", option);
415 }
416 }
417 }
418
419 fn print_help_completion(def: &CommandLineInterface, help_cmd: &CliCommand, args: &[String]) {
420
421 let mut done = HashMap::new();
422
423 match def {
424 CommandLineInterface::Simple(_) => {
425 print_simple_completion(help_cmd, &mut done, &help_cmd.arg_param, &help_cmd.arg_param, args);
426 }
427 CommandLineInterface::Nested(map) => {
428 if args.is_empty() {
429 for cmd in map.commands.keys() {
430 println!("{}", cmd);
431 }
432 return;
433 }
434
435 let first = &args[0];
436
437 if first.starts_with("-") {
438 print_simple_completion(help_cmd, &mut done, &help_cmd.arg_param, &help_cmd.arg_param, args);
439 return;
440 }
441
442 if let Some(sub_cmd) = map.commands.get(first) {
443 print_help_completion(sub_cmd, help_cmd, &args[1..]);
444 return;
445 }
446
447 for cmd in map.commands.keys() {
448 if cmd.starts_with(first) {
449 println!("{}", cmd);
450 }
451 }
452 }
453 }
454 }
455
456 fn print_nested_completion(def: &CommandLineInterface, args: &[String]) {
457
458 match def {
459 CommandLineInterface::Simple(cli_cmd) => {
460 let mut done: HashMap<String, String> = HashMap::new();
461 cli_cmd.fixed_param.iter().for_each(|(key, value)| {
462 record_done_argument(&mut done, &cli_cmd.info.parameters, &key, &value);
463 });
464 print_simple_completion(cli_cmd, &mut done, &cli_cmd.arg_param, &cli_cmd.arg_param, args);
465 }
466 CommandLineInterface::Nested(map) => {
467 if args.is_empty() {
468 for cmd in map.commands.keys() {
469 println!("{}", cmd);
470 }
471 return;
472 }
473 let first = &args[0];
474 if args.len() > 1 {
475 if let Some(sub_cmd) = map.commands.get(first) {
476 print_nested_completion(sub_cmd, &args[1..]);
477 return;
478 }
479 }
480 for cmd in map.commands.keys() {
481 if cmd.starts_with(first) {
482 println!("{}", cmd);
483 }
484 }
485 }
486 }
487 }
488
489 pub fn print_bash_completion(def: &CommandLineInterface) {
490
491 let comp_point: usize = match std::env::var("COMP_POINT") {
492 Ok(val) => {
493 match usize::from_str_radix(&val, 10) {
494 Ok(i) => i,
495 Err(_) => return,
496 }
497 }
498 Err(_) => return,
499 };
500
501 let cmdline = match std::env::var("COMP_LINE") {
502 Ok(val) => val[0..comp_point].to_owned(),
503 Err(_) => return,
504 };
505
506 let mut args = match shellwords::split(&cmdline) {
507 Ok(v) => v,
508 Err(_) => return,
509 };
510
511 if args.len() == 0 { return; }
512
513 args.remove(0); //no need for program name
514
515 if cmdline.ends_with(char::is_whitespace) {
516 //eprintln!("CMDLINE {:?}", cmdline);
517 args.push("".into());
518 }
519
520 if !args.is_empty() && args[0] == "help" {
521 print_help_completion(def, &help_command_def(), &args[1..]);
522 } else {
523 print_nested_completion(def, &args);
524 }
525 }
526
527 fn help_command_def() -> CliCommand {
528 CliCommand::new(
529 ApiMethod::new_dummy(
530 ObjectSchema::new("Get help about specified command.")
531 .optional("verbose", BooleanSchema::new("Verbose help."))
532 )
533 )
534 }
535
536 pub fn run_cli_command(def: CommandLineInterface) {
537
538 let def = match def {
539 CommandLineInterface::Simple(cli_cmd) => CommandLineInterface::Simple(cli_cmd),
540 CommandLineInterface::Nested(map) =>
541 CommandLineInterface::Nested(map.insert("help", help_command_def().into())),
542 };
543
544 let top_def = &def; // we pass this to the help function ...
545
546 let mut args = std::env::args();
547
548 let prefix = args.next().unwrap();
549 let prefix = prefix.rsplit('/').next().unwrap(); // without path
550
551 let args: Vec<String> = args.collect();
552
553 if !args.is_empty() {
554 if args[0] == "bashcomplete" {
555 print_bash_completion(&def);
556 return;
557 }
558
559 if args[0] == "printdoc" {
560 let usage = match def {
561 CommandLineInterface::Simple(cli_cmd) => {
562 generate_usage_str(&prefix, &cli_cmd, DocumentationFormat::ReST, "")
563 }
564 CommandLineInterface::Nested(map) => {
565 generate_nested_usage(&prefix, &map, DocumentationFormat::ReST)
566 }
567 };
568 println!("{}", usage);
569 return;
570 }
571 }
572
573 match def {
574 CommandLineInterface::Simple(ref cli_cmd) => handle_simple_command(top_def, &prefix, &cli_cmd, args),
575 CommandLineInterface::Nested(ref map) => handle_nested_command(top_def, &prefix, &map, args),
576 };
577 }
578
579 pub type CompletionFunction = fn(&str, &HashMap<String, String>) -> Vec<String>;
580
581 pub struct CliCommand {
582 pub info: ApiMethod,
583 pub arg_param: Vec<&'static str>,
584 pub fixed_param: HashMap<&'static str, String>,
585 pub completion_functions: HashMap<String, CompletionFunction>,
586 }
587
588 impl CliCommand {
589
590 pub fn new(info: ApiMethod) -> Self {
591 Self {
592 info, arg_param: vec![],
593 fixed_param: HashMap::new(),
594 completion_functions: HashMap::new(),
595 }
596 }
597
598 pub fn arg_param(mut self, names: Vec<&'static str>) -> Self {
599 self.arg_param = names;
600 self
601 }
602
603 pub fn fixed_param(mut self, key: &'static str, value: String) -> Self {
604 self.fixed_param.insert(key, value);
605 self
606 }
607
608 pub fn completion_cb(mut self, param_name: &str, cb: CompletionFunction) -> Self {
609 self.completion_functions.insert(param_name.into(), cb);
610 self
611 }
612 }
613
614 pub struct CliCommandMap {
615 pub commands: HashMap<String, CommandLineInterface>,
616 }
617
618 impl CliCommandMap {
619
620 pub fn new() -> Self {
621 Self { commands: HashMap:: new() }
622 }
623
624 pub fn insert<S: Into<String>>(mut self, name: S, cli: CommandLineInterface) -> Self {
625 self.commands.insert(name.into(), cli);
626 self
627 }
628 }
629
630 pub enum CommandLineInterface {
631 Simple(CliCommand),
632 Nested(CliCommandMap),
633 }
634
635 impl From<CliCommand> for CommandLineInterface {
636 fn from(cli_cmd: CliCommand) -> Self {
637 CommandLineInterface::Simple(cli_cmd)
638 }
639 }
640
641 impl From<CliCommandMap> for CommandLineInterface {
642 fn from(list: CliCommandMap) -> Self {
643 CommandLineInterface::Nested(list)
644 }
645 }