src/cli/format.rs: move doc generation code into extra file
This commit is contained in:
parent
2f2b87e659
commit
12b4098660
96
src/cli.rs
96
src/cli.rs
@ -7,9 +7,105 @@
|
||||
mod environment;
|
||||
pub use environment::*;
|
||||
|
||||
mod format;
|
||||
pub use format::*;
|
||||
|
||||
mod getopts;
|
||||
pub use getopts::*;
|
||||
|
||||
mod command;
|
||||
pub use command::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use proxmox::api::ApiMethod;
|
||||
|
||||
pub type CompletionFunction = fn(&str, &HashMap<String, String>) -> Vec<String>;
|
||||
|
||||
pub struct CliCommand {
|
||||
pub info: &'static ApiMethod,
|
||||
pub arg_param: &'static [&'static str],
|
||||
pub fixed_param: HashMap<&'static str, String>,
|
||||
pub completion_functions: HashMap<String, CompletionFunction>,
|
||||
}
|
||||
|
||||
impl CliCommand {
|
||||
|
||||
pub fn new(info: &'static ApiMethod) -> Self {
|
||||
Self {
|
||||
info, arg_param: &[],
|
||||
fixed_param: HashMap::new(),
|
||||
completion_functions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arg_param(mut self, names: &'static [&'static str]) -> Self {
|
||||
self.arg_param = names;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fixed_param(mut self, key: &'static str, value: String) -> Self {
|
||||
self.fixed_param.insert(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn completion_cb(mut self, param_name: &str, cb: CompletionFunction) -> Self {
|
||||
self.completion_functions.insert(param_name.into(), cb);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CliCommandMap {
|
||||
pub commands: HashMap<String, CommandLineInterface>,
|
||||
}
|
||||
|
||||
impl CliCommandMap {
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self { commands: HashMap:: new() }
|
||||
}
|
||||
|
||||
pub fn insert<S: Into<String>>(mut self, name: S, cli: CommandLineInterface) -> Self {
|
||||
self.commands.insert(name.into(), cli);
|
||||
self
|
||||
}
|
||||
|
||||
fn find_command(&self, name: &str) -> Option<&CommandLineInterface> {
|
||||
|
||||
if let Some(sub_cmd) = self.commands.get(name) {
|
||||
return Some(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(sub_cmd);
|
||||
};
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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,7 +1,6 @@
|
||||
use failure::*;
|
||||
use serde_json::Value;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use proxmox::api::format::*;
|
||||
use proxmox::api::schema::*;
|
||||
@ -10,158 +9,14 @@ use proxmox::api::{ApiHandler, ApiMethod};
|
||||
use super::environment::CliEnvironment;
|
||||
|
||||
use super::getopts;
|
||||
use super::{CommandLineInterface, CliCommand, CliCommandMap, CompletionFunction};
|
||||
use super::format::*;
|
||||
|
||||
pub const OUTPUT_FORMAT: Schema =
|
||||
StringSchema::new("Output format.")
|
||||
.format(&ApiStringFormat::Enum(&["text", "json", "json-pretty"]))
|
||||
.schema();
|
||||
|
||||
/// 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!();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn print_simple_usage_error(prefix: &str, cli_cmd: &CliCommand, err: Error) {
|
||||
|
||||
let usage = generate_usage_str(prefix, cli_cmd, DocumentationFormat::Long, "");
|
||||
eprint!("Error: {}\nUsage: {}", err, usage);
|
||||
}
|
||||
|
||||
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(subcmd) = find_command(map, cmd) {
|
||||
iface = subcmd;
|
||||
prefix.push(' ');
|
||||
prefix.push_str(cmd);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
eprintln!("no such command '{}'", cmd);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
|
||||
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, ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_simple_command(
|
||||
_top_def: &CommandLineInterface,
|
||||
@ -210,61 +65,6 @@ fn handle_simple_command(
|
||||
}
|
||||
}
|
||||
|
||||
fn find_command<'a>(def: &'a CliCommandMap, name: &str) -> Option<&'a CommandLineInterface> {
|
||||
|
||||
if let Some(sub_cmd) = def.commands.get(name) {
|
||||
return Some(sub_cmd);
|
||||
};
|
||||
|
||||
let mut matches: Vec<&str> = vec![];
|
||||
|
||||
for cmd in def.commands.keys() {
|
||||
if cmd.starts_with(name) {
|
||||
matches.push(cmd); }
|
||||
}
|
||||
|
||||
if matches.len() != 1 { return None; }
|
||||
|
||||
if let Some(sub_cmd) = def.commands.get(matches[0]) {
|
||||
return Some(sub_cmd);
|
||||
};
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn print_nested_usage_error(prefix: &str, def: &CliCommandMap, err: Error) {
|
||||
|
||||
let usage = generate_nested_usage(prefix, def, DocumentationFormat::Short);
|
||||
|
||||
eprintln!("Error: {}\n\nUsage:\n\n{}", err, usage);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn handle_nested_command(
|
||||
top_def: &CommandLineInterface,
|
||||
prefix: &str,
|
||||
@ -289,7 +89,7 @@ fn handle_nested_command(
|
||||
|
||||
let command = args.remove(0);
|
||||
|
||||
let sub_cmd = match find_command(def, &command) {
|
||||
let sub_cmd = match def.find_command(&command) {
|
||||
Some(cmd) => cmd,
|
||||
None => {
|
||||
let err = format_err!("no such command '{}'", command);
|
||||
@ -572,70 +372,3 @@ pub fn run_cli_command(def: CommandLineInterface) {
|
||||
};
|
||||
}
|
||||
|
||||
pub type CompletionFunction = fn(&str, &HashMap<String, String>) -> Vec<String>;
|
||||
|
||||
pub struct CliCommand {
|
||||
pub info: &'static ApiMethod,
|
||||
pub arg_param: &'static [&'static str],
|
||||
pub fixed_param: HashMap<&'static str, String>,
|
||||
pub completion_functions: HashMap<String, CompletionFunction>,
|
||||
}
|
||||
|
||||
impl CliCommand {
|
||||
|
||||
pub fn new(info: &'static ApiMethod) -> Self {
|
||||
Self {
|
||||
info, arg_param: &[],
|
||||
fixed_param: HashMap::new(),
|
||||
completion_functions: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arg_param(mut self, names: &'static [&'static str]) -> Self {
|
||||
self.arg_param = names;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn fixed_param(mut self, key: &'static str, value: String) -> Self {
|
||||
self.fixed_param.insert(key, value);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn completion_cb(mut self, param_name: &str, cb: CompletionFunction) -> Self {
|
||||
self.completion_functions.insert(param_name.into(), cb);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CliCommandMap {
|
||||
pub commands: HashMap<String, CommandLineInterface>,
|
||||
}
|
||||
|
||||
impl CliCommandMap {
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self { commands: HashMap:: new() }
|
||||
}
|
||||
|
||||
pub fn insert<S: Into<String>>(mut self, name: S, cli: CommandLineInterface) -> Self {
|
||||
self.commands.insert(name.into(), cli);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
201
src/cli/format.rs
Normal file
201
src/cli/format.rs
Normal file
@ -0,0 +1,201 @@
|
||||
use failure::*;
|
||||
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!();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
pub fn print_simple_usage_error(
|
||||
prefix: &str,
|
||||
cli_cmd: &CliCommand,
|
||||
err: Error,
|
||||
) {
|
||||
let usage = generate_usage_str(prefix, cli_cmd, DocumentationFormat::Long, "");
|
||||
eprint!("Error: {}\nUsage: {}", err, usage);
|
||||
}
|
||||
|
||||
pub fn print_nested_usage_error(
|
||||
prefix: &str,
|
||||
def: &CliCommandMap,
|
||||
err: Error,
|
||||
) {
|
||||
let usage = generate_nested_usage(prefix, def, DocumentationFormat::Short);
|
||||
eprintln!("Error: {}\n\nUsage:\n\n{}", err, usage);
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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(subcmd) = map.find_command(cmd) {
|
||||
iface = subcmd;
|
||||
prefix.push(' ');
|
||||
prefix.push_str(cmd);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
eprintln!("no such command '{}'", cmd);
|
||||
std::process::exit(-1);
|
||||
}
|
||||
|
||||
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, ""));
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user