use cli from proxmox::api
This commit is contained in:
		| @ -1,8 +1,6 @@ | ||||
| use failure::*; | ||||
|  | ||||
| use proxmox::api::*; | ||||
|  | ||||
| use proxmox_backup::cli::*; | ||||
| use proxmox::api::{*, cli::*}; | ||||
|  | ||||
| #[api( | ||||
|     input: { | ||||
|  | ||||
| @ -12,9 +12,9 @@ use proxmox::{sortable, identity}; | ||||
| use proxmox::tools::fs::{file_get_contents, file_get_json, file_set_contents, image_size}; | ||||
| use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment}; | ||||
| use proxmox::api::schema::*; | ||||
| use proxmox::api::cli::*; | ||||
|  | ||||
| use proxmox_backup::tools; | ||||
| use proxmox_backup::cli::*; | ||||
| use proxmox_backup::api2::types::*; | ||||
| use proxmox_backup::client::*; | ||||
| use proxmox_backup::backup::*; | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| extern crate proxmox_backup; | ||||
|  | ||||
| //use proxmox_backup::api2; | ||||
| use proxmox_backup::cli::*; | ||||
| use proxmox::api::cli::*; | ||||
|  | ||||
| fn datastore_commands() -> CommandLineInterface { | ||||
|  | ||||
|  | ||||
| @ -5,9 +5,9 @@ use failure::*; | ||||
| use proxmox::{sortable, identity}; | ||||
| use proxmox::api::{ApiHandler, ApiMethod, RpcEnvironment}; | ||||
| use proxmox::api::schema::*; | ||||
| use proxmox::api::cli::*; | ||||
|  | ||||
| use proxmox_backup::tools; | ||||
| use proxmox_backup::cli::*; | ||||
|  | ||||
| use serde_json::{Value}; | ||||
|  | ||||
|  | ||||
							
								
								
									
										160
									
								
								src/cli.rs
									
									
									
									
									
								
							
							
						
						
									
										160
									
								
								src/cli.rs
									
									
									
									
									
								
							| @ -1,160 +0,0 @@ | ||||
| //! Tools to create command line parsers | ||||
| //! | ||||
| //! This crate provides convenient helpers to create command line | ||||
| //! parsers using Schema definitions. | ||||
| //! | ||||
| //! ## Features | ||||
| //! | ||||
| //! - Use declarative API schema to define the CLI | ||||
| //! - Automatic parameter verification | ||||
| //! - Automatically generate documentation and manual pages | ||||
| //! - Automatically generate bash completion helpers | ||||
| //! - Ability to create interactive commands (using ``rustyline``) | ||||
| //! - Supports complex/nested commands | ||||
|  | ||||
| mod environment; | ||||
| pub use environment::*; | ||||
|  | ||||
| mod shellword; | ||||
| pub use shellword::*; | ||||
|  | ||||
| mod format; | ||||
| pub use format::*; | ||||
|  | ||||
| mod completion; | ||||
| pub use completion::*; | ||||
|  | ||||
| mod getopts; | ||||
| pub use getopts::*; | ||||
|  | ||||
| mod command; | ||||
| pub use command::*; | ||||
|  | ||||
| mod readline; | ||||
| pub use readline::*; | ||||
|  | ||||
| use std::collections::HashMap; | ||||
|  | ||||
| use proxmox::api::ApiMethod; | ||||
|  | ||||
| /// Completion function for single parameters. | ||||
| /// | ||||
| /// Completion functions gets the current parameter value, and should | ||||
| /// return a list of all possible values. | ||||
| pub type CompletionFunction = fn(&str, &HashMap<String, String>) -> Vec<String>; | ||||
|  | ||||
| /// Define a simple CLI command. | ||||
| pub struct CliCommand { | ||||
|     /// The Schema definition. | ||||
|     pub info: &'static ApiMethod, | ||||
|     /// Argument parameter list. | ||||
|     /// | ||||
|     /// Those parameters are expected to be passed as command line | ||||
|     /// arguments in the specified order. All other parameters needs | ||||
|     /// to be specified as ``--option <value>`` pairs. | ||||
|     pub arg_param: &'static [&'static str], | ||||
|     /// Predefined parameters. | ||||
|     pub fixed_param: HashMap<&'static str, String>, | ||||
|     /// Completion functions. | ||||
|     /// | ||||
|     /// Each parameter may have an associated completion function, | ||||
|     /// which is called by the shell completion handler. | ||||
|     pub completion_functions: HashMap<String, CompletionFunction>, | ||||
| } | ||||
|  | ||||
| impl CliCommand { | ||||
|  | ||||
|     /// Create a new instance. | ||||
|     pub fn new(info: &'static ApiMethod) -> Self { | ||||
|         Self { | ||||
|             info, arg_param: &[], | ||||
|             fixed_param: HashMap::new(), | ||||
|             completion_functions: HashMap::new(), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /// Set argument parameter list. | ||||
|     pub fn arg_param(mut self, names: &'static [&'static str]) -> Self { | ||||
|         self.arg_param = names; | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set fixed parameters. | ||||
|     pub fn fixed_param(mut self, key: &'static str, value: String) -> Self { | ||||
|         self.fixed_param.insert(key, value); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Set completion functions. | ||||
|     pub fn completion_cb(mut self, param_name: &str, cb:  CompletionFunction) -> Self { | ||||
|         self.completion_functions.insert(param_name.into(), cb); | ||||
|         self | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Define nested CLI commands. | ||||
| pub struct CliCommandMap { | ||||
|     /// Each command has an unique name. The map associates names with | ||||
|     /// command definitions. | ||||
|     pub commands: HashMap<String, CommandLineInterface>, | ||||
| } | ||||
|  | ||||
| impl CliCommandMap { | ||||
|  | ||||
|     /// Create a new instance. | ||||
|     pub fn new() -> Self { | ||||
|         Self { commands: HashMap:: new() } | ||||
|     } | ||||
|  | ||||
|     /// Insert another command. | ||||
|     pub fn insert<S: Into<String>>(mut self, name: S, cli: CommandLineInterface) -> Self { | ||||
|         self.commands.insert(name.into(), cli); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     /// Insert the help command. | ||||
|     pub fn insert_help(mut self) -> Self { | ||||
|         self.commands.insert(String::from("help"), help_command_def().into()); | ||||
|         self | ||||
|     } | ||||
|  | ||||
|     fn find_command(&self, name: &str) -> Option<(String, &CommandLineInterface)> { | ||||
|  | ||||
|         if let Some(sub_cmd) = self.commands.get(name) { | ||||
|             return Some((name.to_string(), sub_cmd)); | ||||
|         }; | ||||
|  | ||||
|         let mut matches: Vec<&str> = vec![]; | ||||
|  | ||||
|         for cmd in self.commands.keys() { | ||||
|             if cmd.starts_with(name) { | ||||
|                 matches.push(cmd); } | ||||
|         } | ||||
|  | ||||
|         if matches.len() != 1 { return None; } | ||||
|  | ||||
|         if let Some(sub_cmd) = self.commands.get(matches[0]) { | ||||
|             return Some((matches[0].to_string(), sub_cmd)); | ||||
|         }; | ||||
|  | ||||
|         None | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Define Complex command line interfaces. | ||||
| pub enum CommandLineInterface { | ||||
|     Simple(CliCommand), | ||||
|     Nested(CliCommandMap), | ||||
| } | ||||
|  | ||||
| impl From<CliCommand> for CommandLineInterface { | ||||
|     fn from(cli_cmd: CliCommand) -> Self { | ||||
|          CommandLineInterface::Simple(cli_cmd) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<CliCommandMap> for CommandLineInterface { | ||||
|     fn from(list: CliCommandMap) -> Self { | ||||
|         CommandLineInterface::Nested(list) | ||||
|     } | ||||
| } | ||||
| @ -1,260 +0,0 @@ | ||||
| use failure::*; | ||||
| use serde_json::Value; | ||||
| use std::sync::Arc; | ||||
| use std::cell::RefCell; | ||||
|  | ||||
| use proxmox::api::*; | ||||
| use proxmox::api::format::*; | ||||
| use proxmox::api::schema::*; | ||||
| use proxmox::api::{ApiHandler, ApiMethod}; | ||||
|  | ||||
| use super::environment::CliEnvironment; | ||||
|  | ||||
| use super::getopts; | ||||
| use super::{CommandLineInterface, CliCommand, CliCommandMap, completion::*}; | ||||
| use super::format::*; | ||||
|  | ||||
| /// Schema definition for ``--output-format`` parameter. | ||||
| /// | ||||
| /// - ``text``: command specific text format. | ||||
| /// - ``json``: JSON, single line. | ||||
| /// - ``json-pretty``: JSON, human readable. | ||||
| /// | ||||
| pub const OUTPUT_FORMAT: Schema = | ||||
|     StringSchema::new("Output format.") | ||||
|     .format(&ApiStringFormat::Enum(&["text", "json", "json-pretty"])) | ||||
|     .schema(); | ||||
|  | ||||
| fn handle_simple_command( | ||||
|     prefix: &str, | ||||
|     cli_cmd: &CliCommand, | ||||
|     args: Vec<String>, | ||||
| ) -> Result<(), Error> { | ||||
|  | ||||
|     let (params, rest) = match getopts::parse_arguments( | ||||
|         &args, cli_cmd.arg_param, &cli_cmd.info.parameters) { | ||||
|         Ok((p, r)) => (p, r), | ||||
|         Err(err) => { | ||||
|             let err_msg = err.to_string(); | ||||
|             print_simple_usage_error(prefix, cli_cmd, &err_msg); | ||||
|             return Err(format_err!("{}", err_msg)); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if !rest.is_empty() { | ||||
|         let err_msg = format!("got additional arguments: {:?}", rest); | ||||
|         print_simple_usage_error(prefix, cli_cmd, &err_msg); | ||||
|         return Err(format_err!("{}", err_msg)); | ||||
|     } | ||||
|  | ||||
|     let mut rpcenv = CliEnvironment::new(); | ||||
|  | ||||
|     match cli_cmd.info.handler { | ||||
|         ApiHandler::Sync(handler) => { | ||||
|             match (handler)(params, &cli_cmd.info, &mut rpcenv) { | ||||
|                 Ok(value) => { | ||||
|                     if value != Value::Null { | ||||
|                         println!("Result: {}", serde_json::to_string_pretty(&value).unwrap()); | ||||
|                     } | ||||
|                 } | ||||
|                 Err(err) => { | ||||
|                     eprintln!("Error: {}", err); | ||||
|                     return Err(err); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         ApiHandler::AsyncHttp(_) => { | ||||
|             let err_msg = | ||||
|                 "CliHandler does not support ApiHandler::AsyncHttp - internal error"; | ||||
|             print_simple_usage_error(prefix, cli_cmd, err_msg); | ||||
|             return Err(format_err!("{}", err_msg)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| fn handle_nested_command( | ||||
|     prefix: &str, | ||||
|     def: &CliCommandMap, | ||||
|     mut args: Vec<String>, | ||||
| ) -> Result<(), Error> { | ||||
|  | ||||
|     if args.len() < 1 { | ||||
|         let mut cmds: Vec<&String> = def.commands.keys().collect(); | ||||
|         cmds.sort(); | ||||
|  | ||||
|         let list = cmds.iter().fold(String::new(),|mut s,item| { | ||||
|             if !s.is_empty() { s+= ", "; } | ||||
|             s += item; | ||||
|             s | ||||
|         }); | ||||
|  | ||||
|         let err_msg = format!("no command specified.\nPossible commands: {}", list); | ||||
|         print_nested_usage_error(prefix, def, &err_msg); | ||||
|         return Err(format_err!("{}", err_msg)); | ||||
|     } | ||||
|  | ||||
|     let command = args.remove(0); | ||||
|  | ||||
|     let (_, sub_cmd) = match def.find_command(&command) { | ||||
|         Some(cmd) => cmd, | ||||
|         None => { | ||||
|             let err_msg = format!("no such command '{}'", command); | ||||
|             print_nested_usage_error(prefix, def, &err_msg); | ||||
|             return Err(format_err!("{}", err_msg)); | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     let new_prefix = format!("{} {}", prefix, command); | ||||
|  | ||||
|     match sub_cmd { | ||||
|         CommandLineInterface::Simple(cli_cmd) => { | ||||
|             handle_simple_command(&new_prefix, cli_cmd, args)?; | ||||
|         } | ||||
|         CommandLineInterface::Nested(map) => { | ||||
|             handle_nested_command(&new_prefix, map, args)?; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| const API_METHOD_COMMAND_HELP: ApiMethod = ApiMethod::new( | ||||
|     &ApiHandler::Sync(&help_command), | ||||
|     &ObjectSchema::new( | ||||
|         "Get help about specified command (or sub-command).", | ||||
|         &[ | ||||
|             ( "command", | ||||
|                true, | ||||
|                &ArraySchema::new( | ||||
|                    "Command. This may be a list in order to spefify nested sub-commands.", | ||||
|                    &StringSchema::new("Name.").schema() | ||||
|                ).schema() | ||||
|             ), | ||||
|             ( "verbose", | ||||
|                true, | ||||
|                &BooleanSchema::new("Verbose help.").schema() | ||||
|             ), | ||||
|         ], | ||||
|     ) | ||||
| ); | ||||
|  | ||||
| std::thread_local! { | ||||
|     static HELP_CONTEXT: RefCell<Option<Arc<CommandLineInterface>>> = RefCell::new(None); | ||||
| } | ||||
|  | ||||
| fn help_command( | ||||
|     param: Value, | ||||
|     _info: &ApiMethod, | ||||
|     _rpcenv: &mut dyn RpcEnvironment, | ||||
| ) -> Result<Value, Error> { | ||||
|  | ||||
|     let command: Vec<String> = param["command"].as_array().unwrap_or(&Vec::new()) | ||||
|         .iter() | ||||
|         .map(|v| v.as_str().unwrap().to_string()) | ||||
|         .collect(); | ||||
|  | ||||
|  | ||||
|     let verbose = param["verbose"].as_bool(); | ||||
|  | ||||
|     HELP_CONTEXT.with(|ctx| { | ||||
|         match &*ctx.borrow() { | ||||
|             Some(def) => { | ||||
|                  print_help(def, String::from(""), &command, verbose); | ||||
|             } | ||||
|             None => { | ||||
|                 eprintln!("Sorry, help context not set - internal error."); | ||||
|             } | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     Ok(Value::Null) | ||||
| } | ||||
|  | ||||
| fn set_help_context(def: Option<Arc<CommandLineInterface>>) { | ||||
|     HELP_CONTEXT.with(|ctx| { *ctx.borrow_mut() = def; }); | ||||
| } | ||||
|  | ||||
| pub(crate) fn help_command_def() ->  CliCommand { | ||||
|     CliCommand::new(&API_METHOD_COMMAND_HELP) | ||||
|         .arg_param(&["command"]) | ||||
| } | ||||
|  | ||||
| /// Handle command invocation. | ||||
| /// | ||||
| /// This command gets the command line ``args`` and tries to invoke | ||||
| /// the corresponding API handler. | ||||
| pub fn handle_command( | ||||
|     def: Arc<CommandLineInterface>, | ||||
|     prefix: &str, | ||||
|     args: Vec<String>, | ||||
| ) -> Result<(), Error> { | ||||
|  | ||||
|     set_help_context(Some(def.clone())); | ||||
|  | ||||
|     let result = match &*def { | ||||
|         CommandLineInterface::Simple(ref cli_cmd) => { | ||||
|             handle_simple_command(&prefix, &cli_cmd, args) | ||||
|         } | ||||
|         CommandLineInterface::Nested(ref map) => { | ||||
|             handle_nested_command(&prefix, &map, args) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     set_help_context(None); | ||||
|  | ||||
|     result | ||||
| } | ||||
|  | ||||
| /// Helper to get arguments and invoke the command. | ||||
| /// | ||||
| /// This helper reads arguments with ``std::env::args()``. The first | ||||
| /// argument is assumed to be the program name, and is passed as ``prefix`` to | ||||
| /// ``handle_command()``. | ||||
| /// | ||||
| /// This helper automatically add the help command, and two special | ||||
| /// sub-command: | ||||
| /// | ||||
| /// - ``bashcomplete``: Output bash completions instead of running the command. | ||||
| /// - ``printdoc``: Output ReST documentation. | ||||
| /// | ||||
| pub fn run_cli_command(def: CommandLineInterface) { | ||||
|  | ||||
|     let def = match def { | ||||
|         CommandLineInterface::Simple(cli_cmd) => CommandLineInterface::Simple(cli_cmd), | ||||
|         CommandLineInterface::Nested(map) => | ||||
|             CommandLineInterface::Nested(map.insert_help().into()), | ||||
|     }; | ||||
|  | ||||
|     let mut args = std::env::args(); | ||||
|  | ||||
|     let prefix = args.next().unwrap(); | ||||
|     let prefix = prefix.rsplit('/').next().unwrap(); // without path | ||||
|  | ||||
|     let args: Vec<String> = args.collect(); | ||||
|  | ||||
|     if !args.is_empty() { | ||||
|         if args[0] == "bashcomplete" { | ||||
|             print_bash_completion(&def); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if args[0] == "printdoc" { | ||||
|             let usage = match def { | ||||
|                 CommandLineInterface::Simple(cli_cmd) => { | ||||
|                     generate_usage_str(&prefix, &cli_cmd,  DocumentationFormat::ReST, "") | ||||
|                 } | ||||
|                 CommandLineInterface::Nested(map) => { | ||||
|                     generate_nested_usage(&prefix, &map, DocumentationFormat::ReST) | ||||
|                 } | ||||
|             }; | ||||
|             println!("{}", usage); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if let Err(_) = handle_command(Arc::new(def), &prefix, args) { | ||||
|         std::process::exit(-1); | ||||
|     } | ||||
| } | ||||
| @ -1,257 +0,0 @@ | ||||
| use super::*; | ||||
|  | ||||
| use proxmox::api::schema::*; | ||||
|  | ||||
| fn record_done_argument( | ||||
|     done: &mut HashMap<String, String>, | ||||
|     parameters: &ObjectSchema, | ||||
|     key: &str, | ||||
|     value: &str | ||||
| ) { | ||||
|  | ||||
|     if let Some((_, schema)) = parameters.lookup(key) { | ||||
|         match schema { | ||||
|             Schema::Array(_) => { /* do nothing ?? */ } | ||||
|             _ => { done.insert(key.to_owned(), value.to_owned()); } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn get_property_completion( | ||||
|     schema: &Schema, | ||||
|     name: &str, | ||||
|     completion_functions: &HashMap<String, CompletionFunction>, | ||||
|     arg: &str, | ||||
|     param: &HashMap<String, String>, | ||||
| ) -> Vec<String> { | ||||
|  | ||||
|     if let Some(callback) = completion_functions.get(name) { | ||||
|         let list = (callback)(arg, param); | ||||
|         let mut completions = Vec::new(); | ||||
|         for value in list { | ||||
|             if value.starts_with(arg) { | ||||
|                 completions.push(value); | ||||
|             } | ||||
|         } | ||||
|         return completions; | ||||
|     } | ||||
|  | ||||
|     if let Schema::String(StringSchema { format: Some(format),  ..} ) = schema { | ||||
|         if let ApiStringFormat::Enum(list) = format { | ||||
|             let mut completions = Vec::new(); | ||||
|             for value in list.iter() { | ||||
|                 if value.starts_with(arg) { | ||||
|                     completions.push(value.to_string()); | ||||
|                 } | ||||
|             } | ||||
|             return completions; | ||||
|         } | ||||
|     } | ||||
|     return Vec::new(); | ||||
| } | ||||
|  | ||||
| fn get_simple_completion( | ||||
|     cli_cmd: &CliCommand, | ||||
|     done: &mut HashMap<String, String>, | ||||
|     arg_param: &[&str], // we remove done arguments | ||||
|     args: &[String], | ||||
| ) -> Vec<String> { | ||||
|     // fixme: arg_param, fixed_param | ||||
|     //eprintln!("COMPL: {:?} {:?} {}", arg_param, args, args.len()); | ||||
|  | ||||
|     if !arg_param.is_empty() { | ||||
|         let prop_name = arg_param[0]; | ||||
|         if args.len() > 1 { | ||||
|             record_done_argument(done, cli_cmd.info.parameters, prop_name, &args[0]); | ||||
|             return get_simple_completion(cli_cmd, done, &arg_param[1..], &args[1..]); | ||||
|         } else if args.len() == 1 { | ||||
|             record_done_argument(done, cli_cmd.info.parameters, prop_name, &args[0]); | ||||
|             if let Some((_, schema)) = cli_cmd.info.parameters.lookup(prop_name) { | ||||
|                 return get_property_completion(schema, prop_name, &cli_cmd.completion_functions, &args[0], done); | ||||
|             } | ||||
|         } | ||||
|         return Vec::new(); | ||||
|     } | ||||
|     if args.is_empty() { return Vec::new(); } | ||||
|  | ||||
|     // Try to parse all argumnets but last, record args already done | ||||
|     if args.len() > 1 { | ||||
|         let mut errors = ParameterError::new(); // we simply ignore any parsing errors here | ||||
|         let (data, _rest) = getopts::parse_argument_list(&args[0..args.len()-1], &cli_cmd.info.parameters, &mut errors); | ||||
|         for (key, value) in &data { | ||||
|             record_done_argument(done, &cli_cmd.info.parameters, key, value); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let prefix = &args[args.len()-1]; // match on last arg | ||||
|  | ||||
|     // complete option-name or option-value ? | ||||
|     if !prefix.starts_with("-") && args.len() > 1 { | ||||
|         let last = &args[args.len()-2]; | ||||
|         if last.starts_with("--") && last.len() > 2 { | ||||
|             let prop_name = &last[2..]; | ||||
|             if let Some((_, schema)) = cli_cmd.info.parameters.lookup(prop_name) { | ||||
|                 return get_property_completion(schema, prop_name, &cli_cmd.completion_functions, &prefix, done); | ||||
|             } | ||||
|             return Vec::new(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let mut completions = Vec::new(); | ||||
|     for (name, _optional, _schema) in cli_cmd.info.parameters.properties { | ||||
|         if done.contains_key(*name) { continue; } | ||||
|         if cli_cmd.arg_param.contains(name) { continue; } | ||||
|         let option = String::from("--") + name; | ||||
|         if option.starts_with(prefix) { | ||||
|             completions.push(option); | ||||
|         } | ||||
|     } | ||||
|     completions | ||||
| } | ||||
|  | ||||
| fn get_help_completion( | ||||
|     def: &CommandLineInterface, | ||||
|     help_cmd: &CliCommand, | ||||
|     args: &[String], | ||||
| ) -> Vec<String> { | ||||
|  | ||||
|     let mut done = HashMap::new(); | ||||
|  | ||||
|     match def { | ||||
|         CommandLineInterface::Simple(_) => { | ||||
|             return get_simple_completion(help_cmd, &mut done, &[], args); | ||||
|         } | ||||
|         CommandLineInterface::Nested(map) => { | ||||
|             if args.is_empty() { | ||||
|                 let mut completions = Vec::new(); | ||||
|                 for cmd in map.commands.keys() { | ||||
|                     completions.push(cmd.to_string()); | ||||
|                 } | ||||
|                 return completions; | ||||
|             } | ||||
|  | ||||
|             let first = &args[0]; | ||||
|             if args.len() > 1 { | ||||
|                 if let Some(sub_cmd) = map.commands.get(first) { // do exact match here | ||||
|                     return get_help_completion(sub_cmd, help_cmd, &args[1..]); | ||||
|                 } | ||||
|                 return Vec::new(); | ||||
|             } | ||||
|  | ||||
|             if first.starts_with("-") { | ||||
|                 return get_simple_completion(help_cmd, &mut done, &[], args); | ||||
|             } | ||||
|  | ||||
|             let mut completions = Vec::new(); | ||||
|             for cmd in map.commands.keys() { | ||||
|                 if cmd.starts_with(first) { | ||||
|                     completions.push(cmd.to_string()); | ||||
|                 } | ||||
|             } | ||||
|             return completions; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fn get_nested_completion( | ||||
|     def: &CommandLineInterface, | ||||
|     args: &[String], | ||||
| ) -> Vec<String> { | ||||
|  | ||||
|     match def { | ||||
|         CommandLineInterface::Simple(cli_cmd) => { | ||||
|             let mut done: HashMap<String, String> = HashMap::new(); | ||||
|             cli_cmd.fixed_param.iter().for_each(|(key, value)| { | ||||
|                 record_done_argument(&mut done, &cli_cmd.info.parameters, &key, &value); | ||||
|             }); | ||||
|             return get_simple_completion(cli_cmd, &mut done, &cli_cmd.arg_param, args); | ||||
|         } | ||||
|         CommandLineInterface::Nested(map) => { | ||||
|             if args.is_empty() { | ||||
|                 let mut completions = Vec::new(); | ||||
|                 for cmd in map.commands.keys() { | ||||
|                     completions.push(cmd.to_string()); | ||||
|                 } | ||||
|                 return completions; | ||||
|             } | ||||
|             let first = &args[0]; | ||||
|             if args.len() > 1 { | ||||
|                 if let Some((_, sub_cmd)) = map.find_command(first) { | ||||
|                     return get_nested_completion(sub_cmd, &args[1..]); | ||||
|                 } | ||||
|                 return Vec::new(); | ||||
|             } | ||||
|             let mut completions = Vec::new(); | ||||
|             for cmd in map.commands.keys() { | ||||
|                 if cmd.starts_with(first) { | ||||
|                     completions.push(cmd.to_string()); | ||||
|                 } | ||||
|             } | ||||
|             return completions; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Helper to generate bash completions. | ||||
| /// | ||||
| /// This helper extracts the command line from environment variable | ||||
| /// set by ``bash``, namely ``COMP_LINE`` and ``COMP_POINT``. This is | ||||
| /// passed to ``get_completions()``. Returned values are printed to | ||||
| /// ``stdout``. | ||||
| pub fn print_bash_completion(def: &CommandLineInterface) { | ||||
|  | ||||
|     let comp_point: usize = match std::env::var("COMP_POINT") { | ||||
|         Ok(val) => { | ||||
|             match usize::from_str_radix(&val, 10) { | ||||
|                 Ok(i) => i, | ||||
|                 Err(_) => return, | ||||
|             } | ||||
|         } | ||||
|         Err(_) => return, | ||||
|     }; | ||||
|  | ||||
|     let cmdline = match std::env::var("COMP_LINE") { | ||||
|         Ok(val) => val[0..comp_point].to_owned(), | ||||
|         Err(_) => return, | ||||
|     }; | ||||
|  | ||||
|     let (_start, completions) = super::get_completions(def, &cmdline, true); | ||||
|  | ||||
|     for item in completions { | ||||
|         println!("{}", item); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Compute possible completions for a partial command | ||||
| pub fn get_completions( | ||||
|     cmd_def: &CommandLineInterface, | ||||
|     line: &str, | ||||
|     skip_first: bool, | ||||
| ) -> (usize, Vec<String>) { | ||||
|  | ||||
|     let (mut args, start ) = match shellword_split_unclosed(line, false) { | ||||
|         (mut args, None) => { | ||||
|             args.push("".into()); | ||||
|             (args, line.len()) | ||||
|         } | ||||
|         (mut args, Some((start , arg, _quote))) => { | ||||
|             args.push(arg); | ||||
|             (args, start) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if skip_first { | ||||
|  | ||||
|         if args.len() == 0 { return (0, Vec::new()); } | ||||
|  | ||||
|         args.remove(0); // no need for program name | ||||
|     } | ||||
|  | ||||
|     let completions = if !args.is_empty() && args[0] == "help" { | ||||
|         get_help_completion(cmd_def, &help_command_def(), &args[1..]) | ||||
|     } else { | ||||
|         get_nested_completion(cmd_def, &args) | ||||
|     }; | ||||
|  | ||||
|     (start, completions) | ||||
| } | ||||
| @ -1,42 +0,0 @@ | ||||
| use std::collections::HashMap; | ||||
| use serde_json::Value; | ||||
|  | ||||
| use proxmox::api::{RpcEnvironment, RpcEnvironmentType}; | ||||
|  | ||||
| /// `RpcEnvironmet` implementation for command line tools | ||||
| pub struct CliEnvironment { | ||||
|     result_attributes: HashMap<String, Value>, | ||||
|     user: Option<String>, | ||||
| } | ||||
|  | ||||
| impl CliEnvironment { | ||||
|     pub fn new() -> Self { | ||||
|         Self { | ||||
|             result_attributes: HashMap::new(), | ||||
|             user: None, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl RpcEnvironment for CliEnvironment { | ||||
|  | ||||
|     fn set_result_attrib(&mut self, name: &str, value: Value) { | ||||
|         self.result_attributes.insert(name.into(), value); | ||||
|     } | ||||
|  | ||||
|     fn get_result_attrib(&self, name: &str) -> Option<&Value> { | ||||
|         self.result_attributes.get(name) | ||||
|     } | ||||
|  | ||||
|     fn env_type(&self) -> RpcEnvironmentType { | ||||
|         RpcEnvironmentType::CLI | ||||
|     } | ||||
|  | ||||
|     fn set_user(&mut self, user: Option<String>) { | ||||
|         self.user = user; | ||||
|     } | ||||
|  | ||||
|     fn get_user(&self) -> Option<String> { | ||||
|         self.user.clone() | ||||
|     } | ||||
| } | ||||
| @ -1,209 +0,0 @@ | ||||
| use serde_json::Value; | ||||
|  | ||||
| use std::collections::HashSet; | ||||
|  | ||||
| use proxmox::api::schema::*; | ||||
| use proxmox::api::format::*; | ||||
|  | ||||
| use super::{CommandLineInterface, CliCommand, CliCommandMap}; | ||||
|  | ||||
| /// Helper function to format and print result. | ||||
| /// | ||||
| /// This is implemented for machine generatable formats 'json' and | ||||
| /// 'json-pretty'. The 'text' format needs to be handled somewhere | ||||
| /// else. | ||||
| pub fn format_and_print_result( | ||||
|     result: &Value, | ||||
|     output_format: &str, | ||||
| ) { | ||||
|  | ||||
|     if output_format == "json-pretty" { | ||||
|         println!("{}", serde_json::to_string_pretty(&result).unwrap()); | ||||
|     } else if output_format == "json" { | ||||
|         println!("{}", serde_json::to_string(&result).unwrap()); | ||||
|     } else { | ||||
|         unimplemented!(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Helper to generate command usage text for simple commands. | ||||
| pub fn generate_usage_str( | ||||
|     prefix: &str, | ||||
|     cli_cmd: &CliCommand, | ||||
|     format: DocumentationFormat, | ||||
|     indent: &str) -> String { | ||||
|  | ||||
|     let arg_param = cli_cmd.arg_param; | ||||
|     let fixed_param = &cli_cmd.fixed_param; | ||||
|     let schema = cli_cmd.info.parameters; | ||||
|  | ||||
|     let mut done_hash = HashSet::<&str>::new(); | ||||
|     let mut args = String::new(); | ||||
|  | ||||
|     for positional_arg in arg_param { | ||||
|         match schema.lookup(positional_arg) { | ||||
|             Some((optional, param_schema)) => { | ||||
|                 args.push(' '); | ||||
|  | ||||
|                 let is_array = if let Schema::Array(_) = param_schema { true } else { false }; | ||||
|                 if optional { args.push('['); } | ||||
|                 if is_array { args.push('{'); } | ||||
|                 args.push('<'); args.push_str(positional_arg); args.push('>'); | ||||
|                 if is_array { args.push('}'); } | ||||
|                 if optional { args.push(']'); } | ||||
|  | ||||
|                 done_hash.insert(positional_arg); | ||||
|             } | ||||
|             None => panic!("no such property '{}' in schema", positional_arg), | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let mut arg_descr = String::new(); | ||||
|     for positional_arg in arg_param { | ||||
|         let (_optional, param_schema) = schema.lookup(positional_arg).unwrap(); | ||||
|         let param_descr = get_property_description( | ||||
|             positional_arg, param_schema, ParameterDisplayStyle::Fixed, format); | ||||
|         arg_descr.push_str(¶m_descr); | ||||
|     } | ||||
|  | ||||
|     let mut options = String::new(); | ||||
|  | ||||
|     for (prop, optional, param_schema) in schema.properties { | ||||
|         if done_hash.contains(prop) { continue; } | ||||
|         if fixed_param.contains_key(prop) { continue; } | ||||
|  | ||||
|         let type_text = get_schema_type_text(param_schema, ParameterDisplayStyle::Arg); | ||||
|  | ||||
|         if *optional { | ||||
|  | ||||
|             if options.len() > 0 { options.push('\n'); } | ||||
|             options.push_str(&get_property_description(prop, param_schema, ParameterDisplayStyle::Arg, format)); | ||||
|  | ||||
|         } else { | ||||
|             args.push_str(" --"); args.push_str(prop); | ||||
|             args.push(' '); | ||||
|             args.push_str(&type_text); | ||||
|         } | ||||
|  | ||||
|         done_hash.insert(prop); | ||||
|     } | ||||
|  | ||||
|     let option_indicator = if options.len() > 0 { " [OPTIONS]" } else { "" }; | ||||
|  | ||||
|     let mut text = match format { | ||||
|         DocumentationFormat::Short => { | ||||
|             return format!("{}{}{}{}\n\n", indent, prefix, args, option_indicator); | ||||
|         } | ||||
|         DocumentationFormat::Long => { | ||||
|             format!("{}{}{}{}\n\n", indent, prefix, args, option_indicator) | ||||
|         } | ||||
|         DocumentationFormat::Full => { | ||||
|             format!("{}{}{}{}\n\n{}\n\n", indent, prefix, args, option_indicator, schema.description) | ||||
|         } | ||||
|         DocumentationFormat::ReST => { | ||||
|             format!("``{}{}{}``\n\n{}\n\n", prefix, args, option_indicator, schema.description) | ||||
|         } | ||||
|     }; | ||||
|  | ||||
|     if arg_descr.len() > 0 { | ||||
|         text.push_str(&arg_descr); | ||||
|         text.push('\n'); | ||||
|     } | ||||
|     if options.len() > 0 { | ||||
|         text.push_str(&options); | ||||
|         text.push('\n'); | ||||
|     } | ||||
|     text | ||||
| } | ||||
|  | ||||
| /// Print command usage for simple commands to ``stderr``. | ||||
| pub fn print_simple_usage_error( | ||||
|     prefix: &str, | ||||
|     cli_cmd: &CliCommand, | ||||
|     err_msg: &str, | ||||
| ) { | ||||
|     let usage =  generate_usage_str(prefix, cli_cmd, DocumentationFormat::Long, ""); | ||||
|     eprint!("Error: {}\nUsage: {}", err_msg, usage); | ||||
| } | ||||
|  | ||||
| /// Print command usage for nested commands to ``stderr``. | ||||
| pub fn print_nested_usage_error( | ||||
|     prefix: &str, | ||||
|     def: &CliCommandMap, | ||||
|     err_msg: &str, | ||||
| ) { | ||||
|     let usage = generate_nested_usage(prefix, def, DocumentationFormat::Short); | ||||
|     eprintln!("Error: {}\n\nUsage:\n\n{}", err_msg, usage); | ||||
| } | ||||
|  | ||||
| /// Helper to generate command usage text for nested commands. | ||||
| pub fn generate_nested_usage( | ||||
|     prefix: &str, | ||||
|     def: &CliCommandMap, | ||||
|     format: DocumentationFormat | ||||
| ) -> String { | ||||
|  | ||||
|     let mut cmds: Vec<&String> = def.commands.keys().collect(); | ||||
|     cmds.sort(); | ||||
|  | ||||
|     let mut usage = String::new(); | ||||
|  | ||||
|     for cmd in cmds { | ||||
|         let new_prefix = format!("{} {}", prefix, cmd); | ||||
|  | ||||
|         match def.commands.get(cmd).unwrap() { | ||||
|             CommandLineInterface::Simple(cli_cmd) => { | ||||
|                 if usage.len() > 0 && format == DocumentationFormat::ReST { | ||||
|                     usage.push_str("----\n\n"); | ||||
|                 } | ||||
|                 usage.push_str(&generate_usage_str(&new_prefix, cli_cmd, format, "")); | ||||
|             } | ||||
|             CommandLineInterface::Nested(map) => { | ||||
|                 usage.push_str(&generate_nested_usage(&new_prefix, map, format)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     usage | ||||
| } | ||||
|  | ||||
| /// Print help text to ``stderr``. | ||||
| pub fn print_help( | ||||
|     top_def: &CommandLineInterface, | ||||
|     mut prefix: String, | ||||
|     args: &Vec<String>, | ||||
|     verbose: Option<bool>, | ||||
| ) { | ||||
|     let mut iface = top_def; | ||||
|  | ||||
|     for cmd in args { | ||||
|         if let CommandLineInterface::Nested(map) = iface { | ||||
|             if let Some((full_name, subcmd)) = map.find_command(cmd) { | ||||
|                 iface = subcmd; | ||||
|                 if !prefix.is_empty() { prefix.push(' '); } | ||||
|                 prefix.push_str(&full_name); | ||||
|                 continue; | ||||
|             } | ||||
|         } | ||||
|         if prefix.is_empty() { | ||||
|             eprintln!("no such command '{}'", cmd); | ||||
|         } else { | ||||
|             eprintln!("no such command '{} {}'", prefix, cmd); | ||||
|         } | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     let format = match verbose.unwrap_or(false) { | ||||
|         true => DocumentationFormat::Full, | ||||
|         false => DocumentationFormat::Short, | ||||
|     }; | ||||
|  | ||||
|     match iface { | ||||
|         CommandLineInterface::Nested(map) => { | ||||
|             println!("Usage:\n\n{}", generate_nested_usage(&prefix, map, format)); | ||||
|         } | ||||
|         CommandLineInterface::Simple(cli_cmd) => { | ||||
|             println!("Usage: {}", generate_usage_str(&prefix, cli_cmd, format, "")); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,255 +0,0 @@ | ||||
| use failure::*; | ||||
| use serde_json::Value; | ||||
|  | ||||
| use proxmox::api::schema::*; | ||||
|  | ||||
| #[derive(Debug)] | ||||
| enum RawArgument { | ||||
|     Separator, | ||||
|     Argument { value: String }, | ||||
|     Option { name: String, value: Option<String> }, | ||||
| } | ||||
|  | ||||
| fn parse_argument(arg: &str) -> RawArgument { | ||||
|     let bytes = arg.as_bytes(); | ||||
|  | ||||
|     let length = bytes.len(); | ||||
|  | ||||
|     if length < 2 || bytes[0] != b'-' { | ||||
|         return RawArgument::Argument { | ||||
|             value: arg.to_string(), | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     let mut first = 1; | ||||
|  | ||||
|     if bytes[1] == b'-' { | ||||
|         if length == 2 { | ||||
|             return RawArgument::Separator; | ||||
|         } | ||||
|         first = 2; | ||||
|     } | ||||
|  | ||||
|     for start in first..length { | ||||
|         if bytes[start] == b'=' { | ||||
|             // Since we take a &str, we know the contents of it are valid utf8. | ||||
|             // Since bytes[start] == b'=', we know the byte beginning at start is a single-byte | ||||
|             // code pointer. We also know that 'first' points exactly after a single-byte code | ||||
|             // point as it points to the first byte after a hyphen. | ||||
|             // Therefore we know arg[first..start] is valid utf-8, therefore it is safe to use | ||||
|             // get_unchecked() to speed things up. | ||||
|             return RawArgument::Option { | ||||
|                 name: unsafe { arg.get_unchecked(first..start).to_string() }, | ||||
|                 value: Some(unsafe { arg.get_unchecked((start + 1)..).to_string() }), | ||||
|             }; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     RawArgument::Option { | ||||
|         name: unsafe { arg.get_unchecked(first..).to_string() }, | ||||
|         value: None, | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// parse as many arguments as possible into a Vec<String, String>. This does not | ||||
| /// verify the schema. | ||||
| /// Returns parsed data and the rest as separate array | ||||
| pub (crate) fn parse_argument_list<T: AsRef<str>>( | ||||
|     args: &[T], | ||||
|     schema: &ObjectSchema, | ||||
|     errors: &mut ParameterError, | ||||
| ) -> (Vec<(String, String)>, Vec<String>) { | ||||
|  | ||||
|     let mut data: Vec<(String, String)> = vec![]; | ||||
|     let mut rest: Vec<String> = vec![]; | ||||
|  | ||||
|     let mut pos = 0; | ||||
|  | ||||
|     while pos < args.len() { | ||||
|         match parse_argument(args[pos].as_ref()) { | ||||
|             RawArgument::Separator => { | ||||
|                 break; | ||||
|             } | ||||
|             RawArgument::Option { name, value } => match value { | ||||
|                 None => { | ||||
|                     let mut want_bool = false; | ||||
|                     let mut can_default = false; | ||||
|                     if let Some((_optional, param_schema)) = schema.lookup(&name) { | ||||
|                         if let Schema::Boolean(boolean_schema) = param_schema { | ||||
|                             want_bool = true; | ||||
|                             if let Some(default) = boolean_schema.default { | ||||
|                                 if default == false { | ||||
|                                     can_default = true; | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 can_default = true; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     let mut next_is_argument = false; | ||||
|                     let mut next_is_bool = false; | ||||
|  | ||||
|                     if (pos + 1) < args.len() { | ||||
|                         let next = args[pos + 1].as_ref(); | ||||
|                         if let RawArgument::Argument { .. } = parse_argument(next) { | ||||
|                             next_is_argument = true; | ||||
|                             if let Ok(_) = parse_boolean(next) { | ||||
|                                 next_is_bool = true; | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     if want_bool { | ||||
|                         if next_is_bool { | ||||
|                             pos += 1; | ||||
|                             data.push((name, args[pos].as_ref().to_string())); | ||||
|                         } else if can_default { | ||||
|                             data.push((name, "true".to_string())); | ||||
|                         } else { | ||||
|                             errors.push(format_err!("parameter '{}': {}", name, | ||||
|                                                     "missing boolean value.")); | ||||
|                         } | ||||
|  | ||||
|                     } else if next_is_argument { | ||||
|                         pos += 1; | ||||
|                         data.push((name, args[pos].as_ref().to_string())); | ||||
|                     } else { | ||||
|                         errors.push(format_err!("parameter '{}': {}", name, | ||||
|                                                 "missing parameter value.")); | ||||
|                     } | ||||
|                 } | ||||
|                 Some(v) => { | ||||
|                     data.push((name, v)); | ||||
|                 } | ||||
|             }, | ||||
|             RawArgument::Argument { value } => { | ||||
|                 rest.push(value); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         pos += 1; | ||||
|     } | ||||
|  | ||||
|     rest.reserve(args.len() - pos); | ||||
|     for i in &args[pos..] { | ||||
|         rest.push(i.as_ref().to_string()); | ||||
|     } | ||||
|  | ||||
|     (data, rest) | ||||
| } | ||||
|  | ||||
| /// Parses command line arguments using a `Schema` | ||||
| /// | ||||
| /// Returns parsed options as json object, together with the | ||||
| /// list of additional command line arguments. | ||||
| pub fn parse_arguments<T: AsRef<str>>( | ||||
|     args: &[T], | ||||
|     arg_param: &[&str], | ||||
|     schema: &ObjectSchema, | ||||
| ) -> Result<(Value, Vec<String>), ParameterError> { | ||||
|     let mut errors = ParameterError::new(); | ||||
|  | ||||
|     // first check if all arg_param exists in schema | ||||
|  | ||||
|     let mut last_arg_param_is_optional = false; | ||||
|     let mut last_arg_param_is_array = false; | ||||
|  | ||||
|     for i in 0..arg_param.len() { | ||||
|         let name = arg_param[i]; | ||||
|         if let Some((optional, param_schema)) = schema.lookup(&name) { | ||||
|             if i == arg_param.len() -1 { | ||||
|                 last_arg_param_is_optional = optional; | ||||
|                 if let Schema::Array(_) = param_schema { | ||||
|                     last_arg_param_is_array = true; | ||||
|                 } | ||||
|             } else if optional { | ||||
|                 panic!("positional argument '{}' may not be optional", name); | ||||
|             } | ||||
|         } else { | ||||
|             panic!("no such property '{}' in schema", name); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     let (mut data, mut rest) = parse_argument_list(args, schema, &mut errors); | ||||
|  | ||||
|     for i in 0..arg_param.len() { | ||||
|  | ||||
|         let name = arg_param[i]; | ||||
|         let is_last_arg_param = i == (arg_param.len() - 1); | ||||
|  | ||||
|         if rest.len() == 0 { | ||||
|             if !(is_last_arg_param && last_arg_param_is_optional) { | ||||
|                 errors.push(format_err!("missing argument '{}'", name)); | ||||
|             } | ||||
|         } else if is_last_arg_param && last_arg_param_is_array { | ||||
|             for value in rest { | ||||
|                 data.push((name.to_string(), value)); | ||||
|             } | ||||
|             rest = vec![]; | ||||
|         } else { | ||||
|             data.push((name.to_string(), rest.remove(0))); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if errors.len() > 0 { | ||||
|         return Err(errors); | ||||
|     } | ||||
|  | ||||
|     let options = parse_parameter_strings(&data, schema, true)?; | ||||
|  | ||||
|     Ok((options, rest)) | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_boolean_arg() { | ||||
|  | ||||
|     const PARAMETERS: ObjectSchema = ObjectSchema::new( | ||||
|         "Parameters:", | ||||
|         &[ ("enable", false, &BooleanSchema::new("Enable").schema()) ], | ||||
|     ); | ||||
|  | ||||
|     let mut variants: Vec<(Vec<&str>, bool)> = vec![]; | ||||
|     variants.push((vec!["-enable"], true)); | ||||
|     variants.push((vec!["-enable=1"], true)); | ||||
|     variants.push((vec!["-enable", "yes"], true)); | ||||
|     variants.push((vec!["-enable", "Yes"], true)); | ||||
|     variants.push((vec!["--enable", "1"], true)); | ||||
|     variants.push((vec!["--enable", "ON"], true)); | ||||
|     variants.push((vec!["--enable", "true"], true)); | ||||
|  | ||||
|     variants.push((vec!["--enable", "0"], false)); | ||||
|     variants.push((vec!["--enable", "no"], false)); | ||||
|     variants.push((vec!["--enable", "off"], false)); | ||||
|     variants.push((vec!["--enable", "false"], false)); | ||||
|  | ||||
|     for (args, expect) in variants { | ||||
|         let res = parse_arguments(&args, &vec![], &PARAMETERS); | ||||
|         assert!(res.is_ok()); | ||||
|         if let Ok((options, rest)) = res { | ||||
|             assert!(options["enable"] == expect); | ||||
|             assert!(rest.len() == 0); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_argument_paramenter() { | ||||
|  | ||||
|     const PARAMETERS: ObjectSchema = ObjectSchema::new( | ||||
|         "Parameters:", | ||||
|         &[ | ||||
|             ("enable", false, &BooleanSchema::new("Enable.").schema()), | ||||
|             ("storage", false, &StringSchema::new("Storage.").schema()), | ||||
|         ], | ||||
|     ); | ||||
|  | ||||
|     let args = vec!["-enable", "local"]; | ||||
|     let res = parse_arguments(&args, &vec!["storage"], &PARAMETERS); | ||||
|     assert!(res.is_ok()); | ||||
|     if let Ok((options, rest)) = res { | ||||
|         assert!(options["enable"] == true); | ||||
|         assert!(options["storage"] == "local"); | ||||
|         assert!(rest.len() == 0); | ||||
|     } | ||||
| } | ||||
| @ -1,45 +0,0 @@ | ||||
| use std::sync::Arc; | ||||
|  | ||||
| use super::*; | ||||
|  | ||||
| /// Helper trait implementation for ``rustyline``. | ||||
| /// | ||||
| /// This can be used to generate interactive commands using | ||||
| /// ``rustyline`` (readline implementation). | ||||
| /// | ||||
| pub struct CliHelper { | ||||
|     cmd_def: Arc<CommandLineInterface>, | ||||
| } | ||||
|  | ||||
| impl CliHelper { | ||||
|  | ||||
|     pub fn new(cmd_def: CommandLineInterface) -> Self { | ||||
|         Self { cmd_def: Arc::new(cmd_def) } | ||||
|     } | ||||
|  | ||||
|     pub fn cmd_def(&self) -> Arc<CommandLineInterface> { | ||||
|         self.cmd_def.clone() | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl rustyline::completion::Completer for CliHelper { | ||||
|     type Candidate = String; | ||||
|  | ||||
|     fn complete( | ||||
|         &self, | ||||
|         line: &str, | ||||
|         pos: usize, | ||||
|         _ctx: &rustyline::Context<'_>, | ||||
|     ) -> rustyline::Result<(usize, Vec<Self::Candidate>)> { | ||||
|  | ||||
|         let line = &line[..pos]; | ||||
|  | ||||
|         let (start, completions) = super::get_completions(&*self.cmd_def, line, false); | ||||
|  | ||||
|         return Ok((start, completions)); | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl rustyline::hint::Hinter for CliHelper {} | ||||
| impl rustyline::highlight::Highlighter for CliHelper {} | ||||
| impl rustyline::Helper for CliHelper {} | ||||
| @ -1,160 +0,0 @@ | ||||
| use failure::*; | ||||
| use rustyline::completion::Quote; | ||||
|  | ||||
| #[derive(PartialEq)] | ||||
| enum ParseMode { | ||||
|     Space, | ||||
|     DoubleQuote, | ||||
|     EscapeNormal, | ||||
|     EscapeInDoubleQuote, | ||||
|     Normal, | ||||
|     SingleQuote, | ||||
| } | ||||
|  | ||||
| /// Parsing strings as they would be interpreted by the UNIX Bourne shell. | ||||
| /// | ||||
| /// - ``finalize``: assume this is a complete command line. Set this | ||||
| ///   to false for the 'completion' helper, which needs to get | ||||
| ///   information about the last unfinished parameter. | ||||
| /// | ||||
| /// Returns the list of fully parsed words (unescaped and quotes | ||||
| /// removed). If there are unclosed quotes, the start of that | ||||
| /// parameter, the parameter value (unescaped and quotes removed), and | ||||
| /// the quote type are returned. | ||||
| pub fn shellword_split_unclosed(s: &str, finalize: bool) -> (Vec<String>, Option<(usize, String, Quote)>) { | ||||
|  | ||||
|     let char_indices = s.char_indices(); | ||||
|     let mut args: Vec<String> = Vec::new(); | ||||
|     let mut field_start = None; | ||||
|     let mut field = String::new(); | ||||
|     let mut mode = ParseMode::Space; | ||||
|  | ||||
|     let space_chars = [' ', '\t', '\n']; | ||||
|  | ||||
|     for (index, c) in char_indices { | ||||
|         match mode { | ||||
|             ParseMode::Space => match c { | ||||
|                 '"' => { | ||||
|                     mode = ParseMode::DoubleQuote; | ||||
|                     field_start = Some((index, Quote::Double)); | ||||
|                 } | ||||
|                 '\\' => { | ||||
|                     mode = ParseMode::EscapeNormal; | ||||
|                     field_start = Some((index, Quote::None)); | ||||
|                 } | ||||
|                 '\'' => { | ||||
|                     mode = ParseMode::SingleQuote; | ||||
|                     field_start = Some((index, Quote::Single)); | ||||
|                 } | ||||
|                 c if space_chars.contains(&c) => (), // skip space | ||||
|                 c => { | ||||
|                     mode = ParseMode::Normal; | ||||
|                     field_start = Some((index, Quote::None)); | ||||
|                     field.push(c); | ||||
|                 } | ||||
|             } | ||||
|             ParseMode::EscapeNormal => { | ||||
|                 mode = ParseMode::Normal; | ||||
|                 field.push(c); | ||||
|             } | ||||
|             ParseMode::EscapeInDoubleQuote => { | ||||
|                 // Within double quoted strings, backslashes are only | ||||
|                 // treated as metacharacters when followed by one of | ||||
|                 // the following characters: $ ' " \ newline | ||||
|                 match c { | ||||
|                     '$' | '\'' | '"' | '\\' | '\n' => (), | ||||
|                     _ => field.push('\\'), | ||||
|                 } | ||||
|                 field.push(c); | ||||
|                 mode = ParseMode::DoubleQuote; | ||||
|             } | ||||
|             ParseMode::Normal => match c { | ||||
|                 '"' => mode = ParseMode::DoubleQuote, | ||||
|                 '\'' => mode = ParseMode::SingleQuote, | ||||
|                 '\\' => mode = ParseMode::EscapeNormal, | ||||
|                 c if space_chars.contains(&c) => { | ||||
|                     mode = ParseMode::Space; | ||||
|                     let (_start, _quote) = field_start.take().unwrap(); | ||||
|                     args.push(field.split_off(0)); | ||||
|                 } | ||||
|                 c => field.push(c), // continue | ||||
|             } | ||||
|             ParseMode::DoubleQuote => match c { | ||||
|                 '"' => mode = ParseMode::Normal, | ||||
|                 '\\' => mode = ParseMode::EscapeInDoubleQuote, | ||||
|                 c => field.push(c), // continue | ||||
|             } | ||||
|             ParseMode::SingleQuote => match c { | ||||
|                 // Note: no escape in single quotes | ||||
|                 '\'' => mode = ParseMode::Normal, | ||||
|                 c => field.push(c), // continue | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if finalize && mode == ParseMode::Normal { | ||||
|         let (_start, _quote) = field_start.take().unwrap(); | ||||
|         args.push(field.split_off(0)); | ||||
|     } | ||||
|  | ||||
|     match field_start { | ||||
|         Some ((start, quote)) => { | ||||
|             (args, Some((start, field, quote))) | ||||
|         } | ||||
|         None => { | ||||
|             (args, None) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Splits a string into a vector of words in the same way the UNIX Bourne shell does. | ||||
| /// | ||||
| /// Return words unescaped and without quotes. | ||||
| pub fn shellword_split(s: &str) -> Result<Vec<String>, Error> { | ||||
|  | ||||
|     let (args, unclosed_field) = shellword_split_unclosed(s, true); | ||||
|     if !unclosed_field.is_none() { | ||||
|         bail!("shellword split failed - found unclosed quote."); | ||||
|     } | ||||
|     Ok(args) | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_shellword_split() { | ||||
|  | ||||
|     let expect = [ "ls", "/etc" ]; | ||||
|     let expect: Vec<String> = expect.iter().map(|v| v.to_string()).collect(); | ||||
|  | ||||
|     assert_eq!(expect, shellword_split("ls /etc").unwrap()); | ||||
|     assert_eq!(expect, shellword_split("ls \"/etc\"").unwrap()); | ||||
|     assert_eq!(expect, shellword_split("ls '/etc'").unwrap()); | ||||
|     assert_eq!(expect, shellword_split("ls '/etc'").unwrap()); | ||||
|  | ||||
|     assert_eq!(expect, shellword_split("ls /e\"t\"c").unwrap()); | ||||
|     assert_eq!(expect, shellword_split("ls /e'tc'").unwrap()); | ||||
|     assert_eq!(expect, shellword_split("ls /e't''c'").unwrap()); | ||||
|  | ||||
|     let expect = [ "ls", "/etc 08x" ]; | ||||
|     let expect: Vec<String> = expect.iter().map(|v| v.to_string()).collect(); | ||||
|     assert_eq!(expect, shellword_split("ls /etc\\ \\08x").unwrap()); | ||||
|  | ||||
|     let expect = [ "ls", "/etc \\08x" ]; | ||||
|     let expect: Vec<String> = expect.iter().map(|v| v.to_string()).collect(); | ||||
|     assert_eq!(expect, shellword_split("ls \"/etc \\08x\"").unwrap()); | ||||
| } | ||||
|  | ||||
| #[test] | ||||
| fn test_shellword_split_unclosed() { | ||||
|  | ||||
|     let expect = [ "ls".to_string() ].to_vec(); | ||||
|     assert_eq!( | ||||
|         (expect, Some((3, "./File1 name with spaces".to_string(), Quote::Single))), | ||||
|         shellword_split_unclosed("ls './File1 name with spaces", false) | ||||
|     ); | ||||
|  | ||||
|     let expect = [ "ls".to_string() ].to_vec(); | ||||
|     assert_eq!( | ||||
|         (expect, Some((3, "./File2 name with spaces".to_string(), Quote::Double))), | ||||
|         shellword_split_unclosed("ls \"./File2 \"name\" with spaces", false) | ||||
|     ); | ||||
| } | ||||
| @ -20,8 +20,6 @@ pub mod storage { | ||||
|     pub mod config; | ||||
| } | ||||
|  | ||||
| pub mod cli; | ||||
|  | ||||
| pub mod api2; | ||||
|  | ||||
| pub mod client; | ||||
|  | ||||
		Reference in New Issue
	
	Block a user