mirror of
https://git.proxmox.com/git/proxmox
synced 2025-08-07 07:38:59 +00:00
router: cli: doc generation with global options
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
69c2f94aab
commit
667fa6bc6b
@ -9,6 +9,12 @@ description = "proxmox API Router and CLI utilities"
|
|||||||
|
|
||||||
exclude.workspace = true
|
exclude.workspace = true
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "docs"
|
||||||
|
path = "tests/docs.rs"
|
||||||
|
test = true
|
||||||
|
required-features = [ "cli" ]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
env_logger = { workspace = true, optional = true }
|
env_logger = { workspace = true, optional = true }
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use anyhow::{format_err, Error};
|
use anyhow::{bail, format_err, Error};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@ -9,8 +9,8 @@ use proxmox_schema::*;
|
|||||||
use super::environment::CliEnvironment;
|
use super::environment::CliEnvironment;
|
||||||
use super::getopts;
|
use super::getopts;
|
||||||
use super::{
|
use super::{
|
||||||
generate_nested_usage, generate_usage_str, print_help, print_nested_usage_error,
|
generate_nested_usage, generate_usage_str_do, print_help, print_nested_usage_error,
|
||||||
print_simple_usage_error, CliCommand, CliCommandMap, CommandLineInterface,
|
print_simple_usage_error_do, CliCommand, CliCommandMap, CommandLineInterface,
|
||||||
};
|
};
|
||||||
use crate::{ApiFuture, ApiHandler, ApiMethod, RpcEnvironment};
|
use crate::{ApiFuture, ApiHandler, ApiMethod, RpcEnvironment};
|
||||||
|
|
||||||
@ -28,7 +28,12 @@ pub const OUTPUT_FORMAT: Schema = StringSchema::new("Output format.")
|
|||||||
]))
|
]))
|
||||||
.schema();
|
.schema();
|
||||||
|
|
||||||
fn parse_arguments(prefix: &str, cli_cmd: &CliCommand, args: Vec<String>) -> Result<Value, Error> {
|
fn parse_arguments(
|
||||||
|
prefix: &str,
|
||||||
|
cli_cmd: &CliCommand,
|
||||||
|
args: Vec<String>,
|
||||||
|
global_options_iter: impl Iterator<Item = &'static str>,
|
||||||
|
) -> Result<Value, Error> {
|
||||||
let (params, remaining) = match getopts::parse_arguments(
|
let (params, remaining) = match getopts::parse_arguments(
|
||||||
&args,
|
&args,
|
||||||
cli_cmd.arg_param,
|
cli_cmd.arg_param,
|
||||||
@ -38,14 +43,14 @@ fn parse_arguments(prefix: &str, cli_cmd: &CliCommand, args: Vec<String>) -> Res
|
|||||||
Ok((p, r)) => (p, r),
|
Ok((p, r)) => (p, r),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let err_msg = err.to_string();
|
let err_msg = err.to_string();
|
||||||
print_simple_usage_error(prefix, cli_cmd, &err_msg);
|
print_simple_usage_error_do(prefix, cli_cmd, &err_msg, global_options_iter);
|
||||||
return Err(format_err!("{}", err_msg));
|
return Err(format_err!("{}", err_msg));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if !remaining.is_empty() {
|
if !remaining.is_empty() {
|
||||||
let err_msg = format!("got additional arguments: {:?}", remaining);
|
let err_msg = format!("got additional arguments: {:?}", remaining);
|
||||||
print_simple_usage_error(prefix, cli_cmd, &err_msg);
|
print_simple_usage_error_do(prefix, cli_cmd, &err_msg, global_options_iter);
|
||||||
return Err(format_err!("{}", err_msg));
|
return Err(format_err!("{}", err_msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,7 +63,7 @@ async fn handle_simple_command_future(
|
|||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
mut rpcenv: CliEnvironment,
|
mut rpcenv: CliEnvironment,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let params = parse_arguments(prefix, cli_cmd, args)?;
|
let params = parse_arguments(prefix, cli_cmd, args, [].into_iter())?;
|
||||||
|
|
||||||
let result = match cli_cmd.info.handler {
|
let result = match cli_cmd.info.handler {
|
||||||
ApiHandler::Sync(handler) => (handler)(params, cli_cmd.info, &mut rpcenv),
|
ApiHandler::Sync(handler) => (handler)(params, cli_cmd.info, &mut rpcenv),
|
||||||
@ -70,9 +75,7 @@ async fn handle_simple_command_future(
|
|||||||
.and_then(|r| r.to_value().map_err(Error::from)),
|
.and_then(|r| r.to_value().map_err(Error::from)),
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
ApiHandler::AsyncHttp(_) => {
|
ApiHandler::AsyncHttp(_) => {
|
||||||
let err_msg = "CliHandler does not support ApiHandler::AsyncHttp - internal error";
|
bail!("CliHandler does not support ApiHandler::AsyncHttp - internal error")
|
||||||
print_simple_usage_error(prefix, cli_cmd, err_msg);
|
|
||||||
return Err(format_err!("{}", err_msg));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -97,35 +100,28 @@ pub(crate) fn handle_simple_command(
|
|||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
rpcenv: &mut CliEnvironment,
|
rpcenv: &mut CliEnvironment,
|
||||||
run: Option<fn(ApiFuture) -> Result<Value, Error>>,
|
run: Option<fn(ApiFuture) -> Result<Value, Error>>,
|
||||||
|
global_options_iter: impl Iterator<Item = &'static str>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
let params = parse_arguments(prefix, cli_cmd, args)?;
|
let params = parse_arguments(prefix, cli_cmd, args, global_options_iter)?;
|
||||||
|
|
||||||
let result = match cli_cmd.info.handler {
|
let result = match cli_cmd.info.handler {
|
||||||
ApiHandler::Sync(handler) => (handler)(params, cli_cmd.info, rpcenv),
|
ApiHandler::Sync(handler) => (handler)(params, cli_cmd.info, rpcenv),
|
||||||
ApiHandler::StreamingSync(handler) => {
|
ApiHandler::StreamingSync(handler) => {
|
||||||
(handler)(params, cli_cmd.info, rpcenv).and_then(|r| r.to_value().map_err(Error::from))
|
(handler)(params, cli_cmd.info, rpcenv).and_then(|r| r.to_value().map_err(Error::from))
|
||||||
}
|
}
|
||||||
ApiHandler::Async(handler) => match run {
|
ApiHandler::Async(handler) => {
|
||||||
Some(run) => {
|
let run = run.ok_or_else(|| {
|
||||||
let future = (handler)(params, cli_cmd.info, rpcenv);
|
format_err!("CliHandler does not support ApiHandler::Async - internal error")
|
||||||
(run)(future)
|
})?;
|
||||||
}
|
let future = (handler)(params, cli_cmd.info, rpcenv);
|
||||||
None => {
|
(run)(future)
|
||||||
let err_msg = "CliHandler does not support ApiHandler::Async - internal error";
|
}
|
||||||
print_simple_usage_error(prefix, cli_cmd, err_msg);
|
|
||||||
return Err(format_err!("{}", err_msg));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
ApiHandler::StreamingAsync(_handler) => {
|
ApiHandler::StreamingAsync(_handler) => {
|
||||||
let err_msg = "CliHandler does not support ApiHandler::StreamingAsync - internal error";
|
bail!("CliHandler does not support ApiHandler::StreamingAsync - internal error");
|
||||||
print_simple_usage_error(prefix, cli_cmd, err_msg);
|
|
||||||
return Err(format_err!("{}", err_msg));
|
|
||||||
}
|
}
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
ApiHandler::AsyncHttp(_) => {
|
ApiHandler::AsyncHttp(_) => {
|
||||||
let err_msg = "CliHandler does not support ApiHandler::AsyncHttp - internal error";
|
bail!("CliHandler does not support ApiHandler::AsyncHttp - internal error");
|
||||||
print_simple_usage_error(prefix, cli_cmd, err_msg);
|
|
||||||
return Err(format_err!("{}", err_msg));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -325,12 +321,12 @@ pub fn handle_command(
|
|||||||
|
|
||||||
let result = match &*def {
|
let result = match &*def {
|
||||||
CommandLineInterface::Simple(ref cli_cmd) => {
|
CommandLineInterface::Simple(ref cli_cmd) => {
|
||||||
handle_simple_command(prefix, cli_cmd, args, &mut rpcenv, run)
|
handle_simple_command(prefix, cli_cmd, args, &mut rpcenv, run, [].into_iter())
|
||||||
}
|
}
|
||||||
CommandLineInterface::Nested(ref map) => {
|
CommandLineInterface::Nested(ref map) => {
|
||||||
let mut prefix = prefix.to_string();
|
let mut prefix = prefix.to_string();
|
||||||
let cli_cmd = parse_nested_command(&mut prefix, map, &mut args)?;
|
let cli_cmd = parse_nested_command(&mut prefix, map, &mut args)?;
|
||||||
handle_simple_command(&prefix, cli_cmd, args, &mut rpcenv, run)
|
handle_simple_command(&prefix, cli_cmd, args, &mut rpcenv, run, [].into_iter())
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -359,9 +355,14 @@ where
|
|||||||
|
|
||||||
if args[0] == "printdoc" {
|
if args[0] == "printdoc" {
|
||||||
let usage = match def {
|
let usage = match def {
|
||||||
CommandLineInterface::Simple(cli_cmd) => {
|
CommandLineInterface::Simple(cli_cmd) => generate_usage_str_do(
|
||||||
generate_usage_str(&prefix, cli_cmd, DocumentationFormat::ReST, "", &[])
|
&prefix,
|
||||||
}
|
cli_cmd,
|
||||||
|
DocumentationFormat::ReST,
|
||||||
|
"",
|
||||||
|
&[],
|
||||||
|
[].into_iter(),
|
||||||
|
),
|
||||||
CommandLineInterface::Nested(map) => {
|
CommandLineInterface::Nested(map) => {
|
||||||
generate_nested_usage(&prefix, map, DocumentationFormat::ReST)
|
generate_nested_usage(&prefix, map, DocumentationFormat::ReST)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
#![allow(clippy::match_bool)] // just no...
|
#![allow(clippy::match_bool)] // just no...
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use anyhow::{bail, Error};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ use proxmox_schema::format::{
|
|||||||
use proxmox_schema::*;
|
use proxmox_schema::*;
|
||||||
|
|
||||||
use super::{value_to_text, TableFormatOptions};
|
use super::{value_to_text, TableFormatOptions};
|
||||||
use super::{CliCommand, CliCommandMap, CommandLineInterface};
|
use super::{CliCommand, CliCommandMap, CommandLineInterface, GlobalOptions};
|
||||||
|
|
||||||
/// Helper function to format and print result.
|
/// Helper function to format and print result.
|
||||||
///
|
///
|
||||||
@ -56,6 +57,7 @@ pub fn format_and_print_result_full(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deprecated = "to be removed, not meant as a public interface"]
|
||||||
/// Helper to generate command usage text for simple commands.
|
/// Helper to generate command usage text for simple commands.
|
||||||
pub fn generate_usage_str(
|
pub fn generate_usage_str(
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
@ -63,6 +65,24 @@ pub fn generate_usage_str(
|
|||||||
format: DocumentationFormat,
|
format: DocumentationFormat,
|
||||||
indent: &str,
|
indent: &str,
|
||||||
skip_options: &[&str],
|
skip_options: &[&str],
|
||||||
|
) -> String {
|
||||||
|
generate_usage_str_do(
|
||||||
|
prefix,
|
||||||
|
cli_cmd,
|
||||||
|
format,
|
||||||
|
indent,
|
||||||
|
skip_options,
|
||||||
|
[].into_iter(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn generate_usage_str_do(
|
||||||
|
prefix: &str,
|
||||||
|
cli_cmd: &CliCommand,
|
||||||
|
format: DocumentationFormat,
|
||||||
|
indent: &str,
|
||||||
|
skip_options: &[&str],
|
||||||
|
global_options_iter: impl Iterator<Item = &'static str>,
|
||||||
) -> String {
|
) -> String {
|
||||||
let arg_param = cli_cmd.arg_param;
|
let arg_param = cli_cmd.arg_param;
|
||||||
let fixed_param = &cli_cmd.fixed_param;
|
let fixed_param = &cli_cmd.fixed_param;
|
||||||
@ -180,12 +200,51 @@ pub fn generate_usage_str(
|
|||||||
text.push_str("Optional parameters:\n\n");
|
text.push_str("Optional parameters:\n\n");
|
||||||
text.push_str(&options);
|
text.push_str(&options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut global_options = String::new();
|
||||||
|
let mut separator = "";
|
||||||
|
for opt in global_options_iter {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
|
if done_hash.contains(opt) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = match format {
|
||||||
|
DocumentationFormat::ReST => writeln!(global_options, "{separator}``--{opt}``"),
|
||||||
|
_ => writeln!(global_options, "--{opt}"),
|
||||||
|
};
|
||||||
|
separator = "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if !global_options.is_empty() {
|
||||||
|
text.push_str("Inherited group parameters:\n\n");
|
||||||
|
text.push_str(&global_options);
|
||||||
|
}
|
||||||
|
|
||||||
text
|
text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deprecated = "will be removed, not meant to be a public interface"]
|
||||||
/// Print command usage for simple commands to ``stderr``.
|
/// Print command usage for simple commands to ``stderr``.
|
||||||
pub fn print_simple_usage_error(prefix: &str, cli_cmd: &CliCommand, err_msg: &str) {
|
pub fn print_simple_usage_error(prefix: &str, cli_cmd: &CliCommand, err_msg: &str) {
|
||||||
let usage = generate_usage_str(prefix, cli_cmd, DocumentationFormat::Long, "", &[]);
|
print_simple_usage_error_do(prefix, cli_cmd, err_msg, [].into_iter())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print command usage for simple commands to ``stderr``.
|
||||||
|
pub(crate) fn print_simple_usage_error_do(
|
||||||
|
prefix: &str,
|
||||||
|
cli_cmd: &CliCommand,
|
||||||
|
err_msg: &str,
|
||||||
|
global_options_iter: impl Iterator<Item = &'static str>,
|
||||||
|
) {
|
||||||
|
let usage = generate_usage_str_do(
|
||||||
|
prefix,
|
||||||
|
cli_cmd,
|
||||||
|
DocumentationFormat::Long,
|
||||||
|
"",
|
||||||
|
&[],
|
||||||
|
global_options_iter,
|
||||||
|
);
|
||||||
eprint!("Error: {}\nUsage: {}", err_msg, usage);
|
eprint!("Error: {}\nUsage: {}", err_msg, usage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,12 +254,89 @@ pub fn print_nested_usage_error(prefix: &str, def: &CliCommandMap, err_msg: &str
|
|||||||
eprintln!("Error: {}\n\nUsage:\n\n{}", err_msg, usage);
|
eprintln!("Error: {}\n\nUsage:\n\n{}", err_msg, usage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// While going through nested commands, this keeps track of the available global options.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct UsageState {
|
||||||
|
global_options: Vec<Vec<&'static Schema>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UsageState {
|
||||||
|
fn push_global_options(&mut self, options: &HashMap<std::any::TypeId, GlobalOptions>) {
|
||||||
|
self.global_options
|
||||||
|
.push(options.values().map(|o| o.schema).collect());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_global_options(&mut self) {
|
||||||
|
self.global_options.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn describe_current(&self, prefix: &str, format: DocumentationFormat) -> String {
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
|
let mut out = String::new();
|
||||||
|
|
||||||
|
let Some(opts) = self.global_options.last() else {
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
if opts.is_empty() {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !matches!(
|
||||||
|
format,
|
||||||
|
DocumentationFormat::ReST | DocumentationFormat::Full
|
||||||
|
) {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if format == DocumentationFormat::ReST {
|
||||||
|
let _ = write!(out, "----\n\n");
|
||||||
|
}
|
||||||
|
let _ = write!(out, "Options available for command group ``{prefix}``:\n\n");
|
||||||
|
for opt in opts {
|
||||||
|
for (name, _optional, schema) in opt
|
||||||
|
.any_object()
|
||||||
|
.expect("non-object schema in global optiosn")
|
||||||
|
.properties()
|
||||||
|
{
|
||||||
|
let _ = write!(
|
||||||
|
out,
|
||||||
|
"{}",
|
||||||
|
get_property_description(name, schema, ParameterDisplayStyle::Arg, format)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn global_options_iter(&self) -> impl Iterator<Item = &'static str> + '_ {
|
||||||
|
self.global_options
|
||||||
|
.iter()
|
||||||
|
.flat_map(|list| list.iter().copied())
|
||||||
|
.flat_map(|o| o.any_object().unwrap().properties())
|
||||||
|
.map(|(name, _optional, _schema)| *name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper to generate command usage text for nested commands.
|
/// Helper to generate command usage text for nested commands.
|
||||||
pub fn generate_nested_usage(
|
pub fn generate_nested_usage(
|
||||||
prefix: &str,
|
prefix: &str,
|
||||||
def: &CliCommandMap,
|
def: &CliCommandMap,
|
||||||
format: DocumentationFormat,
|
format: DocumentationFormat,
|
||||||
) -> String {
|
) -> String {
|
||||||
|
generate_nested_usage_do(&mut UsageState::default(), prefix, def, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn generate_nested_usage_do(
|
||||||
|
state: &mut UsageState,
|
||||||
|
prefix: &str,
|
||||||
|
def: &CliCommandMap,
|
||||||
|
format: DocumentationFormat,
|
||||||
|
) -> String {
|
||||||
|
state.push_global_options(&def.global_options);
|
||||||
|
|
||||||
let mut cmds: Vec<&String> = def.commands.keys().collect();
|
let mut cmds: Vec<&String> = def.commands.keys().collect();
|
||||||
cmds.sort();
|
cmds.sort();
|
||||||
|
|
||||||
@ -208,6 +344,11 @@ pub fn generate_nested_usage(
|
|||||||
|
|
||||||
let mut usage = String::new();
|
let mut usage = String::new();
|
||||||
|
|
||||||
|
let globals = state.describe_current(prefix, format);
|
||||||
|
if !globals.is_empty() {
|
||||||
|
usage.push_str(&globals);
|
||||||
|
}
|
||||||
|
|
||||||
for cmd in cmds {
|
for cmd in cmds {
|
||||||
let new_prefix = if prefix.is_empty() {
|
let new_prefix = if prefix.is_empty() {
|
||||||
String::from(cmd)
|
String::from(cmd)
|
||||||
@ -220,34 +361,55 @@ pub fn generate_nested_usage(
|
|||||||
if !usage.is_empty() && format == DocumentationFormat::ReST {
|
if !usage.is_empty() && format == DocumentationFormat::ReST {
|
||||||
usage.push_str("----\n\n");
|
usage.push_str("----\n\n");
|
||||||
}
|
}
|
||||||
usage.push_str(&generate_usage_str(
|
usage.push_str(&generate_usage_str_do(
|
||||||
&new_prefix,
|
&new_prefix,
|
||||||
cli_cmd,
|
cli_cmd,
|
||||||
format,
|
format,
|
||||||
"",
|
"",
|
||||||
skip_options,
|
skip_options,
|
||||||
|
state.global_options_iter(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
CommandLineInterface::Nested(map) => {
|
CommandLineInterface::Nested(map) => {
|
||||||
usage.push_str(&generate_nested_usage(&new_prefix, map, format));
|
usage.push_str(&generate_nested_usage_do(state, &new_prefix, map, format));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.pop_global_options();
|
||||||
|
|
||||||
usage
|
usage
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Print help text to ``stderr``.
|
/// Print help text to ``stderr``.
|
||||||
pub fn print_help(
|
pub fn print_help(
|
||||||
|
top_def: &CommandLineInterface,
|
||||||
|
prefix: String,
|
||||||
|
args: &[String],
|
||||||
|
verbose: Option<bool>,
|
||||||
|
) {
|
||||||
|
let mut message = String::new();
|
||||||
|
match print_help_to(top_def, prefix, args, verbose, &mut message) {
|
||||||
|
Ok(()) => print!("{message}"),
|
||||||
|
Err(err) => eprintln!("{err}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print_help_to(
|
||||||
top_def: &CommandLineInterface,
|
top_def: &CommandLineInterface,
|
||||||
mut prefix: String,
|
mut prefix: String,
|
||||||
args: &[String],
|
args: &[String],
|
||||||
mut verbose: Option<bool>,
|
mut verbose: Option<bool>,
|
||||||
) {
|
mut to: impl std::fmt::Write,
|
||||||
|
) -> Result<(), Error> {
|
||||||
let mut iface = top_def;
|
let mut iface = top_def;
|
||||||
|
|
||||||
|
let mut usage_state = UsageState::default();
|
||||||
|
|
||||||
for cmd in args {
|
for cmd in args {
|
||||||
if let CommandLineInterface::Nested(map) = iface {
|
if let CommandLineInterface::Nested(map) = iface {
|
||||||
|
usage_state.push_global_options(&map.global_options);
|
||||||
|
|
||||||
if let Some((full_name, subcmd)) = map.find_command(cmd) {
|
if let Some((full_name, subcmd)) = map.find_command(cmd) {
|
||||||
iface = subcmd;
|
iface = subcmd;
|
||||||
if !prefix.is_empty() {
|
if !prefix.is_empty() {
|
||||||
@ -258,11 +420,10 @@ pub fn print_help(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if prefix.is_empty() {
|
if prefix.is_empty() {
|
||||||
eprintln!("no such command '{}'", cmd);
|
bail!("no such command '{}'", cmd);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("no such command '{} {}'", prefix, cmd);
|
bail!("no such command '{} {}'", prefix, cmd);
|
||||||
}
|
}
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if verbose.is_none() {
|
if verbose.is_none() {
|
||||||
@ -278,13 +439,27 @@ pub fn print_help(
|
|||||||
|
|
||||||
match iface {
|
match iface {
|
||||||
CommandLineInterface::Nested(map) => {
|
CommandLineInterface::Nested(map) => {
|
||||||
println!("Usage:\n\n{}", generate_nested_usage(&prefix, map, format));
|
write!(
|
||||||
|
to,
|
||||||
|
"Usage:\n\n{}",
|
||||||
|
generate_nested_usage_do(&mut usage_state, &prefix, map, format)
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
CommandLineInterface::Simple(cli_cmd) => {
|
CommandLineInterface::Simple(cli_cmd) => {
|
||||||
println!(
|
write!(
|
||||||
|
to,
|
||||||
"Usage: {}",
|
"Usage: {}",
|
||||||
generate_usage_str(&prefix, cli_cmd, format, "", &[])
|
generate_usage_str_do(
|
||||||
);
|
&prefix,
|
||||||
|
cli_cmd,
|
||||||
|
format,
|
||||||
|
"",
|
||||||
|
&[],
|
||||||
|
usage_state.global_options_iter()
|
||||||
|
)
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -544,8 +544,14 @@ impl<'cli> CommandLineParseState<'cli> {
|
|||||||
Ok(Invocation {
|
Ok(Invocation {
|
||||||
call: Box::new(move |rpcenv| {
|
call: Box::new(move |rpcenv| {
|
||||||
command::set_help_context(Some(interface));
|
command::set_help_context(Some(interface));
|
||||||
let out =
|
let out = command::handle_simple_command(
|
||||||
command::handle_simple_command(&self.prefix, cli, args, rpcenv, self.async_run);
|
&self.prefix,
|
||||||
|
cli,
|
||||||
|
args,
|
||||||
|
rpcenv,
|
||||||
|
self.async_run,
|
||||||
|
self.global_option_schemas.keys().copied(),
|
||||||
|
);
|
||||||
command::set_help_context(None);
|
command::set_help_context(None);
|
||||||
out
|
out
|
||||||
}),
|
}),
|
||||||
|
316
proxmox-router/tests/docs.rs
Normal file
316
proxmox-router/tests/docs.rs
Normal file
@ -0,0 +1,316 @@
|
|||||||
|
use anyhow::Error;
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use proxmox_router::cli::{CliCommand, CliCommandMap, CommandLineInterface, GlobalOptions};
|
||||||
|
use proxmox_router::{ApiHandler, ApiMethod, RpcEnvironment};
|
||||||
|
use proxmox_schema::format::DocumentationFormat;
|
||||||
|
use proxmox_schema::{
|
||||||
|
ApiStringFormat, ApiType, BooleanSchema, EnumEntry, ObjectSchema, Schema, StringSchema,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn dummy_method(
|
||||||
|
_param: Value,
|
||||||
|
_info: &ApiMethod,
|
||||||
|
_rpcenv: &mut dyn RpcEnvironment,
|
||||||
|
) -> Result<Value, Error> {
|
||||||
|
Ok(Value::Null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const API_METHOD_SIMPLE1: ApiMethod = ApiMethod::new(
|
||||||
|
&ApiHandler::Sync(&dummy_method),
|
||||||
|
&ObjectSchema::new(
|
||||||
|
"Simple API method with one required and one optional argument.",
|
||||||
|
&[
|
||||||
|
(
|
||||||
|
"optional-arg",
|
||||||
|
true,
|
||||||
|
&BooleanSchema::new("Optional boolean argument.")
|
||||||
|
.default(false)
|
||||||
|
.schema(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"required-arg",
|
||||||
|
false,
|
||||||
|
&StringSchema::new("Required string argument.").schema(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"another-required-arg",
|
||||||
|
false,
|
||||||
|
&StringSchema::new("A second required string argument.").schema(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct GlobalOpts {
|
||||||
|
global: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for GlobalOpts {
|
||||||
|
fn deserialize<D>(_deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
unreachable!("not used in tests, implemented to satisfy `.global_option` constraint");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ApiType for GlobalOpts {
|
||||||
|
const API_SCHEMA: Schema = ObjectSchema::new(
|
||||||
|
"Global options.",
|
||||||
|
&[
|
||||||
|
(
|
||||||
|
"global1",
|
||||||
|
true,
|
||||||
|
&StringSchema::new("A global option.")
|
||||||
|
.format(&ApiStringFormat::Enum(&[
|
||||||
|
EnumEntry::new("one", "Option one."),
|
||||||
|
EnumEntry::new("two", "Option two."),
|
||||||
|
]))
|
||||||
|
.schema(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"global2",
|
||||||
|
true,
|
||||||
|
&StringSchema::new("A second global option.").schema(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.schema();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates the following:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// clicmd l0c1 --required-arg --another-required-arg [--optional-arg]
|
||||||
|
/// clicmd l0c2 <required-arg> --another-required-arg [--optional-arg]
|
||||||
|
/// clicmd l0sub l1c1 --required-arg --another-required-arg [--optional-arg]
|
||||||
|
/// clicmd l0sub l1c2 --required-arg --another-required-arg [--optional-arg]
|
||||||
|
/// ```
|
||||||
|
fn get_complex_test_cmddef() -> CliCommandMap {
|
||||||
|
let sub_def = CliCommandMap::new()
|
||||||
|
.global_option(GlobalOptions::of::<GlobalOpts>())
|
||||||
|
.insert("l1c1", CliCommand::new(&API_METHOD_SIMPLE1))
|
||||||
|
.insert("l1c2", CliCommand::new(&API_METHOD_SIMPLE1));
|
||||||
|
|
||||||
|
CliCommandMap::new()
|
||||||
|
.insert_help()
|
||||||
|
.insert("l0sub", CommandLineInterface::Nested(sub_def))
|
||||||
|
.insert("l0c1", CliCommand::new(&API_METHOD_SIMPLE1))
|
||||||
|
.insert(
|
||||||
|
"l0c2",
|
||||||
|
CliCommand::new(&API_METHOD_SIMPLE1).arg_param(&["required-arg"]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_toplevel_help_text() -> &'static str {
|
||||||
|
r##"
|
||||||
|
Usage:
|
||||||
|
|
||||||
|
clicmd help [{<command>}] [OPTIONS]
|
||||||
|
clicmd l0c1 --required-arg <string> --another-required-arg <string> [OPTIONS]
|
||||||
|
clicmd l0c2 <required-arg> --another-required-arg <string> [OPTIONS]
|
||||||
|
clicmd l0sub l1c1 --required-arg <string> --another-required-arg <string> [OPTIONS]
|
||||||
|
clicmd l0sub l1c2 --required-arg <string> --another-required-arg <string> [OPTIONS]
|
||||||
|
"##
|
||||||
|
.trim_start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_group_help_text() -> &'static str {
|
||||||
|
r##"
|
||||||
|
Usage: clicmd l0sub l1c1 --required-arg <string> --another-required-arg <string> [OPTIONS]
|
||||||
|
|
||||||
|
Simple API method with one required and one optional argument.
|
||||||
|
|
||||||
|
--required-arg <string>
|
||||||
|
Required string argument.
|
||||||
|
|
||||||
|
--another-required-arg <string>
|
||||||
|
A second required string argument.
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
|
||||||
|
--optional-arg <boolean> (default=false)
|
||||||
|
Optional boolean argument.
|
||||||
|
|
||||||
|
Inherited group parameters:
|
||||||
|
|
||||||
|
--global1
|
||||||
|
--global2
|
||||||
|
"##
|
||||||
|
.trim_start()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expected_nested_usage_text() -> &'static str {
|
||||||
|
r##"
|
||||||
|
``clicmd help [{<command>}] [OPTIONS]``
|
||||||
|
|
||||||
|
Get help about specified command (or sub-command).
|
||||||
|
|
||||||
|
``<command>`` : ``<string>``
|
||||||
|
Command. This may be a list in order to spefify nested sub-commands. Can be
|
||||||
|
specified more than once.
|
||||||
|
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
|
||||||
|
``--verbose`` ``<boolean>``
|
||||||
|
Verbose help.
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
``clicmd l0c1 --required-arg <string> --another-required-arg <string> [OPTIONS]``
|
||||||
|
|
||||||
|
Simple API method with one required and one optional argument.
|
||||||
|
|
||||||
|
``--required-arg`` ``<string>``
|
||||||
|
Required string argument.
|
||||||
|
|
||||||
|
|
||||||
|
``--another-required-arg`` ``<string>``
|
||||||
|
A second required string argument.
|
||||||
|
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
|
||||||
|
``--optional-arg`` ``<boolean> (default=false)``
|
||||||
|
Optional boolean argument.
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
``clicmd l0c2 <required-arg> --another-required-arg <string> [OPTIONS]``
|
||||||
|
|
||||||
|
Simple API method with one required and one optional argument.
|
||||||
|
|
||||||
|
``<required-arg>`` : ``<string>``
|
||||||
|
Required string argument.
|
||||||
|
|
||||||
|
|
||||||
|
``--another-required-arg`` ``<string>``
|
||||||
|
A second required string argument.
|
||||||
|
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
|
||||||
|
``--optional-arg`` ``<boolean> (default=false)``
|
||||||
|
Optional boolean argument.
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
Options available for command group ``clicmd l0sub``:
|
||||||
|
|
||||||
|
``--global1`` ``one|two``
|
||||||
|
A global option.
|
||||||
|
|
||||||
|
|
||||||
|
``--global2`` ``<string>``
|
||||||
|
A second global option.
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
``clicmd l0sub l1c1 --required-arg <string> --another-required-arg <string> [OPTIONS]``
|
||||||
|
|
||||||
|
Simple API method with one required and one optional argument.
|
||||||
|
|
||||||
|
``--required-arg`` ``<string>``
|
||||||
|
Required string argument.
|
||||||
|
|
||||||
|
|
||||||
|
``--another-required-arg`` ``<string>``
|
||||||
|
A second required string argument.
|
||||||
|
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
|
||||||
|
``--optional-arg`` ``<boolean> (default=false)``
|
||||||
|
Optional boolean argument.
|
||||||
|
|
||||||
|
|
||||||
|
Inherited group parameters:
|
||||||
|
|
||||||
|
``--global1``
|
||||||
|
|
||||||
|
``--global2``
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
``clicmd l0sub l1c2 --required-arg <string> --another-required-arg <string> [OPTIONS]``
|
||||||
|
|
||||||
|
Simple API method with one required and one optional argument.
|
||||||
|
|
||||||
|
``--required-arg`` ``<string>``
|
||||||
|
Required string argument.
|
||||||
|
|
||||||
|
|
||||||
|
``--another-required-arg`` ``<string>``
|
||||||
|
A second required string argument.
|
||||||
|
|
||||||
|
|
||||||
|
Optional parameters:
|
||||||
|
|
||||||
|
``--optional-arg`` ``<boolean> (default=false)``
|
||||||
|
Optional boolean argument.
|
||||||
|
|
||||||
|
|
||||||
|
Inherited group parameters:
|
||||||
|
|
||||||
|
``--global1``
|
||||||
|
|
||||||
|
``--global2``
|
||||||
|
"##
|
||||||
|
.trim_start()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_nested_usage() {
|
||||||
|
let doc = proxmox_router::cli::generate_nested_usage(
|
||||||
|
"clicmd",
|
||||||
|
&get_complex_test_cmddef(),
|
||||||
|
DocumentationFormat::ReST,
|
||||||
|
);
|
||||||
|
println!("--- BEGIN EXPECTED DOC OUTPUT ---");
|
||||||
|
println!("{doc}");
|
||||||
|
println!("--- END EXPECTED DOC OUTPUT ---");
|
||||||
|
assert_eq!(doc, expected_nested_usage_text());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_toplevel_help() {
|
||||||
|
let mut help = String::new();
|
||||||
|
proxmox_router::cli::print_help_to(
|
||||||
|
&get_complex_test_cmddef().into(),
|
||||||
|
"clicmd".to_string(),
|
||||||
|
&[],
|
||||||
|
None,
|
||||||
|
&mut help,
|
||||||
|
)
|
||||||
|
.expect("failed to format help string");
|
||||||
|
// println!("--- BEGIN EXPECTED DOC OUTPUT ---");
|
||||||
|
// println!("{help}");
|
||||||
|
// println!("--- END EXPECTED DOC OUTPUT ---");
|
||||||
|
assert_eq!(help, expected_toplevel_help_text());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_group_help() {
|
||||||
|
let mut help = String::new();
|
||||||
|
proxmox_router::cli::print_help_to(
|
||||||
|
&get_complex_test_cmddef().into(),
|
||||||
|
"clicmd".to_string(),
|
||||||
|
&["l0sub".to_string(), "l1c1".to_string()],
|
||||||
|
None,
|
||||||
|
&mut help,
|
||||||
|
)
|
||||||
|
.expect("failed to format help string");
|
||||||
|
// println!("--- BEGIN EXPECTED DOC OUTPUT ---");
|
||||||
|
// println!("{help}");
|
||||||
|
// println!("--- END EXPECTED DOC OUTPUT ---");
|
||||||
|
assert_eq!(help, expected_group_help_text());
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user