diff --git a/src/bin/completion.rs b/src/bin/completion.rs index 5108b4e3..e80d4fd7 100644 --- a/src/bin/completion.rs +++ b/src/bin/completion.rs @@ -1,8 +1,6 @@ use failure::*; -use proxmox::api::*; - -use proxmox_backup::cli::*; +use proxmox::api::{*, cli::*}; #[api( input: { diff --git a/src/bin/proxmox-backup-client.rs b/src/bin/proxmox-backup-client.rs index 6f146d9e..db4bde54 100644 --- a/src/bin/proxmox-backup-client.rs +++ b/src/bin/proxmox-backup-client.rs @@ -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::*; diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs index c5bdc7d0..9052c632 100644 --- a/src/bin/proxmox-backup-manager.rs +++ b/src/bin/proxmox-backup-manager.rs @@ -1,7 +1,6 @@ extern crate proxmox_backup; -//use proxmox_backup::api2; -use proxmox_backup::cli::*; +use proxmox::api::cli::*; fn datastore_commands() -> CommandLineInterface { diff --git a/src/bin/pxar.rs b/src/bin/pxar.rs index 5d4b53c7..e8987158 100644 --- a/src/bin/pxar.rs +++ b/src/bin/pxar.rs @@ -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}; diff --git a/src/cli.rs b/src/cli.rs deleted file mode 100644 index 2855aa43..00000000 --- a/src/cli.rs +++ /dev/null @@ -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) -> Vec; - -/// 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 `` 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, -} - -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, -} - -impl CliCommandMap { - - /// Create a new instance. - pub fn new() -> Self { - Self { commands: HashMap:: new() } - } - - /// Insert another command. - pub fn insert>(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 for CommandLineInterface { - fn from(cli_cmd: CliCommand) -> Self { - CommandLineInterface::Simple(cli_cmd) - } -} - -impl From for CommandLineInterface { - fn from(list: CliCommandMap) -> Self { - CommandLineInterface::Nested(list) - } -} diff --git a/src/cli/command.rs b/src/cli/command.rs deleted file mode 100644 index d070f436..00000000 --- a/src/cli/command.rs +++ /dev/null @@ -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, -) -> 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, -) -> 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>> = RefCell::new(None); -} - -fn help_command( - param: Value, - _info: &ApiMethod, - _rpcenv: &mut dyn RpcEnvironment, -) -> Result { - - let command: Vec = 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>) { - 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, - prefix: &str, - args: Vec, -) -> 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 = 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); - } -} diff --git a/src/cli/completion.rs b/src/cli/completion.rs deleted file mode 100644 index 2df2ddfb..00000000 --- a/src/cli/completion.rs +++ /dev/null @@ -1,257 +0,0 @@ -use super::*; - -use proxmox::api::schema::*; - -fn record_done_argument( - done: &mut HashMap, - 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, - arg: &str, - param: &HashMap, -) -> Vec { - - 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, - arg_param: &[&str], // we remove done arguments - args: &[String], -) -> Vec { - // 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 { - - 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 { - - match def { - CommandLineInterface::Simple(cli_cmd) => { - let mut done: HashMap = 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) { - - 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) -} diff --git a/src/cli/environment.rs b/src/cli/environment.rs deleted file mode 100644 index ad40ca1a..00000000 --- a/src/cli/environment.rs +++ /dev/null @@ -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, - user: Option, -} - -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) { - self.user = user; - } - - fn get_user(&self) -> Option { - self.user.clone() - } -} diff --git a/src/cli/format.rs b/src/cli/format.rs deleted file mode 100644 index 4158e6ec..00000000 --- a/src/cli/format.rs +++ /dev/null @@ -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, - verbose: Option, -) { - 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, "")); - } - } -} diff --git a/src/cli/getopts.rs b/src/cli/getopts.rs deleted file mode 100644 index 8e242edf..00000000 --- a/src/cli/getopts.rs +++ /dev/null @@ -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 }, -} - -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. This does not -/// verify the schema. -/// Returns parsed data and the rest as separate array -pub (crate) fn parse_argument_list>( - args: &[T], - schema: &ObjectSchema, - errors: &mut ParameterError, -) -> (Vec<(String, String)>, Vec) { - - let mut data: Vec<(String, String)> = vec![]; - let mut rest: Vec = 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>( - args: &[T], - arg_param: &[&str], - schema: &ObjectSchema, -) -> Result<(Value, Vec), 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); - } -} diff --git a/src/cli/readline.rs b/src/cli/readline.rs deleted file mode 100644 index 79217263..00000000 --- a/src/cli/readline.rs +++ /dev/null @@ -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, -} - -impl CliHelper { - - pub fn new(cmd_def: CommandLineInterface) -> Self { - Self { cmd_def: Arc::new(cmd_def) } - } - - pub fn cmd_def(&self) -> Arc { - 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)> { - - 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 {} diff --git a/src/cli/shellword.rs b/src/cli/shellword.rs deleted file mode 100644 index 5248fc22..00000000 --- a/src/cli/shellword.rs +++ /dev/null @@ -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, Option<(usize, String, Quote)>) { - - let char_indices = s.char_indices(); - let mut args: Vec = 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, 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 = 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 = 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 = 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) - ); -} diff --git a/src/lib.rs b/src/lib.rs index 054e2cc6..a3767367 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,8 +20,6 @@ pub mod storage { pub mod config; } -pub mod cli; - pub mod api2; pub mod client;