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