From 4b21242cb9ba8bfd6c26f8336d9960838e7ac518 Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Mon, 13 Dec 2021 12:09:44 -0800 Subject: [PATCH 01/13] Adding --help-json to get JSON encoded help message (#1) * Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. * Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. --- argh/src/lib.rs | 21 +++- argh/tests/lib.rs | 148 +++++++++++++++++++++- argh_derive/src/help.rs | 81 +++++++----- argh_derive/src/help_json.rs | 234 +++++++++++++++++++++++++++++++++++ argh_derive/src/lib.rs | 11 +- 5 files changed, 456 insertions(+), 39 deletions(-) create mode 100644 argh_derive/src/help_json.rs diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 984d927..4f12a59 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -258,6 +258,7 @@ pub trait FromArgs: Sized { /// /// Options: /// --help display usage information + /// --help-json display usage information encoded in JSON /// /// Commands: /// list list all the classes. @@ -282,6 +283,7 @@ pub trait FromArgs: Sized { /// Options: /// --teacher-name list classes for only this teacher. /// --help display usage information + /// --help-json display usage information encoded in JSON /// "#.to_string(), /// status: Ok(()), /// }, @@ -424,6 +426,7 @@ pub trait FromArgs: Sized { /// /// Options: /// --help display usage information + /// --help-json display usage information encoded in JSON /// /// Commands: /// list list all the classes. @@ -668,7 +671,8 @@ impl_flag_for_integers![u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,]; /// `parse_options`: Helper to parse optional arguments. /// `parse_positionals`: Helper to parse positional arguments. /// `parse_subcommand`: Helper to parse a subcommand. -/// `help_func`: Generate a help message. +/// `help_func`: Generate a help message as plain text. +/// `help_json_func`: Generate a help message serialized into JSON. #[doc(hidden)] pub fn parse_struct_args( cmd_name: &[&str], @@ -677,19 +681,29 @@ pub fn parse_struct_args( mut parse_positionals: ParseStructPositionals<'_>, mut parse_subcommand: Option>, help_func: &dyn Fn() -> String, + help_json_func: &dyn Fn() -> String, ) -> Result<(), EarlyExit> { let mut help = false; + let mut help_json = false; let mut remaining_args = args; let mut positional_index = 0; let mut options_ended = false; 'parse_args: while let Some(&next_arg) = remaining_args.get(0) { remaining_args = &remaining_args[1..]; + if (next_arg == "--help" || next_arg == "help") && !options_ended { help = true; continue; } + // look for help-json for json formatted help output. + if (next_arg == "--help-json" || next_arg == "help-json") && !options_ended { + help = true; + help_json = true; + continue; + } + if next_arg.starts_with("-") && !options_ended { if next_arg == "--" { options_ended = true; @@ -714,8 +728,9 @@ pub fn parse_struct_args( parse_positionals.parse(&mut positional_index, next_arg)?; } - - if help { + if help_json { + Err(EarlyExit { output: help_json_func(), status: Ok(()) }) + } else if help { Err(EarlyExit { output: help_func(), status: Ok(()) }) } else { Ok(()) diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index fe8c858..d7b7346 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -86,6 +86,105 @@ fn subcommand_example() { assert_eq!(two, TopLevel { nested: MySubCommandEnum::Two(SubCommandTwo { fooey: true }) },); } +#[test] +fn help_json_test_subcommand() { + #[derive(FromArgs, PartialEq, Debug)] + /// Top-level command. + struct TopLevel { + #[argh(subcommand)] + nested: MySubCommandEnum, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum MySubCommandEnum { + One(SubCommandOne), + Two(SubCommandTwo), + } + + #[derive(FromArgs, PartialEq, Debug)] + /// First subcommand. + #[argh(subcommand, name = "one")] + struct SubCommandOne { + #[argh(option)] + /// how many x + x: usize, + } + + #[derive(FromArgs, PartialEq, Debug)] + /// Second subcommand. + #[argh(subcommand, name = "two")] + struct SubCommandTwo { + #[argh(switch)] + /// whether to fooey + fooey: bool, + } + + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Top-level command.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, + {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [{"name": "one", "description": "First subcommand."}, + {"name": "two", "description": "Second subcommand."}] +} +"###, + ); + + assert_help_json_string::( + vec!["one", "--help-json"], + r###"{ +"usage": "test_arg_0 one --x ", +"description": "First subcommand.", +"options": [{"short": "", "long": "--x", "description": "how many x"}, + {"short": "", "long": "--help", "description": "display usage information"}, + {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_multiline_doc_comment() { + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(switch)] + /// a switch with a description + /// that is spread across + /// a number of + /// lines of comments. + _s: bool, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [--s]", +"description": "Short description", +"options": [{"short": "", "long": "--s", "description": "a switch with a description that is spread across a number of lines of comments."}, + {"short": "", "long": "--help", "description": "display usage information"}, + {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + #[test] fn multiline_doc_comment_description() { #[derive(FromArgs)] @@ -108,6 +207,7 @@ Options: --s a switch with a description that is spread across a number of lines of comments. --help display usage information + --help-json display usage information encoded in JSON "###, ); } @@ -195,6 +295,16 @@ fn assert_help_string(help_str: &str) { } } +fn assert_help_json_string(args: Vec<&str>, help_str: &str) { + match T::from_args(&["test_arg_0"], &args) { + Ok(_) => panic!("help-json was parsed as args"), + Err(e) => { + assert_eq!(help_str, e.output); + e.status.expect("help-json returned an error"); + } + } +} + fn assert_output(args: &[&str], expected: T) { let t = T::from_args(&["cmd"], args).expect("failed to parse"); assert_eq!(t, expected); @@ -245,6 +355,7 @@ Woot Options: -n, --n fooey --help display usage information + --help-json display usage information encoded in JSON "###, ); } @@ -267,6 +378,7 @@ Woot Options: --option-name fooey --help display usage information + --help-json display usage information encoded in JSON "###, ); } @@ -305,6 +417,7 @@ Positional Arguments: Options: --help display usage information + --help-json display usage information encoded in JSON "###, ); } @@ -694,6 +807,7 @@ A type for testing `--help`/`help` Options: --help display usage information + --help-json display usage information encoded in JSON Commands: first First subcommmand for testing `help`. @@ -705,6 +819,7 @@ First subcommmand for testing `help`. Options: --help display usage information + --help-json display usage information encoded in JSON Commands: second Second subcommand for testing `help`. @@ -716,6 +831,7 @@ Second subcommand for testing `help`. Options: --help display usage information + --help-json display usage information encoded in JSON "###; #[test] @@ -758,7 +874,7 @@ Options: #[derive(FromArgs, PartialEq, Debug)] #[argh( - description = "Destroy the contents of .", + description = "Destroy the contents of with a specific \"method of destruction\".", example = "Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp", note = "Use `{command_name} help ` for details on [] for a subcommand.", error_code(2, "The blade is too dull."), @@ -851,7 +967,7 @@ Options: assert_help_string::( r###"Usage: test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] [] -Destroy the contents of . +Destroy the contents of with a specific "method of destruction". Options: -f, --force force, ignore minor errors. This description is so long that @@ -861,6 +977,7 @@ Options: -s, --scribble write repeatedly -v, --verbose say more. Defaults to $BLAST_VERBOSE. --help display usage information + --help-json display usage information encoded in JSON Commands: blow-up explosively separate @@ -880,6 +997,31 @@ Error codes: ); } + #[test] + fn help_json_example() { + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] []", +"description": "Destroy the contents of with a specific \"method of destruction\".", +"options": [{"short": "f", "long": "--force", "description": "force, ignore minor errors. This description is so long that it wraps to the next line."}, + {"short": "", "long": "--really-really-really-long-name-for-pat", "description": "documentation"}, + {"short": "s", "long": "--scribble", "description": "write repeatedly"}, + {"short": "v", "long": "--verbose", "description": "say more. Defaults to $BLAST_VERBOSE."}, + {"short": "", "long": "--help", "description": "display usage information"}, + {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "Scribble 'abc' and then run |grind|.\n$ test_arg_0 -s 'abc' grind old.txt taxes.cp", +"notes": "Use `test_arg_0 help ` for details on [] for a subcommand.", +"error_codes": [{"name": "2", "description": "The blade is too dull."}, + {"name": "3", "description": "Out of fuel."}], +"subcommands": [{"name": "blow-up", "description": "explosively separate"}, + {"name": "grind", "description": "make smaller by many small cuts"}] +} +"###, + ); + } + #[allow(dead_code)] #[derive(argh::FromArgs)] /// Destroy the contents of . @@ -900,6 +1042,7 @@ Positional Arguments: Options: --help display usage information + --help-json display usage information encoded in JSON "###, ); } @@ -1263,6 +1406,7 @@ Woot Options: -n, --n fooey --help display usage information + --help-json display usage information encoded in JSON "### .to_string(), status: Ok(()), diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs index 5bf02b1..43c4c2c 100644 --- a/argh_derive/src/help.rs +++ b/argh_derive/src/help.rs @@ -15,51 +15,35 @@ use { const SECTION_SEPARATOR: &str = "\n\n"; +// Define constants for strings used for both help formats. +pub(crate) const HELP_FLAG: &str = "--help"; +pub(crate) const HELP_DESCRIPTION: &str = "display usage information"; +pub(crate) const HELP_JSON_FLAG: &str = "--help-json"; +pub(crate) const HELP_JSON_DESCRIPTION: &str = "display usage information encoded in JSON"; + /// Returns a `TokenStream` generating a `String` help message. /// /// Note: `fields` entries with `is_subcommand.is_some()` will be ignored /// in favor of the `subcommand` argument. pub(crate) fn help( errors: &Errors, - cmd_name_str_array_ident: syn::Ident, + cmd_name_str_array_ident: &syn::Ident, ty_attrs: &TypeAttrs, fields: &[StructField<'_>], subcommand: Option<&StructField<'_>>, ) -> TokenStream { let mut format_lit = "Usage: {command_name}".to_string(); - let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional); - let mut has_positional = false; - for arg in positional.clone() { - has_positional = true; - format_lit.push(' '); - positional_usage(&mut format_lit, arg); - } - - let options = fields.iter().filter(|f| f.long_name.is_some()); - for option in options.clone() { - format_lit.push(' '); - option_usage(&mut format_lit, option); - } - - if let Some(subcommand) = subcommand { - format_lit.push(' '); - if !subcommand.optionality.is_required() { - format_lit.push('['); - } - format_lit.push_str(""); - if !subcommand.optionality.is_required() { - format_lit.push(']'); - } - format_lit.push_str(" []"); - } + build_usage_command_line(&mut format_lit, fields, subcommand); format_lit.push_str(SECTION_SEPARATOR); let description = require_description(errors, Span::call_site(), &ty_attrs.description, "type"); format_lit.push_str(&description); - if has_positional { + let mut positional = fields.iter().filter(|f| f.kind == FieldKind::Positional).peekable(); + + if positional.peek().is_some() { format_lit.push_str(SECTION_SEPARATOR); format_lit.push_str("Positional Arguments:"); for arg in positional { @@ -69,11 +53,13 @@ pub(crate) fn help( format_lit.push_str(SECTION_SEPARATOR); format_lit.push_str("Options:"); + let options = fields.iter().filter(|f| f.long_name.is_some()); for option in options { option_description(errors, &mut format_lit, option); } - // Also include "help" - option_description_format(&mut format_lit, None, "--help", "display usage information"); + // Also include "help" and "help-json" + option_description_format(&mut format_lit, None, HELP_FLAG, HELP_DESCRIPTION); + option_description_format(&mut format_lit, None, HELP_JSON_FLAG, HELP_JSON_DESCRIPTION); let subcommand_calculation; let subcommand_format_arg; @@ -96,7 +82,7 @@ pub(crate) fn help( lits_section(&mut format_lit, "Notes:", &ty_attrs.notes); - if ty_attrs.error_codes.len() != 0 { + if !ty_attrs.error_codes.is_empty() { format_lit.push_str(SECTION_SEPARATOR); format_lit.push_str("Error codes:"); for (code, text) in &ty_attrs.error_codes { @@ -106,7 +92,7 @@ pub(crate) fn help( } } - format_lit.push_str("\n"); + format_lit.push('\n'); quote! { { #subcommand_calculation @@ -116,7 +102,7 @@ pub(crate) fn help( /// A section composed of exactly just the literals provided to the program. fn lits_section(out: &mut String, heading: &str, lits: &[syn::LitStr]) { - if lits.len() != 0 { + if !lits.is_empty() { out.push_str(SECTION_SEPARATOR); out.push_str(heading); for lit in lits { @@ -252,3 +238,34 @@ fn option_description_format( let info = argh_shared::CommandInfo { name: &*name, description }; argh_shared::write_description(out, &info); } + +/// Builds the usage description command line and appends it to "out". +pub(crate) fn build_usage_command_line( + out: &mut String, + fields: &[StructField<'_>], + subcommand: Option<&StructField<'_>>, +) { + let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional); + for arg in positional.clone() { + out.push(' '); + positional_usage(out, arg); + } + + let options = fields.iter().filter(|f| f.long_name.is_some()); + for option in options.clone() { + out.push(' '); + option_usage(out, option); + } + + if let Some(subcommand) = subcommand { + out.push(' '); + if !subcommand.optionality.is_required() { + out.push('['); + } + out.push_str(""); + if !subcommand.optionality.is_required() { + out.push(']'); + } + out.push_str(" []"); + } +} diff --git a/argh_derive/src/help_json.rs b/argh_derive/src/help_json.rs new file mode 100644 index 0000000..72c6e86 --- /dev/null +++ b/argh_derive/src/help_json.rs @@ -0,0 +1,234 @@ +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +use { + crate::{ + errors::Errors, + help::{ + build_usage_command_line, require_description, HELP_DESCRIPTION, HELP_FLAG, + HELP_JSON_DESCRIPTION, HELP_JSON_FLAG, + }, + parse_attrs::{FieldKind, TypeAttrs}, + StructField, + }, + proc_macro2::{Span, TokenStream}, + quote::quote, +}; + +struct OptionHelp { + short: String, + long: String, + description: String, +} + +struct PositionalHelp { + name: String, + description: String, +} +struct HelpJSON { + usage: String, + description: String, + positional_args: Vec, + options: Vec, + examples: String, + notes: String, + error_codes: Vec, +} + +fn option_elements_json(options: &[OptionHelp]) -> String { + let mut retval = String::from(""); + for opt in options { + if !retval.is_empty() { + retval.push_str(",\n "); + } + retval.push_str(&format!( + "{{\"short\": \"{}\", \"long\": \"{}\", \"description\": \"{}\"}}", + opt.short, + opt.long, + escape_json(&opt.description) + )); + } + retval +} +fn help_elements_json(elements: &[PositionalHelp]) -> String { + let mut retval = String::from(""); + for pos in elements { + if !retval.is_empty() { + retval.push_str(",\n "); + } + retval.push_str(&format!( + "{{\"name\": \"{}\", \"description\": \"{}\"}}", + pos.name, + escape_json(&pos.description) + )); + } + retval +} + +/// Returns a `TokenStream` generating a `String` help message containing JSON. +/// +/// Note: `fields` entries with `is_subcommand.is_some()` will be ignored +/// in favor of the `subcommand` argument. +pub(crate) fn help_json( + errors: &Errors, + cmd_name_str_array_ident: &syn::Ident, + ty_attrs: &TypeAttrs, + fields: &[StructField<'_>], + subcommand: Option<&StructField<'_>>, +) -> TokenStream { + let mut usage_format_pattern = "{command_name}".to_string(); + build_usage_command_line(&mut usage_format_pattern, fields, subcommand); + + let mut help_obj = HelpJSON { + usage: String::from(""), + description: String::from(""), + positional_args: vec![], + options: vec![], + examples: String::from(""), + notes: String::from(""), + error_codes: vec![], + }; + + // Add positional args to the help object. + let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional); + for arg in positional { + let mut description = String::from(""); + if let Some(desc) = &arg.attrs.description { + description = desc.content.value().trim().to_owned(); + } + help_obj.positional_args.push(PositionalHelp { name: arg.arg_name(), description }); + } + + // Add options to the help object. + let options = fields.iter().filter(|f| f.long_name.is_some()); + for option in options { + let short = match option.attrs.short.as_ref().map(|s| s.value()) { + Some(c) => String::from(c), + None => String::from(""), + }; + let long_with_leading_dashes = + option.long_name.as_ref().expect("missing long name for option"); + let description = + require_description(errors, option.name.span(), &option.attrs.description, "field"); + help_obj.options.push(OptionHelp { + short, + long: long_with_leading_dashes.to_owned(), + description, + }); + } + // Also include "help" and "help-json" + help_obj.options.push(OptionHelp { + short: String::from(""), + long: String::from(HELP_FLAG), + description: String::from(HELP_DESCRIPTION), + }); + help_obj.options.push(OptionHelp { + short: String::from(""), + long: String::from(HELP_JSON_FLAG), + description: String::from(HELP_JSON_DESCRIPTION), + }); + + let subcommand_calculation; + if let Some(subcommand) = subcommand { + let subcommand_ty = subcommand.ty_without_wrapper; + subcommand_calculation = quote! { + let mut subcommands = String::from(""); + for cmd in <#subcommand_ty as argh::SubCommands>::COMMANDS { + if !subcommands.is_empty() { + subcommands.push_str(",\n "); + } + subcommands.push_str(&format!("{{\"name\": \"{}\", \"description\": \"{}\"}}", + cmd.name, cmd.description)); + } + }; + } else { + subcommand_calculation = quote! { + let subcommands = String::from(""); + }; + } + + help_obj.usage = usage_format_pattern.clone(); + + help_obj.description = + require_description(errors, Span::call_site(), &ty_attrs.description, "type"); + + let mut example: String = String::from(""); + for lit in &ty_attrs.examples { + example.push_str(&lit.value()); + } + help_obj.examples = example; + + let mut note: String = String::from(""); + for lit in &ty_attrs.notes { + note.push_str(&lit.value()); + } + help_obj.notes = note; + + if !ty_attrs.error_codes.is_empty() { + for (code, text) in &ty_attrs.error_codes { + help_obj.error_codes.push(PositionalHelp { + name: code.to_string(), + description: escape_json(&text.value().to_string()), + }); + } + } + + let help_options_json = option_elements_json(&help_obj.options); + let help_positional_json = help_elements_json(&help_obj.positional_args); + let help_error_codes_json = help_elements_json(&help_obj.error_codes); + + let help_description = escape_json(&help_obj.description); + let help_examples: TokenStream; + let help_notes: TokenStream; + + let notes_pattern = escape_json(&help_obj.notes); + // check if we need to interpolate the string. + if notes_pattern.contains("{command_name}") { + help_notes = quote! { + json_help_string.push_str(&format!(#notes_pattern,command_name = #cmd_name_str_array_ident.join(" "))); + }; + } else { + help_notes = quote! { + json_help_string.push_str(#notes_pattern); + }; + } + let examples_pattern = escape_json(&help_obj.examples); + if examples_pattern.contains("{command_name}") { + help_examples = quote! { + json_help_string.push_str(&format!(#examples_pattern,command_name = #cmd_name_str_array_ident.join(" "))); + }; + } else { + help_examples = quote! { + json_help_string.push_str(#examples_pattern); + }; + } + + quote! {{ + #subcommand_calculation + + // Build up the string for json. The name of the command needs to be dereferenced, so it + // can't be done in the macro. + let mut json_help_string = "{\n".to_string(); + let usage_value = format!(#usage_format_pattern,command_name = #cmd_name_str_array_ident.join(" ")); + json_help_string.push_str(&format!("\"usage\": \"{}\",\n",usage_value)); + json_help_string.push_str(&format!("\"description\": \"{}\",\n", #help_description)); + json_help_string.push_str(&format!("\"options\": [{}],\n", #help_options_json)); + json_help_string.push_str(&format!("\"positional\": [{}],\n", #help_positional_json)); + json_help_string.push_str("\"examples\": \""); + #help_examples; + json_help_string.push_str("\",\n"); + json_help_string.push_str("\"notes\": \""); + #help_notes; + json_help_string.push_str("\",\n"); + json_help_string.push_str(&format!("\"error_codes\": [{}],\n", #help_error_codes_json)); + json_help_string.push_str(&format!("\"subcommands\": [{}]\n", subcommands)); + json_help_string.push_str("}\n"); + json_help_string + }} +} + +/// Escape characters in strings to be JSON compatible. +fn escape_json(value: &str) -> String { + value.replace("\n", r#"\n"#).replace("\"", r#"\""#) +} diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index a123d4e..f0e894b 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -21,6 +21,7 @@ use { mod errors; mod help; +mod help_json; mod parse_attrs; /// Entrypoint for `#[derive(FromArgs)]`. @@ -324,7 +325,9 @@ fn impl_from_args_struct_from_args<'a>( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); - let help = help::help(errors, cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help = help::help(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help_json = + help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); let method_impl = quote_spanned! { impl_span => fn from_args(__cmd_name: &[&str], __args: &[&str]) @@ -352,6 +355,7 @@ fn impl_from_args_struct_from_args<'a>( }, #parse_subcommands, &|| #help, + &|| #help_json )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); @@ -436,7 +440,9 @@ fn impl_from_args_struct_redact_arg_values<'a>( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); - let help = help::help(errors, cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help = help::help(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help_json = + help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); let method_impl = quote_spanned! { impl_span => fn redact_arg_values(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result, argh::EarlyExit> { @@ -462,6 +468,7 @@ fn impl_from_args_struct_redact_arg_values<'a>( }, #redact_subcommands, &|| #help, + &|| #help_json )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); From 56f4a211aec6d755c496526dfbd15be62265913f Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Tue, 14 Dec 2021 13:55:58 -0800 Subject: [PATCH 02/13] Adding --help-json to get JSON encoded help message (#2) * Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. * Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. * Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. --- README.md | 44 +++ argh/tests/help_json_tests.rs | 640 ++++++++++++++++++++++++++++++++++ argh/tests/lib.rs | 29 +- argh_derive/src/help_json.rs | 8 +- 4 files changed, 691 insertions(+), 30 deletions(-) create mode 100644 argh/tests/help_json_tests.rs diff --git a/README.md b/README.md index 4e949e4..3f71234 100644 --- a/README.md +++ b/README.md @@ -174,4 +174,48 @@ struct SubCommandTwo { } ``` +## Attribute Summary +### Type attributes for `argh` + +The attributes used to configure the argh information for a type are defined in +[parse_attrs::TypeAttrs](argh_derive/src/parse_attrs.rs). + +* `subcommand` - a subcommand type. This attribute must appear on both enumeration and each struct that + is a variant for the enumerated subcommand. +* `error_code(code, description)` - an error code for the command. This attribute can appear zero + or more times. +* `examples=` - Formatted text containing examples of how to use this command. This + is an optional attribute. +* `name=` - (required for subcommand variant) the name of the subcommand. +* `notes=` - Formatted text containing usage notes for this command. This + is an optional attribute. + pub error_codes: Vec<(syn::LitInt, syn::LitStr)>, + +### Field attributes for `argh` + +The attributes used to configure the argh information for a field are +defined in [parse_attrs.rs](argh_derive/src/parse_attrs.rs). + +* Field kind. This is the first attribute. Valid kinds are: + * `switch` - a boolean flag, its presence on the command sets the field to `true`. + * `option` - a value. This can be a simple type like String, or usize, and enumeration. + This can be a scalar or Vec<> for repeated values. + * `subcommand` - a subcommand. The type of this field is an enumeration with a value for each + subcommand. This attribute must appear on both the "top level" field and each struct that + is a variant for the enumerated subcommand. + * `positional` - a positional argument. This can be scalar or Vec<>. Only the last positional + argument can be Option<>, Vec<>, or defaulted. +* `arg_name=` - the name to use for a positional argument in the help or the value of a `option`. + If not given, the default is the name of the field. +* `default=` - the default value for the `option` or `positional` fields. +* `description=` - the description of the flag or argument. The default value is the doc comment + for the field. +* `from_str_fn` is the name of a custom deserialization function for this field with the signature: + `fn(&str) -> Result`. +* `long=` - the long format of the option or switch name. If `long` is not present, the + flag name defaults to the field name. +* `short=` - the single character for this flag. If `short` is not present, there is no + short equivalent flag. + + NOTE: This is not an officially supported Google product. diff --git a/argh/tests/help_json_tests.rs b/argh/tests/help_json_tests.rs new file mode 100644 index 0000000..cae34fd --- /dev/null +++ b/argh/tests/help_json_tests.rs @@ -0,0 +1,640 @@ +#![cfg(test)] +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +use std::path::PathBuf; +use {argh::FromArgs, std::fmt::Debug}; + +#[test] +fn help_json_test_subcommand() { + #[derive(FromArgs, PartialEq, Debug)] + /// Top-level command. + struct TopLevel { + #[argh(subcommand)] + nested: MySubCommandEnum, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum MySubCommandEnum { + One(SubCommandOne), + Two(SubCommandTwo), + } + + #[derive(FromArgs, PartialEq, Debug)] + /// First subcommand. + #[argh(subcommand, name = "one")] + struct SubCommandOne { + #[argh(option)] + /// how many x + x: usize, + } + + #[derive(FromArgs, PartialEq, Debug)] + /// Second subcommand. + #[argh(subcommand, name = "two")] + struct SubCommandTwo { + #[argh(switch)] + /// whether to fooey + fooey: bool, + } + + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Top-level command.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [{"name": "one", "description": "First subcommand."}, +{"name": "two", "description": "Second subcommand."}] +} +"###, + ); + + assert_help_json_string::( + vec!["one", "--help-json"], + r###"{ +"usage": "test_arg_0 one --x ", +"description": "First subcommand.", +"options": [{"short": "", "long": "--x", "description": "how many x"}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_multiline_doc_comment() { + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(switch)] + /// a switch with a description + /// that is spread across + /// a number of + /// lines of comments. + _s: bool, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [--s]", +"description": "Short description", +"options": [{"short": "", "long": "--s", "description": "a switch with a description that is spread across a number of lines of comments."}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_basic_args() { + #[allow(dead_code)] + #[derive(FromArgs)] + /// Basic command args demonstrating multiple types and cardinality. "With quotes" + struct Basic { + /// should the power be on. "Quoted value" should work too. + #[argh(switch)] + power: bool, + + /// option that is required because of no default and not Option<>. + #[argh(option, long = "required")] + required_flag: String, + + /// optional speed if not specified it is None. + #[argh(option, short = 's')] + speed: Option, + + /// repeatable option. + #[argh(option, arg_name = "url")] + link: Vec, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [--power] --required [-s ] [--link ]", +"description": "Basic command args demonstrating multiple types and cardinality. \"With quotes\"", +"options": [{"short": "", "long": "--power", "description": "should the power be on. \"Quoted value\" should work too."}, +{"short": "", "long": "--required", "description": "option that is required because of no default and not Option<>."}, +{"short": "s", "long": "--speed", "description": "optional speed if not specified it is None."}, +{"short": "", "long": "--link", "description": "repeatable option."}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_positional_args() { + #[allow(dead_code)] + #[derive(FromArgs)] + /// Command with positional args demonstrating. "With quotes" + struct Positional { + /// the "root" position. + #[argh(positional, arg_name = "root")] + root_value: String, + + /// trunk value + #[argh(positional)] + trunk: String, + + /// leaves. There can be many leaves. + #[argh(positional)] + leaves: Vec, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Command with positional args demonstrating. \"With quotes\"", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "root", "description": "the \"root\" position."}, +{"name": "trunk", "description": "trunk value"}, +{"name": "leaves", "description": "leaves. There can be many leaves."}], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_optional_positional_args() { + #[allow(dead_code)] + #[derive(FromArgs)] + /// Command with positional args demonstrating last value is optional + struct Positional { + /// the "root" position. + #[argh(positional, arg_name = "root")] + root_value: String, + + /// trunk value + #[argh(positional)] + trunk: String, + + /// leaves. There can be many leaves. + #[argh(positional)] + leaves: Option, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Command with positional args demonstrating last value is optional", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "root", "description": "the \"root\" position."}, +{"name": "trunk", "description": "trunk value"}, +{"name": "leaves", "description": "leaves. There can be many leaves."}], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_default_positional_args() { + #[allow(dead_code)] + #[derive(FromArgs)] + /// Command with positional args demonstrating last value is defaulted. + struct Positional { + /// the "root" position. + #[argh(positional, arg_name = "root")] + root_value: String, + + /// trunk value + #[argh(positional)] + trunk: String, + + /// leaves. There can be many leaves. + #[argh(positional, default = "String::from(\"hello\")")] + leaves: String, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Command with positional args demonstrating last value is defaulted.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "root", "description": "the \"root\" position."}, +{"name": "trunk", "description": "trunk value"}, +{"name": "leaves", "description": "leaves. There can be many leaves."}], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_notes_examples_errors() { + #[allow(dead_code)] + #[derive(FromArgs)] + /// Command with Examples and usage Notes, including error codes. + #[argh( + note = r##" + These usage notes appear for {command_name} and how to best use it. + The formatting should be preserved. + one + two + three then a blank + + and one last line with "quoted text"."##, + example = r##" + Use the command with 1 file: + + `{command_name} /path/to/file` + + Use it with a "wildcard": + + `{command_name} /path/to/*` + + a blank line + + and one last line with "quoted text"."##, + error_code(0, "Success"), + error_code(1, "General Error"), + error_code(2, "Some error with \"quotes\"") + )] + struct NotesExamplesErrors { + /// the "root" position. + #[argh(positional, arg_name = "files")] + fields: Vec, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Command with Examples and usage Notes, including error codes.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "files", "description": "the \"root\" position."}], +"examples": "\n Use the command with 1 file:\n\n `test_arg_0 /path/to/file`\n\n Use it with a \"wildcard\":\n\n `test_arg_0 /path/to/*`\n\n a blank line\n \n and one last line with \"quoted text\".", +"notes": "\n These usage notes appear for test_arg_0 and how to best use it.\n The formatting should be preserved.\n one\n two\n three then a blank\n \n and one last line with \"quoted text\".", +"error_codes": [{"name": "0", "description": "Success"}, +{"name": "1", "description": "General Error"}, +{"name": "2", "description": "Some error with \"quotes\""}], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_subcommands() { + #[allow(dead_code)] + #[derive(FromArgs)] + ///Top level command with "subcommands". + struct TopLevel { + /// show verbose output + #[argh(switch)] + verbose: bool, + + /// this doc comment does not appear anywhere. + #[argh(subcommand)] + cmd: SubcommandEnum, + } + + #[derive(FromArgs)] + #[argh(subcommand)] + /// Doc comments for subcommand enums does not appear in the help text. + enum SubcommandEnum { + Command1(Command1Args), + Command2(Command2Args), + Command3(Command3Args), + } + + /// Command1 args are used for Command1. + #[allow(dead_code)] + #[derive(FromArgs)] + #[argh(subcommand, name = "one")] + struct Command1Args { + /// the "root" position. + #[argh(positional, arg_name = "root")] + root_value: String, + + /// trunk value + #[argh(positional)] + trunk: String, + + /// leaves. There can be many leaves. + #[argh(positional, default = "String::from(\"hello\")")] + leaves: String, + } + /// Command2 args are used for Command2. + #[allow(dead_code)] + #[derive(FromArgs)] + #[argh(subcommand, name = "two")] + struct Command2Args { + /// should the power be on. "Quoted value" should work too. + #[argh(switch)] + power: bool, + + /// option that is required because of no default and not Option<>. + #[argh(option, long = "required")] + required_flag: String, + + /// optional speed if not specified it is None. + #[argh(option, short = 's')] + speed: Option, + + /// repeatable option. + #[argh(option, arg_name = "url")] + link: Vec, + } + /// Command3 args are used for Command3 which has no options or arguments. + #[derive(FromArgs)] + #[argh(subcommand, name = "three")] + struct Command3Args {} + + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [--verbose] []", +"description": "Top level command with \"subcommands\".", +"options": [{"short": "", "long": "--verbose", "description": "show verbose output"}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [{"name": "one", "description": "Command1 args are used for Command1."}, +{"name": "two", "description": "Command2 args are used for Command2."}, +{"name": "three", "description": "Command3 args are used for Command3 which has no options or arguments."}] +} +"###, + ); + + assert_help_json_string::( + vec!["one", "--help-json"], + r###"{ +"usage": "test_arg_0 one []", +"description": "Command1 args are used for Command1.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "root", "description": "the \"root\" position."}, +{"name": "trunk", "description": "trunk value"}, +{"name": "leaves", "description": "leaves. There can be many leaves."}], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); + + assert_help_json_string::( + vec!["two", "--help-json"], + r###"{ +"usage": "test_arg_0 two [--power] --required [-s ] [--link ]", +"description": "Command2 args are used for Command2.", +"options": [{"short": "", "long": "--power", "description": "should the power be on. \"Quoted value\" should work too."}, +{"short": "", "long": "--required", "description": "option that is required because of no default and not Option<>."}, +{"short": "s", "long": "--speed", "description": "optional speed if not specified it is None."}, +{"short": "", "long": "--link", "description": "repeatable option."}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); + + assert_help_json_string::( + vec!["three", "--help-json"], + r###"{ +"usage": "test_arg_0 three", +"description": "Command3 args are used for Command3 which has no options or arguments.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_subcommand_notes_examples() { + #[allow(dead_code)] + #[derive(FromArgs)] + ///Top level command with "subcommands". + #[argh( + note = "Top level note", + example = "Top level example", + error_code(0, "Top level success") + )] + struct TopLevel { + /// show verbose output + #[argh(switch)] + verbose: bool, + + /// this doc comment does not appear anywhere. + #[argh(subcommand)] + cmd: SubcommandEnum, + } + + #[derive(FromArgs)] + #[argh(subcommand)] + /// Doc comments for subcommand enums does not appear in the help text. + enum SubcommandEnum { + Command1(Command1Args), + } + + /// Command1 args are used for subcommand one. + #[allow(dead_code)] + #[derive(FromArgs)] + #[argh( + subcommand, + name = "one", + note = "{command_name} is used as a subcommand of \"Top level\"", + example = "\"Typical\" usage is `{command_name}`.", + error_code(0, "one level success") + )] + struct Command1Args { + /// the "root" position. + #[argh(positional, arg_name = "root")] + root_value: String, + + /// trunk value + #[argh(positional)] + trunk: String, + + /// leaves. There can be many leaves. + #[argh(positional, default = "String::from(\"hello\")")] + leaves: String, + } + + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [--verbose] []", +"description": "Top level command with \"subcommands\".", +"options": [{"short": "", "long": "--verbose", "description": "show verbose output"}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "Top level example", +"notes": "Top level note", +"error_codes": [{"name": "0", "description": "Top level success"}], +"subcommands": [{"name": "one", "description": "Command1 args are used for subcommand one."}] +} +"###, + ); + + assert_help_json_string::( + vec!["one", "--help-json"], + r###"{ +"usage": "test_arg_0 one []", +"description": "Command1 args are used for subcommand one.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "root", "description": "the \"root\" position."}, +{"name": "trunk", "description": "trunk value"}, +{"name": "leaves", "description": "leaves. There can be many leaves."}], +"examples": "\"Typical\" usage is `test_arg_0 one`.", +"notes": "test_arg_0 one is used as a subcommand of \"Top level\"", +"error_codes": [{"name": "0", "description": "one level success"}], +"subcommands": [] +} +"###, + ); +} + +/// Test that descriptions can start with an initialism despite +/// usually being required to start with a lowercase letter. +#[derive(FromArgs)] +#[allow(unused)] +struct DescriptionStartsWithInitialism { + /// URL fooey + #[argh(option)] + x: u8, +} + +#[test] +fn help_json_test_example() { + #[derive(FromArgs, PartialEq, Debug)] + #[argh( + description = "Destroy the contents of with a specific \"method of destruction\".", + example = "Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp", + note = "Use `{command_name} help ` for details on [] for a subcommand.", + error_code(2, "The blade is too dull."), + error_code(3, "Out of fuel.") + )] + struct HelpExample { + /// force, ignore minor errors. This description is so long that it wraps to the next line. + #[argh(switch, short = 'f')] + force: bool, + + /// documentation + #[argh(switch)] + really_really_really_long_name_for_pat: bool, + + /// write repeatedly + #[argh(option, short = 's')] + scribble: String, + + /// say more. Defaults to $BLAST_VERBOSE. + #[argh(switch, short = 'v')] + verbose: bool, + + #[argh(subcommand)] + command: HelpExampleSubCommands, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum HelpExampleSubCommands { + BlowUp(BlowUp), + Grind(GrindCommand), + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand, name = "blow-up")] + /// explosively separate + struct BlowUp { + /// blow up bombs safely + #[argh(switch)] + safely: bool, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand, name = "grind", description = "make smaller by many small cuts")] + struct GrindCommand { + /// wear a visor while grinding + #[argh(switch)] + safely: bool, + } + + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] []", +"description": "Destroy the contents of with a specific \"method of destruction\".", +"options": [{"short": "f", "long": "--force", "description": "force, ignore minor errors. This description is so long that it wraps to the next line."}, +{"short": "", "long": "--really-really-really-long-name-for-pat", "description": "documentation"}, +{"short": "s", "long": "--scribble", "description": "write repeatedly"}, +{"short": "v", "long": "--verbose", "description": "say more. Defaults to $BLAST_VERBOSE."}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "Scribble 'abc' and then run |grind|.\n$ test_arg_0 -s 'abc' grind old.txt taxes.cp", +"notes": "Use `test_arg_0 help ` for details on [] for a subcommand.", +"error_codes": [{"name": "2", "description": "The blade is too dull."}, +{"name": "3", "description": "Out of fuel."}], +"subcommands": [{"name": "blow-up", "description": "explosively separate"}, +{"name": "grind", "description": "make smaller by many small cuts"}] +} +"###, + ); +} + +fn assert_help_json_string(args: Vec<&str>, help_str: &str) { + match T::from_args(&["test_arg_0"], &args) { + Ok(_) => panic!("help-json was parsed as args"), + Err(e) => { + assert_eq!(help_str, e.output); + e.status.expect("help-json returned an error"); + } + } +} diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index d7b7346..948d944 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -5,6 +5,8 @@ use {argh::FromArgs, std::fmt::Debug}; +mod help_json_tests; + #[test] fn basic_example() { #[derive(FromArgs, PartialEq, Debug)] @@ -996,32 +998,7 @@ Error codes: "###, ); } - - #[test] - fn help_json_example() { - assert_help_json_string::( - vec!["--help-json"], - r###"{ -"usage": "test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] []", -"description": "Destroy the contents of with a specific \"method of destruction\".", -"options": [{"short": "f", "long": "--force", "description": "force, ignore minor errors. This description is so long that it wraps to the next line."}, - {"short": "", "long": "--really-really-really-long-name-for-pat", "description": "documentation"}, - {"short": "s", "long": "--scribble", "description": "write repeatedly"}, - {"short": "v", "long": "--verbose", "description": "say more. Defaults to $BLAST_VERBOSE."}, - {"short": "", "long": "--help", "description": "display usage information"}, - {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], -"positional": [], -"examples": "Scribble 'abc' and then run |grind|.\n$ test_arg_0 -s 'abc' grind old.txt taxes.cp", -"notes": "Use `test_arg_0 help ` for details on [] for a subcommand.", -"error_codes": [{"name": "2", "description": "The blade is too dull."}, - {"name": "3", "description": "Out of fuel."}], -"subcommands": [{"name": "blow-up", "description": "explosively separate"}, - {"name": "grind", "description": "make smaller by many small cuts"}] -} -"###, - ); - } - + #[allow(dead_code)] #[derive(argh::FromArgs)] /// Destroy the contents of . diff --git a/argh_derive/src/help_json.rs b/argh_derive/src/help_json.rs index 72c6e86..a84102a 100644 --- a/argh_derive/src/help_json.rs +++ b/argh_derive/src/help_json.rs @@ -40,7 +40,7 @@ fn option_elements_json(options: &[OptionHelp]) -> String { let mut retval = String::from(""); for opt in options { if !retval.is_empty() { - retval.push_str(",\n "); + retval.push_str(",\n"); } retval.push_str(&format!( "{{\"short\": \"{}\", \"long\": \"{}\", \"description\": \"{}\"}}", @@ -55,7 +55,7 @@ fn help_elements_json(elements: &[PositionalHelp]) -> String { let mut retval = String::from(""); for pos in elements { if !retval.is_empty() { - retval.push_str(",\n "); + retval.push_str(",\n"); } retval.push_str(&format!( "{{\"name\": \"{}\", \"description\": \"{}\"}}", @@ -136,7 +136,7 @@ pub(crate) fn help_json( let mut subcommands = String::from(""); for cmd in <#subcommand_ty as argh::SubCommands>::COMMANDS { if !subcommands.is_empty() { - subcommands.push_str(",\n "); + subcommands.push_str(",\n"); } subcommands.push_str(&format!("{{\"name\": \"{}\", \"description\": \"{}\"}}", cmd.name, cmd.description)); @@ -169,7 +169,7 @@ pub(crate) fn help_json( for (code, text) in &ty_attrs.error_codes { help_obj.error_codes.push(PositionalHelp { name: code.to_string(), - description: escape_json(&text.value().to_string()), + description: text.value().to_string(), }); } } From 287703bf7aaec795450ab2b746c8bded67ac4e13 Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Mon, 13 Dec 2021 10:43:23 -0800 Subject: [PATCH 03/13] Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. --- argh/tests/lib.rs | 111 +---------------------------------- argh_derive/src/help_json.rs | 2 +- 2 files changed, 2 insertions(+), 111 deletions(-) diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index 948d944..6f59ec6 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -88,105 +88,6 @@ fn subcommand_example() { assert_eq!(two, TopLevel { nested: MySubCommandEnum::Two(SubCommandTwo { fooey: true }) },); } -#[test] -fn help_json_test_subcommand() { - #[derive(FromArgs, PartialEq, Debug)] - /// Top-level command. - struct TopLevel { - #[argh(subcommand)] - nested: MySubCommandEnum, - } - - #[derive(FromArgs, PartialEq, Debug)] - #[argh(subcommand)] - enum MySubCommandEnum { - One(SubCommandOne), - Two(SubCommandTwo), - } - - #[derive(FromArgs, PartialEq, Debug)] - /// First subcommand. - #[argh(subcommand, name = "one")] - struct SubCommandOne { - #[argh(option)] - /// how many x - x: usize, - } - - #[derive(FromArgs, PartialEq, Debug)] - /// Second subcommand. - #[argh(subcommand, name = "two")] - struct SubCommandTwo { - #[argh(switch)] - /// whether to fooey - fooey: bool, - } - - assert_help_json_string::( - vec!["--help-json"], - r###"{ -"usage": "test_arg_0 []", -"description": "Top-level command.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}, - {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], -"positional": [], -"examples": "", -"notes": "", -"error_codes": [], -"subcommands": [{"name": "one", "description": "First subcommand."}, - {"name": "two", "description": "Second subcommand."}] -} -"###, - ); - - assert_help_json_string::( - vec!["one", "--help-json"], - r###"{ -"usage": "test_arg_0 one --x ", -"description": "First subcommand.", -"options": [{"short": "", "long": "--x", "description": "how many x"}, - {"short": "", "long": "--help", "description": "display usage information"}, - {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], -"positional": [], -"examples": "", -"notes": "", -"error_codes": [], -"subcommands": [] -} -"###, - ); -} - -#[test] -fn help_json_test_multiline_doc_comment() { - #[derive(FromArgs)] - /// Short description - struct Cmd { - #[argh(switch)] - /// a switch with a description - /// that is spread across - /// a number of - /// lines of comments. - _s: bool, - } - assert_help_json_string::( - vec!["--help-json"], - r###"{ -"usage": "test_arg_0 [--s]", -"description": "Short description", -"options": [{"short": "", "long": "--s", "description": "a switch with a description that is spread across a number of lines of comments."}, - {"short": "", "long": "--help", "description": "display usage information"}, - {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], -"positional": [], -"examples": "", -"notes": "", -"error_codes": [], -"subcommands": [] -} -"###, - ); -} - #[test] fn multiline_doc_comment_description() { #[derive(FromArgs)] @@ -297,16 +198,6 @@ fn assert_help_string(help_str: &str) { } } -fn assert_help_json_string(args: Vec<&str>, help_str: &str) { - match T::from_args(&["test_arg_0"], &args) { - Ok(_) => panic!("help-json was parsed as args"), - Err(e) => { - assert_eq!(help_str, e.output); - e.status.expect("help-json returned an error"); - } - } -} - fn assert_output(args: &[&str], expected: T) { let t = T::from_args(&["cmd"], args).expect("failed to parse"); assert_eq!(t, expected); @@ -998,7 +889,7 @@ Error codes: "###, ); } - + #[allow(dead_code)] #[derive(argh::FromArgs)] /// Destroy the contents of . diff --git a/argh_derive/src/help_json.rs b/argh_derive/src/help_json.rs index a84102a..b5e095d 100644 --- a/argh_derive/src/help_json.rs +++ b/argh_derive/src/help_json.rs @@ -40,7 +40,7 @@ fn option_elements_json(options: &[OptionHelp]) -> String { let mut retval = String::from(""); for opt in options { if !retval.is_empty() { - retval.push_str(",\n"); + retval.push_str(",\n"); } retval.push_str(&format!( "{{\"short\": \"{}\", \"long\": \"{}\", \"description\": \"{}\"}}", From 767fc8de22572e65969731139d558c49dff7d0b9 Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Mon, 13 Dec 2021 12:09:44 -0800 Subject: [PATCH 04/13] Adding --help-json to get JSON encoded help message (#1) * Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. * Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. --- argh/src/lib.rs | 14 ++- argh/tests/lib.rs | 148 +++++++++++++++++++++- argh_derive/src/help.rs | 75 ++++++----- argh_derive/src/help_json.rs | 234 +++++++++++++++++++++++++++++++++++ argh_derive/src/lib.rs | 11 +- 5 files changed, 446 insertions(+), 36 deletions(-) create mode 100644 argh_derive/src/help_json.rs diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 1bd6ba9..b3c0d60 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -256,6 +256,7 @@ pub trait FromArgs: Sized { /// /// Options: /// --help display usage information + /// --help-json display usage information encoded in JSON /// /// Commands: /// list list all the classes. @@ -280,6 +281,7 @@ pub trait FromArgs: Sized { /// Options: /// --teacher-name list classes for only this teacher. /// --help display usage information + /// --help-json display usage information encoded in JSON /// "#.to_string(), /// status: Ok(()), /// }, @@ -422,6 +424,7 @@ pub trait FromArgs: Sized { /// /// Options: /// --help display usage information + /// --help-json display usage information encoded in JSON /// /// Commands: /// list list all the classes. @@ -666,7 +669,8 @@ impl_flag_for_integers![u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,]; /// `parse_options`: Helper to parse optional arguments. /// `parse_positionals`: Helper to parse positional arguments. /// `parse_subcommand`: Helper to parse a subcommand. -/// `help_func`: Generate a help message. +/// `help_func`: Generate a help message as plain text. +/// `help_json_func`: Generate a help message serialized into JSON. #[doc(hidden)] pub fn parse_struct_args( cmd_name: &[&str], @@ -675,14 +679,17 @@ pub fn parse_struct_args( mut parse_positionals: ParseStructPositionals<'_>, mut parse_subcommand: Option>, help_func: &dyn Fn() -> String, + help_json_func: &dyn Fn() -> String, ) -> Result<(), EarlyExit> { let mut help = false; + let help_json = false; let mut remaining_args = args; let mut positional_index = 0; let mut options_ended = false; 'parse_args: while let Some(&next_arg) = remaining_args.get(0) { remaining_args = &remaining_args[1..]; + if (next_arg == "--help" || next_arg == "help") && !options_ended { help = true; continue; @@ -712,8 +719,9 @@ pub fn parse_struct_args( parse_positionals.parse(&mut positional_index, next_arg)?; } - - if help { + if help_json { + Err(EarlyExit { output: help_json_func(), status: Ok(()) }) + } else if help { Err(EarlyExit { output: help_func(), status: Ok(()) }) } else { Ok(()) diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index e01e6d0..b521b28 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -94,6 +94,105 @@ fn subcommand_example() { assert_eq!(two, TopLevel { nested: MySubCommandEnum::Two(SubCommandTwo { fooey: true }) },); } +#[test] +fn help_json_test_subcommand() { + #[derive(FromArgs, PartialEq, Debug)] + /// Top-level command. + struct TopLevel { + #[argh(subcommand)] + nested: MySubCommandEnum, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum MySubCommandEnum { + One(SubCommandOne), + Two(SubCommandTwo), + } + + #[derive(FromArgs, PartialEq, Debug)] + /// First subcommand. + #[argh(subcommand, name = "one")] + struct SubCommandOne { + #[argh(option)] + /// how many x + x: usize, + } + + #[derive(FromArgs, PartialEq, Debug)] + /// Second subcommand. + #[argh(subcommand, name = "two")] + struct SubCommandTwo { + #[argh(switch)] + /// whether to fooey + fooey: bool, + } + + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Top-level command.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, + {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [{"name": "one", "description": "First subcommand."}, + {"name": "two", "description": "Second subcommand."}] +} +"###, + ); + + assert_help_json_string::( + vec!["one", "--help-json"], + r###"{ +"usage": "test_arg_0 one --x ", +"description": "First subcommand.", +"options": [{"short": "", "long": "--x", "description": "how many x"}, + {"short": "", "long": "--help", "description": "display usage information"}, + {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_multiline_doc_comment() { + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(switch)] + /// a switch with a description + /// that is spread across + /// a number of + /// lines of comments. + _s: bool, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [--s]", +"description": "Short description", +"options": [{"short": "", "long": "--s", "description": "a switch with a description that is spread across a number of lines of comments."}, + {"short": "", "long": "--help", "description": "display usage information"}, + {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + #[test] fn multiline_doc_comment_description() { #[derive(FromArgs)] @@ -116,6 +215,7 @@ Options: --s a switch with a description that is spread across a number of lines of comments. --help display usage information + --help-json display usage information encoded in JSON "###, ); } @@ -203,6 +303,16 @@ fn assert_help_string(help_str: &str) { } } +fn assert_help_json_string(args: Vec<&str>, help_str: &str) { + match T::from_args(&["test_arg_0"], &args) { + Ok(_) => panic!("help-json was parsed as args"), + Err(e) => { + assert_eq!(help_str, e.output); + e.status.expect("help-json returned an error"); + } + } +} + fn assert_output(args: &[&str], expected: T) { let t = T::from_args(&["cmd"], args).expect("failed to parse"); assert_eq!(t, expected); @@ -253,6 +363,7 @@ Woot Options: -n, --n fooey --help display usage information + --help-json display usage information encoded in JSON "###, ); } @@ -275,6 +386,7 @@ Woot Options: --option-name fooey --help display usage information + --help-json display usage information encoded in JSON "###, ); } @@ -313,6 +425,7 @@ Positional Arguments: Options: --help display usage information + --help-json display usage information encoded in JSON "###, ); } @@ -702,6 +815,7 @@ A type for testing `--help`/`help` Options: --help display usage information + --help-json display usage information encoded in JSON Commands: first First subcommmand for testing `help`. @@ -713,6 +827,7 @@ First subcommmand for testing `help`. Options: --help display usage information + --help-json display usage information encoded in JSON Commands: second Second subcommand for testing `help`. @@ -724,6 +839,7 @@ Second subcommand for testing `help`. Options: --help display usage information + --help-json display usage information encoded in JSON "###; #[test] @@ -766,7 +882,7 @@ Options: #[derive(FromArgs, PartialEq, Debug)] #[argh( - description = "Destroy the contents of .", + description = "Destroy the contents of with a specific \"method of destruction\".", example = "Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp", note = "Use `{command_name} help ` for details on [] for a subcommand.", error_code(2, "The blade is too dull."), @@ -859,7 +975,7 @@ Options: assert_help_string::( r###"Usage: test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] [] -Destroy the contents of . +Destroy the contents of with a specific "method of destruction". Options: -f, --force force, ignore minor errors. This description is so long that @@ -869,6 +985,7 @@ Options: -s, --scribble write repeatedly -v, --verbose say more. Defaults to $BLAST_VERBOSE. --help display usage information + --help-json display usage information encoded in JSON Commands: blow-up explosively separate @@ -888,6 +1005,31 @@ Error codes: ); } + #[test] + fn help_json_example() { + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] []", +"description": "Destroy the contents of with a specific \"method of destruction\".", +"options": [{"short": "f", "long": "--force", "description": "force, ignore minor errors. This description is so long that it wraps to the next line."}, + {"short": "", "long": "--really-really-really-long-name-for-pat", "description": "documentation"}, + {"short": "s", "long": "--scribble", "description": "write repeatedly"}, + {"short": "v", "long": "--verbose", "description": "say more. Defaults to $BLAST_VERBOSE."}, + {"short": "", "long": "--help", "description": "display usage information"}, + {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "Scribble 'abc' and then run |grind|.\n$ test_arg_0 -s 'abc' grind old.txt taxes.cp", +"notes": "Use `test_arg_0 help ` for details on [] for a subcommand.", +"error_codes": [{"name": "2", "description": "The blade is too dull."}, + {"name": "3", "description": "Out of fuel."}], +"subcommands": [{"name": "blow-up", "description": "explosively separate"}, + {"name": "grind", "description": "make smaller by many small cuts"}] +} +"###, + ); + } + #[allow(dead_code)] #[derive(argh::FromArgs)] /// Destroy the contents of . @@ -908,6 +1050,7 @@ Positional Arguments: Options: --help display usage information + --help-json display usage information encoded in JSON "###, ); } @@ -1271,6 +1414,7 @@ Woot Options: -n, --n fooey --help display usage information + --help-json display usage information encoded in JSON "### .to_owned(), status: Ok(()), diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs index c295825..43c4c2c 100644 --- a/argh_derive/src/help.rs +++ b/argh_derive/src/help.rs @@ -15,51 +15,35 @@ use { const SECTION_SEPARATOR: &str = "\n\n"; +// Define constants for strings used for both help formats. +pub(crate) const HELP_FLAG: &str = "--help"; +pub(crate) const HELP_DESCRIPTION: &str = "display usage information"; +pub(crate) const HELP_JSON_FLAG: &str = "--help-json"; +pub(crate) const HELP_JSON_DESCRIPTION: &str = "display usage information encoded in JSON"; + /// Returns a `TokenStream` generating a `String` help message. /// /// Note: `fields` entries with `is_subcommand.is_some()` will be ignored /// in favor of the `subcommand` argument. pub(crate) fn help( errors: &Errors, - cmd_name_str_array_ident: syn::Ident, + cmd_name_str_array_ident: &syn::Ident, ty_attrs: &TypeAttrs, fields: &[StructField<'_>], subcommand: Option<&StructField<'_>>, ) -> TokenStream { let mut format_lit = "Usage: {command_name}".to_string(); - let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional); - let mut has_positional = false; - for arg in positional.clone() { - has_positional = true; - format_lit.push(' '); - positional_usage(&mut format_lit, arg); - } - - let options = fields.iter().filter(|f| f.long_name.is_some()); - for option in options.clone() { - format_lit.push(' '); - option_usage(&mut format_lit, option); - } - - if let Some(subcommand) = subcommand { - format_lit.push(' '); - if !subcommand.optionality.is_required() { - format_lit.push('['); - } - format_lit.push_str(""); - if !subcommand.optionality.is_required() { - format_lit.push(']'); - } - format_lit.push_str(" []"); - } + build_usage_command_line(&mut format_lit, fields, subcommand); format_lit.push_str(SECTION_SEPARATOR); let description = require_description(errors, Span::call_site(), &ty_attrs.description, "type"); format_lit.push_str(&description); - if has_positional { + let mut positional = fields.iter().filter(|f| f.kind == FieldKind::Positional).peekable(); + + if positional.peek().is_some() { format_lit.push_str(SECTION_SEPARATOR); format_lit.push_str("Positional Arguments:"); for arg in positional { @@ -69,11 +53,13 @@ pub(crate) fn help( format_lit.push_str(SECTION_SEPARATOR); format_lit.push_str("Options:"); + let options = fields.iter().filter(|f| f.long_name.is_some()); for option in options { option_description(errors, &mut format_lit, option); } - // Also include "help" - option_description_format(&mut format_lit, None, "--help", "display usage information"); + // Also include "help" and "help-json" + option_description_format(&mut format_lit, None, HELP_FLAG, HELP_DESCRIPTION); + option_description_format(&mut format_lit, None, HELP_JSON_FLAG, HELP_JSON_DESCRIPTION); let subcommand_calculation; let subcommand_format_arg; @@ -252,3 +238,34 @@ fn option_description_format( let info = argh_shared::CommandInfo { name: &*name, description }; argh_shared::write_description(out, &info); } + +/// Builds the usage description command line and appends it to "out". +pub(crate) fn build_usage_command_line( + out: &mut String, + fields: &[StructField<'_>], + subcommand: Option<&StructField<'_>>, +) { + let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional); + for arg in positional.clone() { + out.push(' '); + positional_usage(out, arg); + } + + let options = fields.iter().filter(|f| f.long_name.is_some()); + for option in options.clone() { + out.push(' '); + option_usage(out, option); + } + + if let Some(subcommand) = subcommand { + out.push(' '); + if !subcommand.optionality.is_required() { + out.push('['); + } + out.push_str(""); + if !subcommand.optionality.is_required() { + out.push(']'); + } + out.push_str(" []"); + } +} diff --git a/argh_derive/src/help_json.rs b/argh_derive/src/help_json.rs new file mode 100644 index 0000000..72c6e86 --- /dev/null +++ b/argh_derive/src/help_json.rs @@ -0,0 +1,234 @@ +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +use { + crate::{ + errors::Errors, + help::{ + build_usage_command_line, require_description, HELP_DESCRIPTION, HELP_FLAG, + HELP_JSON_DESCRIPTION, HELP_JSON_FLAG, + }, + parse_attrs::{FieldKind, TypeAttrs}, + StructField, + }, + proc_macro2::{Span, TokenStream}, + quote::quote, +}; + +struct OptionHelp { + short: String, + long: String, + description: String, +} + +struct PositionalHelp { + name: String, + description: String, +} +struct HelpJSON { + usage: String, + description: String, + positional_args: Vec, + options: Vec, + examples: String, + notes: String, + error_codes: Vec, +} + +fn option_elements_json(options: &[OptionHelp]) -> String { + let mut retval = String::from(""); + for opt in options { + if !retval.is_empty() { + retval.push_str(",\n "); + } + retval.push_str(&format!( + "{{\"short\": \"{}\", \"long\": \"{}\", \"description\": \"{}\"}}", + opt.short, + opt.long, + escape_json(&opt.description) + )); + } + retval +} +fn help_elements_json(elements: &[PositionalHelp]) -> String { + let mut retval = String::from(""); + for pos in elements { + if !retval.is_empty() { + retval.push_str(",\n "); + } + retval.push_str(&format!( + "{{\"name\": \"{}\", \"description\": \"{}\"}}", + pos.name, + escape_json(&pos.description) + )); + } + retval +} + +/// Returns a `TokenStream` generating a `String` help message containing JSON. +/// +/// Note: `fields` entries with `is_subcommand.is_some()` will be ignored +/// in favor of the `subcommand` argument. +pub(crate) fn help_json( + errors: &Errors, + cmd_name_str_array_ident: &syn::Ident, + ty_attrs: &TypeAttrs, + fields: &[StructField<'_>], + subcommand: Option<&StructField<'_>>, +) -> TokenStream { + let mut usage_format_pattern = "{command_name}".to_string(); + build_usage_command_line(&mut usage_format_pattern, fields, subcommand); + + let mut help_obj = HelpJSON { + usage: String::from(""), + description: String::from(""), + positional_args: vec![], + options: vec![], + examples: String::from(""), + notes: String::from(""), + error_codes: vec![], + }; + + // Add positional args to the help object. + let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional); + for arg in positional { + let mut description = String::from(""); + if let Some(desc) = &arg.attrs.description { + description = desc.content.value().trim().to_owned(); + } + help_obj.positional_args.push(PositionalHelp { name: arg.arg_name(), description }); + } + + // Add options to the help object. + let options = fields.iter().filter(|f| f.long_name.is_some()); + for option in options { + let short = match option.attrs.short.as_ref().map(|s| s.value()) { + Some(c) => String::from(c), + None => String::from(""), + }; + let long_with_leading_dashes = + option.long_name.as_ref().expect("missing long name for option"); + let description = + require_description(errors, option.name.span(), &option.attrs.description, "field"); + help_obj.options.push(OptionHelp { + short, + long: long_with_leading_dashes.to_owned(), + description, + }); + } + // Also include "help" and "help-json" + help_obj.options.push(OptionHelp { + short: String::from(""), + long: String::from(HELP_FLAG), + description: String::from(HELP_DESCRIPTION), + }); + help_obj.options.push(OptionHelp { + short: String::from(""), + long: String::from(HELP_JSON_FLAG), + description: String::from(HELP_JSON_DESCRIPTION), + }); + + let subcommand_calculation; + if let Some(subcommand) = subcommand { + let subcommand_ty = subcommand.ty_without_wrapper; + subcommand_calculation = quote! { + let mut subcommands = String::from(""); + for cmd in <#subcommand_ty as argh::SubCommands>::COMMANDS { + if !subcommands.is_empty() { + subcommands.push_str(",\n "); + } + subcommands.push_str(&format!("{{\"name\": \"{}\", \"description\": \"{}\"}}", + cmd.name, cmd.description)); + } + }; + } else { + subcommand_calculation = quote! { + let subcommands = String::from(""); + }; + } + + help_obj.usage = usage_format_pattern.clone(); + + help_obj.description = + require_description(errors, Span::call_site(), &ty_attrs.description, "type"); + + let mut example: String = String::from(""); + for lit in &ty_attrs.examples { + example.push_str(&lit.value()); + } + help_obj.examples = example; + + let mut note: String = String::from(""); + for lit in &ty_attrs.notes { + note.push_str(&lit.value()); + } + help_obj.notes = note; + + if !ty_attrs.error_codes.is_empty() { + for (code, text) in &ty_attrs.error_codes { + help_obj.error_codes.push(PositionalHelp { + name: code.to_string(), + description: escape_json(&text.value().to_string()), + }); + } + } + + let help_options_json = option_elements_json(&help_obj.options); + let help_positional_json = help_elements_json(&help_obj.positional_args); + let help_error_codes_json = help_elements_json(&help_obj.error_codes); + + let help_description = escape_json(&help_obj.description); + let help_examples: TokenStream; + let help_notes: TokenStream; + + let notes_pattern = escape_json(&help_obj.notes); + // check if we need to interpolate the string. + if notes_pattern.contains("{command_name}") { + help_notes = quote! { + json_help_string.push_str(&format!(#notes_pattern,command_name = #cmd_name_str_array_ident.join(" "))); + }; + } else { + help_notes = quote! { + json_help_string.push_str(#notes_pattern); + }; + } + let examples_pattern = escape_json(&help_obj.examples); + if examples_pattern.contains("{command_name}") { + help_examples = quote! { + json_help_string.push_str(&format!(#examples_pattern,command_name = #cmd_name_str_array_ident.join(" "))); + }; + } else { + help_examples = quote! { + json_help_string.push_str(#examples_pattern); + }; + } + + quote! {{ + #subcommand_calculation + + // Build up the string for json. The name of the command needs to be dereferenced, so it + // can't be done in the macro. + let mut json_help_string = "{\n".to_string(); + let usage_value = format!(#usage_format_pattern,command_name = #cmd_name_str_array_ident.join(" ")); + json_help_string.push_str(&format!("\"usage\": \"{}\",\n",usage_value)); + json_help_string.push_str(&format!("\"description\": \"{}\",\n", #help_description)); + json_help_string.push_str(&format!("\"options\": [{}],\n", #help_options_json)); + json_help_string.push_str(&format!("\"positional\": [{}],\n", #help_positional_json)); + json_help_string.push_str("\"examples\": \""); + #help_examples; + json_help_string.push_str("\",\n"); + json_help_string.push_str("\"notes\": \""); + #help_notes; + json_help_string.push_str("\",\n"); + json_help_string.push_str(&format!("\"error_codes\": [{}],\n", #help_error_codes_json)); + json_help_string.push_str(&format!("\"subcommands\": [{}]\n", subcommands)); + json_help_string.push_str("}\n"); + json_help_string + }} +} + +/// Escape characters in strings to be JSON compatible. +fn escape_json(value: &str) -> String { + value.replace("\n", r#"\n"#).replace("\"", r#"\""#) +} diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index ab72a40..3aac34f 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -21,6 +21,7 @@ use { mod errors; mod help; +mod help_json; mod parse_attrs; /// Entrypoint for `#[derive(FromArgs)]`. @@ -318,7 +319,9 @@ fn impl_from_args_struct_from_args<'a>( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); - let help = help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand); + let help = help::help(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help_json = + help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); let method_impl = quote_spanned! { impl_span => fn from_args(__cmd_name: &[&str], __args: &[&str]) @@ -348,6 +351,7 @@ fn impl_from_args_struct_from_args<'a>( }, #parse_subcommands, &|| #help, + &|| #help_json )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); @@ -432,7 +436,9 @@ fn impl_from_args_struct_redact_arg_values<'a>( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); - let help = help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand); + let help = help::help(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help_json = + help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); let method_impl = quote_spanned! { impl_span => fn redact_arg_values(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result, argh::EarlyExit> { @@ -458,6 +464,7 @@ fn impl_from_args_struct_redact_arg_values<'a>( }, #redact_subcommands, &|| #help, + &|| #help_json )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); From 34a647b9ee5c75610a341308d2898380d482262e Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Tue, 14 Dec 2021 13:55:58 -0800 Subject: [PATCH 05/13] Adding --help-json to get JSON encoded help message (#2) * Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. * Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. * Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. --- README.md | 44 +++ argh/tests/help_json_tests.rs | 640 ++++++++++++++++++++++++++++++++++ argh/tests/lib.rs | 29 +- argh_derive/src/help_json.rs | 8 +- 4 files changed, 691 insertions(+), 30 deletions(-) create mode 100644 argh/tests/help_json_tests.rs diff --git a/README.md b/README.md index 4e949e4..3f71234 100644 --- a/README.md +++ b/README.md @@ -174,4 +174,48 @@ struct SubCommandTwo { } ``` +## Attribute Summary +### Type attributes for `argh` + +The attributes used to configure the argh information for a type are defined in +[parse_attrs::TypeAttrs](argh_derive/src/parse_attrs.rs). + +* `subcommand` - a subcommand type. This attribute must appear on both enumeration and each struct that + is a variant for the enumerated subcommand. +* `error_code(code, description)` - an error code for the command. This attribute can appear zero + or more times. +* `examples=` - Formatted text containing examples of how to use this command. This + is an optional attribute. +* `name=` - (required for subcommand variant) the name of the subcommand. +* `notes=` - Formatted text containing usage notes for this command. This + is an optional attribute. + pub error_codes: Vec<(syn::LitInt, syn::LitStr)>, + +### Field attributes for `argh` + +The attributes used to configure the argh information for a field are +defined in [parse_attrs.rs](argh_derive/src/parse_attrs.rs). + +* Field kind. This is the first attribute. Valid kinds are: + * `switch` - a boolean flag, its presence on the command sets the field to `true`. + * `option` - a value. This can be a simple type like String, or usize, and enumeration. + This can be a scalar or Vec<> for repeated values. + * `subcommand` - a subcommand. The type of this field is an enumeration with a value for each + subcommand. This attribute must appear on both the "top level" field and each struct that + is a variant for the enumerated subcommand. + * `positional` - a positional argument. This can be scalar or Vec<>. Only the last positional + argument can be Option<>, Vec<>, or defaulted. +* `arg_name=` - the name to use for a positional argument in the help or the value of a `option`. + If not given, the default is the name of the field. +* `default=` - the default value for the `option` or `positional` fields. +* `description=` - the description of the flag or argument. The default value is the doc comment + for the field. +* `from_str_fn` is the name of a custom deserialization function for this field with the signature: + `fn(&str) -> Result`. +* `long=` - the long format of the option or switch name. If `long` is not present, the + flag name defaults to the field name. +* `short=` - the single character for this flag. If `short` is not present, there is no + short equivalent flag. + + NOTE: This is not an officially supported Google product. diff --git a/argh/tests/help_json_tests.rs b/argh/tests/help_json_tests.rs new file mode 100644 index 0000000..cae34fd --- /dev/null +++ b/argh/tests/help_json_tests.rs @@ -0,0 +1,640 @@ +#![cfg(test)] +// Copyright (c) 2020 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +use std::path::PathBuf; +use {argh::FromArgs, std::fmt::Debug}; + +#[test] +fn help_json_test_subcommand() { + #[derive(FromArgs, PartialEq, Debug)] + /// Top-level command. + struct TopLevel { + #[argh(subcommand)] + nested: MySubCommandEnum, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum MySubCommandEnum { + One(SubCommandOne), + Two(SubCommandTwo), + } + + #[derive(FromArgs, PartialEq, Debug)] + /// First subcommand. + #[argh(subcommand, name = "one")] + struct SubCommandOne { + #[argh(option)] + /// how many x + x: usize, + } + + #[derive(FromArgs, PartialEq, Debug)] + /// Second subcommand. + #[argh(subcommand, name = "two")] + struct SubCommandTwo { + #[argh(switch)] + /// whether to fooey + fooey: bool, + } + + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Top-level command.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [{"name": "one", "description": "First subcommand."}, +{"name": "two", "description": "Second subcommand."}] +} +"###, + ); + + assert_help_json_string::( + vec!["one", "--help-json"], + r###"{ +"usage": "test_arg_0 one --x ", +"description": "First subcommand.", +"options": [{"short": "", "long": "--x", "description": "how many x"}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_multiline_doc_comment() { + #[derive(FromArgs)] + /// Short description + struct Cmd { + #[argh(switch)] + /// a switch with a description + /// that is spread across + /// a number of + /// lines of comments. + _s: bool, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [--s]", +"description": "Short description", +"options": [{"short": "", "long": "--s", "description": "a switch with a description that is spread across a number of lines of comments."}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_basic_args() { + #[allow(dead_code)] + #[derive(FromArgs)] + /// Basic command args demonstrating multiple types and cardinality. "With quotes" + struct Basic { + /// should the power be on. "Quoted value" should work too. + #[argh(switch)] + power: bool, + + /// option that is required because of no default and not Option<>. + #[argh(option, long = "required")] + required_flag: String, + + /// optional speed if not specified it is None. + #[argh(option, short = 's')] + speed: Option, + + /// repeatable option. + #[argh(option, arg_name = "url")] + link: Vec, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [--power] --required [-s ] [--link ]", +"description": "Basic command args demonstrating multiple types and cardinality. \"With quotes\"", +"options": [{"short": "", "long": "--power", "description": "should the power be on. \"Quoted value\" should work too."}, +{"short": "", "long": "--required", "description": "option that is required because of no default and not Option<>."}, +{"short": "s", "long": "--speed", "description": "optional speed if not specified it is None."}, +{"short": "", "long": "--link", "description": "repeatable option."}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_positional_args() { + #[allow(dead_code)] + #[derive(FromArgs)] + /// Command with positional args demonstrating. "With quotes" + struct Positional { + /// the "root" position. + #[argh(positional, arg_name = "root")] + root_value: String, + + /// trunk value + #[argh(positional)] + trunk: String, + + /// leaves. There can be many leaves. + #[argh(positional)] + leaves: Vec, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Command with positional args demonstrating. \"With quotes\"", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "root", "description": "the \"root\" position."}, +{"name": "trunk", "description": "trunk value"}, +{"name": "leaves", "description": "leaves. There can be many leaves."}], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_optional_positional_args() { + #[allow(dead_code)] + #[derive(FromArgs)] + /// Command with positional args demonstrating last value is optional + struct Positional { + /// the "root" position. + #[argh(positional, arg_name = "root")] + root_value: String, + + /// trunk value + #[argh(positional)] + trunk: String, + + /// leaves. There can be many leaves. + #[argh(positional)] + leaves: Option, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Command with positional args demonstrating last value is optional", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "root", "description": "the \"root\" position."}, +{"name": "trunk", "description": "trunk value"}, +{"name": "leaves", "description": "leaves. There can be many leaves."}], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_default_positional_args() { + #[allow(dead_code)] + #[derive(FromArgs)] + /// Command with positional args demonstrating last value is defaulted. + struct Positional { + /// the "root" position. + #[argh(positional, arg_name = "root")] + root_value: String, + + /// trunk value + #[argh(positional)] + trunk: String, + + /// leaves. There can be many leaves. + #[argh(positional, default = "String::from(\"hello\")")] + leaves: String, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Command with positional args demonstrating last value is defaulted.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "root", "description": "the \"root\" position."}, +{"name": "trunk", "description": "trunk value"}, +{"name": "leaves", "description": "leaves. There can be many leaves."}], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_notes_examples_errors() { + #[allow(dead_code)] + #[derive(FromArgs)] + /// Command with Examples and usage Notes, including error codes. + #[argh( + note = r##" + These usage notes appear for {command_name} and how to best use it. + The formatting should be preserved. + one + two + three then a blank + + and one last line with "quoted text"."##, + example = r##" + Use the command with 1 file: + + `{command_name} /path/to/file` + + Use it with a "wildcard": + + `{command_name} /path/to/*` + + a blank line + + and one last line with "quoted text"."##, + error_code(0, "Success"), + error_code(1, "General Error"), + error_code(2, "Some error with \"quotes\"") + )] + struct NotesExamplesErrors { + /// the "root" position. + #[argh(positional, arg_name = "files")] + fields: Vec, + } + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 []", +"description": "Command with Examples and usage Notes, including error codes.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "files", "description": "the \"root\" position."}], +"examples": "\n Use the command with 1 file:\n\n `test_arg_0 /path/to/file`\n\n Use it with a \"wildcard\":\n\n `test_arg_0 /path/to/*`\n\n a blank line\n \n and one last line with \"quoted text\".", +"notes": "\n These usage notes appear for test_arg_0 and how to best use it.\n The formatting should be preserved.\n one\n two\n three then a blank\n \n and one last line with \"quoted text\".", +"error_codes": [{"name": "0", "description": "Success"}, +{"name": "1", "description": "General Error"}, +{"name": "2", "description": "Some error with \"quotes\""}], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_subcommands() { + #[allow(dead_code)] + #[derive(FromArgs)] + ///Top level command with "subcommands". + struct TopLevel { + /// show verbose output + #[argh(switch)] + verbose: bool, + + /// this doc comment does not appear anywhere. + #[argh(subcommand)] + cmd: SubcommandEnum, + } + + #[derive(FromArgs)] + #[argh(subcommand)] + /// Doc comments for subcommand enums does not appear in the help text. + enum SubcommandEnum { + Command1(Command1Args), + Command2(Command2Args), + Command3(Command3Args), + } + + /// Command1 args are used for Command1. + #[allow(dead_code)] + #[derive(FromArgs)] + #[argh(subcommand, name = "one")] + struct Command1Args { + /// the "root" position. + #[argh(positional, arg_name = "root")] + root_value: String, + + /// trunk value + #[argh(positional)] + trunk: String, + + /// leaves. There can be many leaves. + #[argh(positional, default = "String::from(\"hello\")")] + leaves: String, + } + /// Command2 args are used for Command2. + #[allow(dead_code)] + #[derive(FromArgs)] + #[argh(subcommand, name = "two")] + struct Command2Args { + /// should the power be on. "Quoted value" should work too. + #[argh(switch)] + power: bool, + + /// option that is required because of no default and not Option<>. + #[argh(option, long = "required")] + required_flag: String, + + /// optional speed if not specified it is None. + #[argh(option, short = 's')] + speed: Option, + + /// repeatable option. + #[argh(option, arg_name = "url")] + link: Vec, + } + /// Command3 args are used for Command3 which has no options or arguments. + #[derive(FromArgs)] + #[argh(subcommand, name = "three")] + struct Command3Args {} + + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [--verbose] []", +"description": "Top level command with \"subcommands\".", +"options": [{"short": "", "long": "--verbose", "description": "show verbose output"}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [{"name": "one", "description": "Command1 args are used for Command1."}, +{"name": "two", "description": "Command2 args are used for Command2."}, +{"name": "three", "description": "Command3 args are used for Command3 which has no options or arguments."}] +} +"###, + ); + + assert_help_json_string::( + vec!["one", "--help-json"], + r###"{ +"usage": "test_arg_0 one []", +"description": "Command1 args are used for Command1.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "root", "description": "the \"root\" position."}, +{"name": "trunk", "description": "trunk value"}, +{"name": "leaves", "description": "leaves. There can be many leaves."}], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); + + assert_help_json_string::( + vec!["two", "--help-json"], + r###"{ +"usage": "test_arg_0 two [--power] --required [-s ] [--link ]", +"description": "Command2 args are used for Command2.", +"options": [{"short": "", "long": "--power", "description": "should the power be on. \"Quoted value\" should work too."}, +{"short": "", "long": "--required", "description": "option that is required because of no default and not Option<>."}, +{"short": "s", "long": "--speed", "description": "optional speed if not specified it is None."}, +{"short": "", "long": "--link", "description": "repeatable option."}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); + + assert_help_json_string::( + vec!["three", "--help-json"], + r###"{ +"usage": "test_arg_0 three", +"description": "Command3 args are used for Command3 which has no options or arguments.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +"###, + ); +} + +#[test] +fn help_json_test_subcommand_notes_examples() { + #[allow(dead_code)] + #[derive(FromArgs)] + ///Top level command with "subcommands". + #[argh( + note = "Top level note", + example = "Top level example", + error_code(0, "Top level success") + )] + struct TopLevel { + /// show verbose output + #[argh(switch)] + verbose: bool, + + /// this doc comment does not appear anywhere. + #[argh(subcommand)] + cmd: SubcommandEnum, + } + + #[derive(FromArgs)] + #[argh(subcommand)] + /// Doc comments for subcommand enums does not appear in the help text. + enum SubcommandEnum { + Command1(Command1Args), + } + + /// Command1 args are used for subcommand one. + #[allow(dead_code)] + #[derive(FromArgs)] + #[argh( + subcommand, + name = "one", + note = "{command_name} is used as a subcommand of \"Top level\"", + example = "\"Typical\" usage is `{command_name}`.", + error_code(0, "one level success") + )] + struct Command1Args { + /// the "root" position. + #[argh(positional, arg_name = "root")] + root_value: String, + + /// trunk value + #[argh(positional)] + trunk: String, + + /// leaves. There can be many leaves. + #[argh(positional, default = "String::from(\"hello\")")] + leaves: String, + } + + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [--verbose] []", +"description": "Top level command with \"subcommands\".", +"options": [{"short": "", "long": "--verbose", "description": "show verbose output"}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "Top level example", +"notes": "Top level note", +"error_codes": [{"name": "0", "description": "Top level success"}], +"subcommands": [{"name": "one", "description": "Command1 args are used for subcommand one."}] +} +"###, + ); + + assert_help_json_string::( + vec!["one", "--help-json"], + r###"{ +"usage": "test_arg_0 one []", +"description": "Command1 args are used for subcommand one.", +"options": [{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [{"name": "root", "description": "the \"root\" position."}, +{"name": "trunk", "description": "trunk value"}, +{"name": "leaves", "description": "leaves. There can be many leaves."}], +"examples": "\"Typical\" usage is `test_arg_0 one`.", +"notes": "test_arg_0 one is used as a subcommand of \"Top level\"", +"error_codes": [{"name": "0", "description": "one level success"}], +"subcommands": [] +} +"###, + ); +} + +/// Test that descriptions can start with an initialism despite +/// usually being required to start with a lowercase letter. +#[derive(FromArgs)] +#[allow(unused)] +struct DescriptionStartsWithInitialism { + /// URL fooey + #[argh(option)] + x: u8, +} + +#[test] +fn help_json_test_example() { + #[derive(FromArgs, PartialEq, Debug)] + #[argh( + description = "Destroy the contents of with a specific \"method of destruction\".", + example = "Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp", + note = "Use `{command_name} help ` for details on [] for a subcommand.", + error_code(2, "The blade is too dull."), + error_code(3, "Out of fuel.") + )] + struct HelpExample { + /// force, ignore minor errors. This description is so long that it wraps to the next line. + #[argh(switch, short = 'f')] + force: bool, + + /// documentation + #[argh(switch)] + really_really_really_long_name_for_pat: bool, + + /// write repeatedly + #[argh(option, short = 's')] + scribble: String, + + /// say more. Defaults to $BLAST_VERBOSE. + #[argh(switch, short = 'v')] + verbose: bool, + + #[argh(subcommand)] + command: HelpExampleSubCommands, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand)] + enum HelpExampleSubCommands { + BlowUp(BlowUp), + Grind(GrindCommand), + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand, name = "blow-up")] + /// explosively separate + struct BlowUp { + /// blow up bombs safely + #[argh(switch)] + safely: bool, + } + + #[derive(FromArgs, PartialEq, Debug)] + #[argh(subcommand, name = "grind", description = "make smaller by many small cuts")] + struct GrindCommand { + /// wear a visor while grinding + #[argh(switch)] + safely: bool, + } + + assert_help_json_string::( + vec!["--help-json"], + r###"{ +"usage": "test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] []", +"description": "Destroy the contents of with a specific \"method of destruction\".", +"options": [{"short": "f", "long": "--force", "description": "force, ignore minor errors. This description is so long that it wraps to the next line."}, +{"short": "", "long": "--really-really-really-long-name-for-pat", "description": "documentation"}, +{"short": "s", "long": "--scribble", "description": "write repeatedly"}, +{"short": "v", "long": "--verbose", "description": "say more. Defaults to $BLAST_VERBOSE."}, +{"short": "", "long": "--help", "description": "display usage information"}, +{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"positional": [], +"examples": "Scribble 'abc' and then run |grind|.\n$ test_arg_0 -s 'abc' grind old.txt taxes.cp", +"notes": "Use `test_arg_0 help ` for details on [] for a subcommand.", +"error_codes": [{"name": "2", "description": "The blade is too dull."}, +{"name": "3", "description": "Out of fuel."}], +"subcommands": [{"name": "blow-up", "description": "explosively separate"}, +{"name": "grind", "description": "make smaller by many small cuts"}] +} +"###, + ); +} + +fn assert_help_json_string(args: Vec<&str>, help_str: &str) { + match T::from_args(&["test_arg_0"], &args) { + Ok(_) => panic!("help-json was parsed as args"), + Err(e) => { + assert_eq!(help_str, e.output); + e.status.expect("help-json returned an error"); + } + } +} diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index b521b28..20586e5 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -13,6 +13,8 @@ use {argh::FromArgs, std::fmt::Debug}; +mod help_json_tests; + #[test] fn basic_example() { #[derive(FromArgs, PartialEq, Debug)] @@ -1004,32 +1006,7 @@ Error codes: "###, ); } - - #[test] - fn help_json_example() { - assert_help_json_string::( - vec!["--help-json"], - r###"{ -"usage": "test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] []", -"description": "Destroy the contents of with a specific \"method of destruction\".", -"options": [{"short": "f", "long": "--force", "description": "force, ignore minor errors. This description is so long that it wraps to the next line."}, - {"short": "", "long": "--really-really-really-long-name-for-pat", "description": "documentation"}, - {"short": "s", "long": "--scribble", "description": "write repeatedly"}, - {"short": "v", "long": "--verbose", "description": "say more. Defaults to $BLAST_VERBOSE."}, - {"short": "", "long": "--help", "description": "display usage information"}, - {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], -"positional": [], -"examples": "Scribble 'abc' and then run |grind|.\n$ test_arg_0 -s 'abc' grind old.txt taxes.cp", -"notes": "Use `test_arg_0 help ` for details on [] for a subcommand.", -"error_codes": [{"name": "2", "description": "The blade is too dull."}, - {"name": "3", "description": "Out of fuel."}], -"subcommands": [{"name": "blow-up", "description": "explosively separate"}, - {"name": "grind", "description": "make smaller by many small cuts"}] -} -"###, - ); - } - + #[allow(dead_code)] #[derive(argh::FromArgs)] /// Destroy the contents of . diff --git a/argh_derive/src/help_json.rs b/argh_derive/src/help_json.rs index 72c6e86..a84102a 100644 --- a/argh_derive/src/help_json.rs +++ b/argh_derive/src/help_json.rs @@ -40,7 +40,7 @@ fn option_elements_json(options: &[OptionHelp]) -> String { let mut retval = String::from(""); for opt in options { if !retval.is_empty() { - retval.push_str(",\n "); + retval.push_str(",\n"); } retval.push_str(&format!( "{{\"short\": \"{}\", \"long\": \"{}\", \"description\": \"{}\"}}", @@ -55,7 +55,7 @@ fn help_elements_json(elements: &[PositionalHelp]) -> String { let mut retval = String::from(""); for pos in elements { if !retval.is_empty() { - retval.push_str(",\n "); + retval.push_str(",\n"); } retval.push_str(&format!( "{{\"name\": \"{}\", \"description\": \"{}\"}}", @@ -136,7 +136,7 @@ pub(crate) fn help_json( let mut subcommands = String::from(""); for cmd in <#subcommand_ty as argh::SubCommands>::COMMANDS { if !subcommands.is_empty() { - subcommands.push_str(",\n "); + subcommands.push_str(",\n"); } subcommands.push_str(&format!("{{\"name\": \"{}\", \"description\": \"{}\"}}", cmd.name, cmd.description)); @@ -169,7 +169,7 @@ pub(crate) fn help_json( for (code, text) in &ty_attrs.error_codes { help_obj.error_codes.push(PositionalHelp { name: code.to_string(), - description: escape_json(&text.value().to_string()), + description: text.value().to_string(), }); } } From 66e80be3335ebb1aa218c00d6049fe0ee75f2b08 Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Mon, 13 Dec 2021 10:43:23 -0800 Subject: [PATCH 06/13] Adding --help-json to get JSON encoded help message This adds the `--help-json` flag which prints the help information encoded in a JSON object. This enables template engines to render the help information in other formats such as markdown. --- argh/tests/lib.rs | 111 +---------------------------------- argh_derive/src/help_json.rs | 2 +- 2 files changed, 2 insertions(+), 111 deletions(-) diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index 20586e5..0baabb4 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -96,105 +96,6 @@ fn subcommand_example() { assert_eq!(two, TopLevel { nested: MySubCommandEnum::Two(SubCommandTwo { fooey: true }) },); } -#[test] -fn help_json_test_subcommand() { - #[derive(FromArgs, PartialEq, Debug)] - /// Top-level command. - struct TopLevel { - #[argh(subcommand)] - nested: MySubCommandEnum, - } - - #[derive(FromArgs, PartialEq, Debug)] - #[argh(subcommand)] - enum MySubCommandEnum { - One(SubCommandOne), - Two(SubCommandTwo), - } - - #[derive(FromArgs, PartialEq, Debug)] - /// First subcommand. - #[argh(subcommand, name = "one")] - struct SubCommandOne { - #[argh(option)] - /// how many x - x: usize, - } - - #[derive(FromArgs, PartialEq, Debug)] - /// Second subcommand. - #[argh(subcommand, name = "two")] - struct SubCommandTwo { - #[argh(switch)] - /// whether to fooey - fooey: bool, - } - - assert_help_json_string::( - vec!["--help-json"], - r###"{ -"usage": "test_arg_0 []", -"description": "Top-level command.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}, - {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], -"positional": [], -"examples": "", -"notes": "", -"error_codes": [], -"subcommands": [{"name": "one", "description": "First subcommand."}, - {"name": "two", "description": "Second subcommand."}] -} -"###, - ); - - assert_help_json_string::( - vec!["one", "--help-json"], - r###"{ -"usage": "test_arg_0 one --x ", -"description": "First subcommand.", -"options": [{"short": "", "long": "--x", "description": "how many x"}, - {"short": "", "long": "--help", "description": "display usage information"}, - {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], -"positional": [], -"examples": "", -"notes": "", -"error_codes": [], -"subcommands": [] -} -"###, - ); -} - -#[test] -fn help_json_test_multiline_doc_comment() { - #[derive(FromArgs)] - /// Short description - struct Cmd { - #[argh(switch)] - /// a switch with a description - /// that is spread across - /// a number of - /// lines of comments. - _s: bool, - } - assert_help_json_string::( - vec!["--help-json"], - r###"{ -"usage": "test_arg_0 [--s]", -"description": "Short description", -"options": [{"short": "", "long": "--s", "description": "a switch with a description that is spread across a number of lines of comments."}, - {"short": "", "long": "--help", "description": "display usage information"}, - {"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], -"positional": [], -"examples": "", -"notes": "", -"error_codes": [], -"subcommands": [] -} -"###, - ); -} - #[test] fn multiline_doc_comment_description() { #[derive(FromArgs)] @@ -305,16 +206,6 @@ fn assert_help_string(help_str: &str) { } } -fn assert_help_json_string(args: Vec<&str>, help_str: &str) { - match T::from_args(&["test_arg_0"], &args) { - Ok(_) => panic!("help-json was parsed as args"), - Err(e) => { - assert_eq!(help_str, e.output); - e.status.expect("help-json returned an error"); - } - } -} - fn assert_output(args: &[&str], expected: T) { let t = T::from_args(&["cmd"], args).expect("failed to parse"); assert_eq!(t, expected); @@ -1006,7 +897,7 @@ Error codes: "###, ); } - + #[allow(dead_code)] #[derive(argh::FromArgs)] /// Destroy the contents of . diff --git a/argh_derive/src/help_json.rs b/argh_derive/src/help_json.rs index a84102a..b5e095d 100644 --- a/argh_derive/src/help_json.rs +++ b/argh_derive/src/help_json.rs @@ -40,7 +40,7 @@ fn option_elements_json(options: &[OptionHelp]) -> String { let mut retval = String::from(""); for opt in options { if !retval.is_empty() { - retval.push_str(",\n"); + retval.push_str(",\n"); } retval.push_str(&format!( "{{\"short\": \"{}\", \"long\": \"{}\", \"description\": \"{}\"}}", From 8131de1fe6b16c61e4fbe80bb84deee3b27b1ce3 Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Tue, 28 Dec 2021 17:55:42 -0800 Subject: [PATCH 07/13] Adding argh::help_json_from_args() This adds 2 methods to arghm, help_json_from_args() and help_json(), which is parallel to ::from_env(). The return value is a JSON encoded string suitable for parsing and using as input to generate reference docs. --- argh/src/lib.rs | 34 ++++--- argh/tests/help_json_tests.rs | 85 +++++++--------- argh/tests/lib.rs | 22 +++-- argh_derive/src/help.rs | 3 - argh_derive/src/help_json.rs | 10 +- argh_derive/src/lib.rs | 177 ++++++++++++++++++++++++++++++++-- 6 files changed, 231 insertions(+), 100 deletions(-) diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 8fa1cbd..1e66dd0 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -256,7 +256,6 @@ pub trait FromArgs: Sized { /// /// Options: /// --help display usage information - /// --help-json display usage information encoded in JSON /// /// Commands: /// list list all the classes. @@ -281,7 +280,6 @@ pub trait FromArgs: Sized { /// Options: /// --teacher-name list classes for only this teacher. /// --help display usage information - /// --help-json display usage information encoded in JSON /// "#.to_string(), /// status: Ok(()), /// }, @@ -424,7 +422,7 @@ pub trait FromArgs: Sized { /// /// Options: /// --help display usage information - /// --help-json display usage information encoded in JSON + /// /// Commands: /// list list all the classes. @@ -437,6 +435,22 @@ pub trait FromArgs: Sized { fn redact_arg_values(_command_name: &[&str], _args: &[&str]) -> Result, EarlyExit> { Ok(vec!["<>".into()]) } + + /// Returns a JSON encoded string of the usage information. This is intended to + /// create a "machine readable" version of the help text to enable reference + /// documentation generation. + fn help_json_from_args(command_name: &[&str], args: &[&str]) -> Result; + + /// Returns a JSON encoded string of the usage information based on the command line + /// found in argv, identical to `::from_env()`. This is intended to + /// create a "machine readable" version of the help text to enable reference + /// documentation generation. + fn help_json() -> Result { + let strings: Vec = std::env::args().collect(); + let cmd = cmd(&strings[0], &strings[0]); + let strs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect(); + Self::help_json_from_args(&[cmd], &strs[1..]) + } } /// A top-level `FromArgs` implementation that is not a subcommand. @@ -670,7 +684,6 @@ impl_flag_for_integers![u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,]; /// `parse_positionals`: Helper to parse positional arguments. /// `parse_subcommand`: Helper to parse a subcommand. /// `help_func`: Generate a help message as plain text. -/// `help_json_func`: Generate a help message serialized into JSON. #[doc(hidden)] pub fn parse_struct_args( cmd_name: &[&str], @@ -679,10 +692,8 @@ pub fn parse_struct_args( mut parse_positionals: ParseStructPositionals<'_>, mut parse_subcommand: Option>, help_func: &dyn Fn() -> String, - help_json_func: &dyn Fn() -> String, ) -> Result<(), EarlyExit> { let mut help = false; - let mut help_json = false; let mut remaining_args = args; let mut positional_index = 0; let mut options_ended = false; @@ -695,13 +706,6 @@ pub fn parse_struct_args( continue; } - // look for help-json for json formatted help output. - if (next_arg == "--help-json" || next_arg == "help-json") && !options_ended { - help = true; - help_json = true; - continue; - } - if next_arg.starts_with("-") && !options_ended { if next_arg == "--" { options_ended = true; @@ -726,9 +730,7 @@ pub fn parse_struct_args( parse_positionals.parse(&mut positional_index, next_arg)?; } - if help_json { - Err(EarlyExit { output: help_json_func(), status: Ok(()) }) - } else if help { + if help { Err(EarlyExit { output: help_func(), status: Ok(()) }) } else { Ok(()) diff --git a/argh/tests/help_json_tests.rs b/argh/tests/help_json_tests.rs index cae34fd..7d0c500 100644 --- a/argh/tests/help_json_tests.rs +++ b/argh/tests/help_json_tests.rs @@ -41,12 +41,11 @@ fn help_json_test_subcommand() { } assert_help_json_string::( - vec!["--help-json"], + vec![], r###"{ "usage": "test_arg_0 []", "description": "Top-level command.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"options": [{"short": "", "long": "--help", "description": "display usage information"}], "positional": [], "examples": "", "notes": "", @@ -58,13 +57,12 @@ fn help_json_test_subcommand() { ); assert_help_json_string::( - vec!["one", "--help-json"], + vec!["one"], r###"{ "usage": "test_arg_0 one --x ", "description": "First subcommand.", "options": [{"short": "", "long": "--x", "description": "how many x"}, -{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +{"short": "", "long": "--help", "description": "display usage information"}], "positional": [], "examples": "", "notes": "", @@ -88,13 +86,12 @@ fn help_json_test_multiline_doc_comment() { _s: bool, } assert_help_json_string::( - vec!["--help-json"], + vec![], r###"{ "usage": "test_arg_0 [--s]", "description": "Short description", "options": [{"short": "", "long": "--s", "description": "a switch with a description that is spread across a number of lines of comments."}, -{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +{"short": "", "long": "--help", "description": "display usage information"}], "positional": [], "examples": "", "notes": "", @@ -128,7 +125,7 @@ fn help_json_test_basic_args() { link: Vec, } assert_help_json_string::( - vec!["--help-json"], + vec![], r###"{ "usage": "test_arg_0 [--power] --required [-s ] [--link ]", "description": "Basic command args demonstrating multiple types and cardinality. \"With quotes\"", @@ -136,8 +133,7 @@ fn help_json_test_basic_args() { {"short": "", "long": "--required", "description": "option that is required because of no default and not Option<>."}, {"short": "s", "long": "--speed", "description": "optional speed if not specified it is None."}, {"short": "", "long": "--link", "description": "repeatable option."}, -{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +{"short": "", "long": "--help", "description": "display usage information"}], "positional": [], "examples": "", "notes": "", @@ -167,12 +163,11 @@ fn help_json_test_positional_args() { leaves: Vec, } assert_help_json_string::( - vec!["--help-json"], + vec![], r###"{ "usage": "test_arg_0 []", "description": "Command with positional args demonstrating. \"With quotes\"", -"options": [{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"options": [{"short": "", "long": "--help", "description": "display usage information"}], "positional": [{"name": "root", "description": "the \"root\" position."}, {"name": "trunk", "description": "trunk value"}, {"name": "leaves", "description": "leaves. There can be many leaves."}], @@ -204,12 +199,11 @@ fn help_json_test_optional_positional_args() { leaves: Option, } assert_help_json_string::( - vec!["--help-json"], + vec![], r###"{ "usage": "test_arg_0 []", "description": "Command with positional args demonstrating last value is optional", -"options": [{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"options": [{"short": "", "long": "--help", "description": "display usage information"}], "positional": [{"name": "root", "description": "the \"root\" position."}, {"name": "trunk", "description": "trunk value"}, {"name": "leaves", "description": "leaves. There can be many leaves."}], @@ -241,12 +235,11 @@ fn help_json_test_default_positional_args() { leaves: String, } assert_help_json_string::( - vec!["--help-json"], + vec![], r###"{ "usage": "test_arg_0 []", "description": "Command with positional args demonstrating last value is defaulted.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"options": [{"short": "", "long": "--help", "description": "display usage information"}], "positional": [{"name": "root", "description": "the \"root\" position."}, {"name": "trunk", "description": "trunk value"}, {"name": "leaves", "description": "leaves. There can be many leaves."}], @@ -295,12 +288,11 @@ fn help_json_test_notes_examples_errors() { fields: Vec, } assert_help_json_string::( - vec!["--help-json"], + vec![], r###"{ "usage": "test_arg_0 []", "description": "Command with Examples and usage Notes, including error codes.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"options": [{"short": "", "long": "--help", "description": "display usage information"}], "positional": [{"name": "files", "description": "the \"root\" position."}], "examples": "\n Use the command with 1 file:\n\n `test_arg_0 /path/to/file`\n\n Use it with a \"wildcard\":\n\n `test_arg_0 /path/to/*`\n\n a blank line\n \n and one last line with \"quoted text\".", "notes": "\n These usage notes appear for test_arg_0 and how to best use it.\n The formatting should be preserved.\n one\n two\n three then a blank\n \n and one last line with \"quoted text\".", @@ -381,13 +373,12 @@ fn help_json_test_subcommands() { struct Command3Args {} assert_help_json_string::( - vec!["--help-json"], + vec![], r###"{ "usage": "test_arg_0 [--verbose] []", "description": "Top level command with \"subcommands\".", "options": [{"short": "", "long": "--verbose", "description": "show verbose output"}, -{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +{"short": "", "long": "--help", "description": "display usage information"}], "positional": [], "examples": "", "notes": "", @@ -400,12 +391,11 @@ fn help_json_test_subcommands() { ); assert_help_json_string::( - vec!["one", "--help-json"], + vec!["one"], r###"{ "usage": "test_arg_0 one []", "description": "Command1 args are used for Command1.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"options": [{"short": "", "long": "--help", "description": "display usage information"}], "positional": [{"name": "root", "description": "the \"root\" position."}, {"name": "trunk", "description": "trunk value"}, {"name": "leaves", "description": "leaves. There can be many leaves."}], @@ -418,7 +408,7 @@ fn help_json_test_subcommands() { ); assert_help_json_string::( - vec!["two", "--help-json"], + vec!["two"], r###"{ "usage": "test_arg_0 two [--power] --required [-s ] [--link ]", "description": "Command2 args are used for Command2.", @@ -426,8 +416,7 @@ fn help_json_test_subcommands() { {"short": "", "long": "--required", "description": "option that is required because of no default and not Option<>."}, {"short": "s", "long": "--speed", "description": "optional speed if not specified it is None."}, {"short": "", "long": "--link", "description": "repeatable option."}, -{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +{"short": "", "long": "--help", "description": "display usage information"}], "positional": [], "examples": "", "notes": "", @@ -438,12 +427,11 @@ fn help_json_test_subcommands() { ); assert_help_json_string::( - vec!["three", "--help-json"], + vec!["three"], r###"{ "usage": "test_arg_0 three", "description": "Command3 args are used for Command3 which has no options or arguments.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"options": [{"short": "", "long": "--help", "description": "display usage information"}], "positional": [], "examples": "", "notes": "", @@ -506,13 +494,12 @@ fn help_json_test_subcommand_notes_examples() { } assert_help_json_string::( - vec!["--help-json"], + vec![], r###"{ "usage": "test_arg_0 [--verbose] []", "description": "Top level command with \"subcommands\".", "options": [{"short": "", "long": "--verbose", "description": "show verbose output"}, -{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +{"short": "", "long": "--help", "description": "display usage information"}], "positional": [], "examples": "Top level example", "notes": "Top level note", @@ -523,12 +510,11 @@ fn help_json_test_subcommand_notes_examples() { ); assert_help_json_string::( - vec!["one", "--help-json"], + vec!["one"], r###"{ "usage": "test_arg_0 one []", "description": "Command1 args are used for subcommand one.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +"options": [{"short": "", "long": "--help", "description": "display usage information"}], "positional": [{"name": "root", "description": "the \"root\" position."}, {"name": "trunk", "description": "trunk value"}, {"name": "leaves", "description": "leaves. There can be many leaves."}], @@ -607,7 +593,7 @@ fn help_json_test_example() { } assert_help_json_string::( - vec!["--help-json"], + vec![], r###"{ "usage": "test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] []", "description": "Destroy the contents of with a specific \"method of destruction\".", @@ -615,8 +601,7 @@ fn help_json_test_example() { {"short": "", "long": "--really-really-really-long-name-for-pat", "description": "documentation"}, {"short": "s", "long": "--scribble", "description": "write repeatedly"}, {"short": "v", "long": "--verbose", "description": "say more. Defaults to $BLAST_VERBOSE."}, -{"short": "", "long": "--help", "description": "display usage information"}, -{"short": "", "long": "--help-json", "description": "display usage information encoded in JSON"}], +{"short": "", "long": "--help", "description": "display usage information"}], "positional": [], "examples": "Scribble 'abc' and then run |grind|.\n$ test_arg_0 -s 'abc' grind old.txt taxes.cp", "notes": "Use `test_arg_0 help ` for details on [] for a subcommand.", @@ -630,11 +615,7 @@ fn help_json_test_example() { } fn assert_help_json_string(args: Vec<&str>, help_str: &str) { - match T::from_args(&["test_arg_0"], &args) { - Ok(_) => panic!("help-json was parsed as args"), - Err(e) => { - assert_eq!(help_str, e.output); - e.status.expect("help-json returned an error"); - } - } + let actual_value = T::help_json_from_args(&["test_arg_0"], &args) + .expect("unexpected error getting help_json_from_args"); + assert_eq!(help_str, actual_value) } diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index 0baabb4..2fd0c51 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -118,7 +118,6 @@ Options: --s a switch with a description that is spread across a number of lines of comments. --help display usage information - --help-json display usage information encoded in JSON "###, ); } @@ -256,7 +255,6 @@ Woot Options: -n, --n fooey --help display usage information - --help-json display usage information encoded in JSON "###, ); } @@ -279,7 +277,6 @@ Woot Options: --option-name fooey --help display usage information - --help-json display usage information encoded in JSON "###, ); } @@ -318,7 +315,6 @@ Positional Arguments: Options: --help display usage information - --help-json display usage information encoded in JSON "###, ); } @@ -708,7 +704,6 @@ A type for testing `--help`/`help` Options: --help display usage information - --help-json display usage information encoded in JSON Commands: first First subcommmand for testing `help`. @@ -720,7 +715,6 @@ First subcommmand for testing `help`. Options: --help display usage information - --help-json display usage information encoded in JSON Commands: second Second subcommand for testing `help`. @@ -732,7 +726,6 @@ Second subcommand for testing `help`. Options: --help display usage information - --help-json display usage information encoded in JSON "###; #[test] @@ -878,7 +871,6 @@ Options: -s, --scribble write repeatedly -v, --verbose say more. Defaults to $BLAST_VERBOSE. --help display usage information - --help-json display usage information encoded in JSON Commands: blow-up explosively separate @@ -918,7 +910,6 @@ Positional Arguments: Options: --help display usage information - --help-json display usage information encoded in JSON "###, ); } @@ -1282,7 +1273,6 @@ Woot Options: -n, --n fooey --help display usage information - --help-json display usage information encoded in JSON "### .to_owned(), status: Ok(()), @@ -1364,6 +1354,11 @@ fn subcommand_does_not_panic() { argh::EarlyExit { output: "no subcommand name".into(), status: Err(()) }, ); + assert_eq!( + SubCommandEnum::help_json_from_args(&[], &["5"]).unwrap_err(), + argh::EarlyExit { output: "no subcommand name".into(), status: Err(()) }, + ); + // Passing unknown subcommand name to an emum assert_eq!( SubCommandEnum::from_args(&["fooey"], &["5"]).unwrap_err(), @@ -1375,7 +1370,14 @@ fn subcommand_does_not_panic() { argh::EarlyExit { output: "no subcommand matched".into(), status: Err(()) }, ); + assert_eq!( + SubCommandEnum::help_json_from_args(&["fooey"], &["5"]).unwrap_err(), + argh::EarlyExit { output: "no subcommand matched".into(), status: Err(()) }, + ); + // Passing unknown subcommand name to a struct + // Not testing from_args since it will assign 5 to x as a positional. + // Also not testing help_json_from_args since it is a valid to get help. assert_eq!( SubCommand::redact_arg_values(&[], &["5"]).unwrap_err(), argh::EarlyExit { output: "no subcommand name".into(), status: Err(()) }, diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs index 43c4c2c..afa3126 100644 --- a/argh_derive/src/help.rs +++ b/argh_derive/src/help.rs @@ -18,8 +18,6 @@ const SECTION_SEPARATOR: &str = "\n\n"; // Define constants for strings used for both help formats. pub(crate) const HELP_FLAG: &str = "--help"; pub(crate) const HELP_DESCRIPTION: &str = "display usage information"; -pub(crate) const HELP_JSON_FLAG: &str = "--help-json"; -pub(crate) const HELP_JSON_DESCRIPTION: &str = "display usage information encoded in JSON"; /// Returns a `TokenStream` generating a `String` help message. /// @@ -59,7 +57,6 @@ pub(crate) fn help( } // Also include "help" and "help-json" option_description_format(&mut format_lit, None, HELP_FLAG, HELP_DESCRIPTION); - option_description_format(&mut format_lit, None, HELP_JSON_FLAG, HELP_JSON_DESCRIPTION); let subcommand_calculation; let subcommand_format_arg; diff --git a/argh_derive/src/help_json.rs b/argh_derive/src/help_json.rs index b5e095d..564e828 100644 --- a/argh_derive/src/help_json.rs +++ b/argh_derive/src/help_json.rs @@ -5,10 +5,7 @@ use { crate::{ errors::Errors, - help::{ - build_usage_command_line, require_description, HELP_DESCRIPTION, HELP_FLAG, - HELP_JSON_DESCRIPTION, HELP_JSON_FLAG, - }, + help::{build_usage_command_line, require_description, HELP_DESCRIPTION, HELP_FLAG}, parse_attrs::{FieldKind, TypeAttrs}, StructField, }, @@ -123,11 +120,6 @@ pub(crate) fn help_json( long: String::from(HELP_FLAG), description: String::from(HELP_DESCRIPTION), }); - help_obj.options.push(OptionHelp { - short: String::from(""), - long: String::from(HELP_JSON_FLAG), - description: String::from(HELP_JSON_DESCRIPTION), - }); let subcommand_calculation; if let Some(subcommand) = subcommand { diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index 3aac34f..61e35a0 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -243,6 +243,8 @@ fn impl_from_args_struct( let redact_arg_values_method = impl_from_args_struct_redact_arg_values(errors, type_attrs, &fields); + let json_help_method = impl_help_json(errors, type_attrs, &fields); + let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs); let trait_impl = quote_spanned! { impl_span => @@ -251,6 +253,8 @@ fn impl_from_args_struct( #from_args_method #redact_arg_values_method + + #json_help_method } #top_or_sub_cmd_impl @@ -320,15 +324,12 @@ fn impl_from_args_struct_from_args<'a>( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); let help = help::help(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); - let help_json = - help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); let method_impl = quote_spanned! { impl_span => fn from_args(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result { #![allow(clippy::unwrap_in_result)] - #( #init_fields )* argh::parse_struct_args( @@ -350,8 +351,7 @@ fn impl_from_args_struct_from_args<'a>( last_is_repeating: #last_positional_is_repeating, }, #parse_subcommands, - &|| #help, - &|| #help_json + &|| #help )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); @@ -437,8 +437,6 @@ fn impl_from_args_struct_redact_arg_values<'a>( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); let help = help::help(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); - let help_json = - help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); let method_impl = quote_spanned! { impl_span => fn redact_arg_values(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result, argh::EarlyExit> { @@ -463,8 +461,7 @@ fn impl_from_args_struct_redact_arg_values<'a>( last_is_repeating: #last_positional_is_repeating, }, #redact_subcommands, - &|| #help, - &|| #help_json + &|| #help )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); @@ -490,6 +487,100 @@ fn impl_from_args_struct_redact_arg_values<'a>( method_impl } +fn impl_help_json<'a>( + errors: &Errors, + type_attrs: &TypeAttrs, + fields: &'a [StructField<'a>], +) -> TokenStream { + let init_fields = declare_local_storage_for_help_json_fields(&fields); + + let positional_fields: Vec<&StructField<'_>> = + fields.iter().filter(|field| field.kind == FieldKind::Positional).collect(); + let positional_field_idents = positional_fields.iter().map(|field| &field.field.ident); + let positional_field_names = positional_fields.iter().map(|field| field.name.to_string()); + let last_positional_is_repeating = positional_fields + .last() + .map(|field| field.optionality == Optionality::Repeating) + .unwrap_or(false); + + let flag_output_table = fields.iter().filter_map(|field| { + let field_name = &field.field.ident; + match field.kind { + FieldKind::Option => Some(quote! { argh::ParseStructOption::Value(&mut #field_name) }), + FieldKind::Switch => Some(quote! { argh::ParseStructOption::Flag(&mut #field_name) }), + FieldKind::SubCommand | FieldKind::Positional => None, + } + }); + + let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(&fields); + + let mut subcommands_iter = + fields.iter().filter(|field| field.kind == FieldKind::SubCommand).fuse(); + + let subcommand: Option<&StructField<'_>> = subcommands_iter.next(); + while let Some(dup_subcommand) = subcommands_iter.next() { + errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field); + } + + let impl_span = Span::call_site(); + + let parse_subcommands = if let Some(subcommand) = subcommand { + let ty = subcommand.ty_without_wrapper; + quote_spanned! { impl_span => + Some(argh::ParseStructSubCommand { + subcommands: <#ty as argh::SubCommands>::COMMANDS, + parse_func: &mut |__command, __remaining_args| { + help_string = <#ty as argh::FromArgs>::help_json_from_args(__command, __remaining_args)?; + Ok(()) + }, + }) + } + } else { + quote_spanned! { impl_span => None } + }; + + // Identifier referring to a value containing the name of the current command as an `&[&str]`. + let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); + let help_json = + help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); + + let method_impl = quote_spanned! { impl_span => + fn help_json_from_args(__cmd_name: &[&str], __args: &[&str]) + -> Result + { + let mut help_string : String = #help_json; + + #( #init_fields )* + + argh::parse_struct_args( + __cmd_name, + __args, + argh::ParseStructOptions { + arg_to_slot: &[ #( #flag_str_to_output_table_map ,)* ], + slots: &mut [ #( #flag_output_table, )* ], + }, + argh::ParseStructPositionals { + positionals: &mut [ + #( + argh::ParseStructPositional { + name: #positional_field_names, + slot: &mut #positional_field_idents as &mut argh::ParseValueSlot, + }, + )* + ], + last_is_repeating: #last_positional_is_repeating, + }, + #parse_subcommands, + &|| String::from(""), + )?; + + Ok(help_string) + } + }; + + method_impl +} + /// Ensures that only the last positional arg is non-required. fn ensure_only_last_positional_is_optional(errors: &Errors, fields: &[StructField<'_>]) { let mut first_non_required_span = None; @@ -725,6 +816,55 @@ fn unwrap_redacted_fields<'a>( }) } +/// Declare a local slots to store each field in during parsing. +/// +/// Most fields are stored in `Option` locals. +/// `argh(option)` fields are stored in a `ParseValueSlotTy` along with a +/// function that knows how to decode the appropriate value. +fn declare_local_storage_for_help_json_fields<'a>( + fields: &'a [StructField<'a>], +) -> impl Iterator + 'a { + fields.iter().map(|field| { + let field_name = &field.field.ident; + let field_type = &field.ty_without_wrapper; + + // Wrap field types in `Option` if they aren't already `Option` or `Vec`-wrapped. + let field_slot_type = match field.optionality { + Optionality::Optional | Optionality::Repeating => (&field.field.ty).into_token_stream(), + Optionality::None | Optionality::Defaulted(_) => { + quote! { std::option::Option<#field_type> } + } + }; + + match field.kind { + FieldKind::Option | FieldKind::Positional => { + let from_str_fn = match &field.attrs.from_str_fn { + Some(from_str_fn) => from_str_fn.into_token_stream(), + None => { + quote! { + <#field_type as argh::FromArgValue>::from_arg_value + } + } + }; + + quote! { + let mut #field_name: argh::ParseValueSlotTy<#field_slot_type, #field_type> + = argh::ParseValueSlotTy { + slot: std::default::Default::default(), + parse_func: |_, value| { #from_str_fn(value) }, + }; + } + } + FieldKind::SubCommand => { + quote! {} + } + FieldKind::Switch => { + quote! { let mut #field_name: #field_slot_type = argh::Flag::default(); } + } + } + }) +} + /// Entries of tokens like `("--some-flag-key", 5)` that map from a flag key string /// to an index in the output table. fn flag_str_to_output_table_map_entries<'a>(fields: &'a [StructField<'a>]) -> Vec { @@ -864,6 +1004,7 @@ fn impl_from_args_enum( .collect(); let name_repeating = std::iter::repeat(name.clone()); + let variant_ty = variants.iter().map(|x| x.ty).collect::>(); let variant_names = variants.iter().map(|x| x.name).collect::>(); @@ -901,9 +1042,25 @@ fn impl_from_args_enum( return <#variant_ty as argh::FromArgs>::redact_arg_values(command_name, args); } )* - Err(argh::EarlyExit::from("no subcommand matched".to_owned())) } + + fn help_json_from_args(command_name: &[&str], args: &[&str]) -> std::result::Result + { + let subcommand_name = if let Some(subcommand_name) = command_name.last() { + *subcommand_name + } else { + return Err(argh::EarlyExit::from("no subcommand name".to_owned())); + }; + + #( + if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name { + return #variant_ty::help_json_from_args(command_name, args) + ; + } + )* + Err(argh::EarlyExit::from("no subcommand matched".to_owned())) + } } impl argh::SubCommands for #name { From f00f7624e38ca17480ecfb5dc3ddf9bfa96ac08b Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Tue, 28 Dec 2021 17:55:42 -0800 Subject: [PATCH 08/13] Adding argh::help_json_from_args() This adds 2 methods to arghm, help_json_from_args() and help_json(), which is parallel to ::from_env(). The return value is a JSON encoded string suitable for parsing and using as input to generate reference docs. --- argh/src/lib.rs | 5 ++--- argh_derive/src/help.rs | 4 ++-- argh_derive/src/lib.rs | 8 ++++---- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 1e66dd0..047d78b 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -683,7 +683,7 @@ impl_flag_for_integers![u8, u16, u32, u64, u128, i8, i16, i32, i64, i128,]; /// `parse_options`: Helper to parse optional arguments. /// `parse_positionals`: Helper to parse positional arguments. /// `parse_subcommand`: Helper to parse a subcommand. -/// `help_func`: Generate a help message as plain text. +/// `help_func`: Generate a help message. #[doc(hidden)] pub fn parse_struct_args( cmd_name: &[&str], @@ -700,13 +700,12 @@ pub fn parse_struct_args( 'parse_args: while let Some(&next_arg) = remaining_args.get(0) { remaining_args = &remaining_args[1..]; - if (next_arg == "--help" || next_arg == "help") && !options_ended { help = true; continue; } - if next_arg.starts_with("-") && !options_ended { + if next_arg.starts_with('-') && !options_ended { if next_arg == "--" { options_ended = true; continue; diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs index afa3126..260925b 100644 --- a/argh_derive/src/help.rs +++ b/argh_derive/src/help.rs @@ -25,7 +25,7 @@ pub(crate) const HELP_DESCRIPTION: &str = "display usage information"; /// in favor of the `subcommand` argument. pub(crate) fn help( errors: &Errors, - cmd_name_str_array_ident: &syn::Ident, + cmd_name_str_array_ident: syn::Ident, ty_attrs: &TypeAttrs, fields: &[StructField<'_>], subcommand: Option<&StructField<'_>>, @@ -55,7 +55,7 @@ pub(crate) fn help( for option in options { option_description(errors, &mut format_lit, option); } - // Also include "help" and "help-json" + // Also include "help" option_description_format(&mut format_lit, None, HELP_FLAG, HELP_DESCRIPTION); let subcommand_calculation; diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index 61e35a0..1e70159 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -323,7 +323,7 @@ fn impl_from_args_struct_from_args<'a>( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); - let help = help::help(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help = help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand); let method_impl = quote_spanned! { impl_span => fn from_args(__cmd_name: &[&str], __args: &[&str]) @@ -351,7 +351,7 @@ fn impl_from_args_struct_from_args<'a>( last_is_repeating: #last_positional_is_repeating, }, #parse_subcommands, - &|| #help + &|| #help, )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); @@ -436,7 +436,7 @@ fn impl_from_args_struct_redact_arg_values<'a>( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); - let help = help::help(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help = help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand); let method_impl = quote_spanned! { impl_span => fn redact_arg_values(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result, argh::EarlyExit> { @@ -461,7 +461,7 @@ fn impl_from_args_struct_redact_arg_values<'a>( last_is_repeating: #last_positional_is_repeating, }, #redact_subcommands, - &|| #help + &|| #help, )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); From 03f0229d60c74fc87cea87c8f0f9312afbd1a5ff Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Tue, 28 Dec 2021 18:22:29 -0800 Subject: [PATCH 09/13] reverting unnessary changes. These were made inadvertently when merging changes. --- argh/src/lib.rs | 1 + argh_derive/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 047d78b..e31f0be 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -729,6 +729,7 @@ pub fn parse_struct_args( parse_positionals.parse(&mut positional_index, next_arg)?; } + if help { Err(EarlyExit { output: help_func(), status: Ok(()) }) } else { diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index 1e70159..dfad550 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -1004,7 +1004,6 @@ fn impl_from_args_enum( .collect(); let name_repeating = std::iter::repeat(name.clone()); - let variant_ty = variants.iter().map(|x| x.ty).collect::>(); let variant_names = variants.iter().map(|x| x.name).collect::>(); @@ -1042,6 +1041,7 @@ fn impl_from_args_enum( return <#variant_ty as argh::FromArgs>::redact_arg_values(command_name, args); } )* + Err(argh::EarlyExit::from("no subcommand matched".to_owned())) } From cca7412b278a929e70b3da8a11c7005713b60682 Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Tue, 28 Dec 2021 18:26:35 -0800 Subject: [PATCH 10/13] Addressing clippy warnings --- argh_derive/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index dfad550..19414dc 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -195,7 +195,7 @@ impl<'a> StructField<'a> { FieldKind::SubCommand | FieldKind::Positional => None, }; - Some(StructField { field, attrs, kind, optionality, ty_without_wrapper, name, long_name }) + Some(StructField { field, attrs, name, kind, ty_without_wrapper, optionality, long_name }) } pub(crate) fn arg_name(&self) -> String { @@ -518,7 +518,7 @@ fn impl_help_json<'a>( fields.iter().filter(|field| field.kind == FieldKind::SubCommand).fuse(); let subcommand: Option<&StructField<'_>> = subcommands_iter.next(); - while let Some(dup_subcommand) = subcommands_iter.next() { + for dup_subcommand in subcommands_iter { errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field); } From a2bd1b5dd651632f38b0b9f846b0ef62d3f1f0dd Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Wed, 29 Dec 2021 15:35:41 -0800 Subject: [PATCH 11/13] Consolidating help JSON schema and usage. This makes several changes that make it easier to use the JSON help data and also makes it more complete. * The subcommands are recursively called to collect the help information for each command. This means one call to help_json_from_args on the "TopLevel" struct will generate complete help for all subcommands. * `options` has been renamed to `flags`. * optionality has been added to the flags. This is "required", "optional", "repeated", or a Rust fragment containing for the default value. * "arg_name" as been added to flags. --- argh/src/lib.rs | 5 +- argh/tests/help_json_tests.rs | 257 ++++++++++++++++++++++------------ argh/tests/lib.rs | 10 +- argh_derive/src/help_json.rs | 89 ++++++++++-- argh_derive/src/lib.rs | 138 ++---------------- 5 files changed, 260 insertions(+), 239 deletions(-) diff --git a/argh/src/lib.rs b/argh/src/lib.rs index e31f0be..c6aff75 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -439,7 +439,7 @@ pub trait FromArgs: Sized { /// Returns a JSON encoded string of the usage information. This is intended to /// create a "machine readable" version of the help text to enable reference /// documentation generation. - fn help_json_from_args(command_name: &[&str], args: &[&str]) -> Result; + fn help_json_from_args(command_name: &[&str]) -> Result; /// Returns a JSON encoded string of the usage information based on the command line /// found in argv, identical to `::from_env()`. This is intended to @@ -448,8 +448,7 @@ pub trait FromArgs: Sized { fn help_json() -> Result { let strings: Vec = std::env::args().collect(); let cmd = cmd(&strings[0], &strings[0]); - let strs: Vec<&str> = strings.iter().map(|s| s.as_str()).collect(); - Self::help_json_from_args(&[cmd], &strs[1..]) + Self::help_json_from_args(&[cmd]) } } diff --git a/argh/tests/help_json_tests.rs b/argh/tests/help_json_tests.rs index 7d0c500..0015445 100644 --- a/argh/tests/help_json_tests.rs +++ b/argh/tests/help_json_tests.rs @@ -43,26 +43,52 @@ fn help_json_test_subcommand() { assert_help_json_string::( vec![], r###"{ +"name": "test_arg_0", "usage": "test_arg_0 []", "description": "Top-level command.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}], +"flags": [{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], "positional": [], "examples": "", "notes": "", "error_codes": [], -"subcommands": [{"name": "one", "description": "First subcommand."}, -{"name": "two", "description": "Second subcommand."}] +"subcommands": [{ +"name": "one", +"usage": "test_arg_0 one --x ", +"description": "First subcommand.", +"flags": [{"short": "", "long": "--x", "description": "how many x", "arg_name": "x", "optionality": "required"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +, +{ +"name": "two", +"usage": "test_arg_0 two [--fooey]", +"description": "Second subcommand.", +"flags": [{"short": "", "long": "--fooey", "description": "whether to fooey", "arg_name": "", "optionality": "optional"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +] } "###, ); - assert_help_json_string::( + assert_help_json_string::( vec!["one"], r###"{ +"name": "one", "usage": "test_arg_0 one --x ", "description": "First subcommand.", -"options": [{"short": "", "long": "--x", "description": "how many x"}, -{"short": "", "long": "--help", "description": "display usage information"}], +"flags": [{"short": "", "long": "--x", "description": "how many x", "arg_name": "x", "optionality": "required"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], "positional": [], "examples": "", "notes": "", @@ -88,10 +114,11 @@ fn help_json_test_multiline_doc_comment() { assert_help_json_string::( vec![], r###"{ +"name": "test_arg_0", "usage": "test_arg_0 [--s]", "description": "Short description", -"options": [{"short": "", "long": "--s", "description": "a switch with a description that is spread across a number of lines of comments."}, -{"short": "", "long": "--help", "description": "display usage information"}], +"flags": [{"short": "", "long": "--s", "description": "a switch with a description that is spread across a number of lines of comments.", "arg_name": "", "optionality": "optional"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], "positional": [], "examples": "", "notes": "", @@ -127,13 +154,14 @@ fn help_json_test_basic_args() { assert_help_json_string::( vec![], r###"{ +"name": "test_arg_0", "usage": "test_arg_0 [--power] --required [-s ] [--link ]", "description": "Basic command args demonstrating multiple types and cardinality. \"With quotes\"", -"options": [{"short": "", "long": "--power", "description": "should the power be on. \"Quoted value\" should work too."}, -{"short": "", "long": "--required", "description": "option that is required because of no default and not Option<>."}, -{"short": "s", "long": "--speed", "description": "optional speed if not specified it is None."}, -{"short": "", "long": "--link", "description": "repeatable option."}, -{"short": "", "long": "--help", "description": "display usage information"}], +"flags": [{"short": "", "long": "--power", "description": "should the power be on. \"Quoted value\" should work too.", "arg_name": "", "optionality": "optional"}, +{"short": "", "long": "--required", "description": "option that is required because of no default and not Option<>.", "arg_name": "required_flag", "optionality": "required"}, +{"short": "s", "long": "--speed", "description": "optional speed if not specified it is None.", "arg_name": "speed", "optionality": "optional"}, +{"short": "", "long": "--link", "description": "repeatable option.", "arg_name": "url", "optionality": "repeating"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], "positional": [], "examples": "", "notes": "", @@ -144,6 +172,9 @@ fn help_json_test_basic_args() { ); } +/* +[{\"short\": \"\", \"long\": \"--power\", \"description\": \"should the power be on. \\\"Quoted value\\\" should work too.\", \"arg_name\": \"\", \"optionality\": \"optional\"},\n{\"short\": \"\", \"long\": \"--required\", \"description\": \"option that is required because of no default and not Option<>.\", \"arg_name\": \"required_flag\", \"optionality\": \"required\"},\n{\"short\": \"s\", \"long\": \"--speed\", \"description\": \"optional speed if not specified it is None.\", \"arg_name\": \"speed\", \"optionality\": \"optional\"},\n{\"short\": \"\", \"long\": \"--link\", \"description\": \"repeatable option.\", \"arg_name\": \"url\", \"optionality\": \"repeating\"},\n{\"short\": \"\", \"long\": \"--help\", \"description\": \"display usage information\", \"arg_name\": \"\", \"optionality\": \"optional\"}],\n\"positional\": [],\n\"examples\": \"\",\n\"notes\": \"\",\n\"error_codes\": [],\n\"subcommands\": []\n}\n +*/ #[test] fn help_json_test_positional_args() { #[allow(dead_code)] @@ -165,12 +196,13 @@ fn help_json_test_positional_args() { assert_help_json_string::( vec![], r###"{ +"name": "test_arg_0", "usage": "test_arg_0 []", "description": "Command with positional args demonstrating. \"With quotes\"", -"options": [{"short": "", "long": "--help", "description": "display usage information"}], -"positional": [{"name": "root", "description": "the \"root\" position."}, -{"name": "trunk", "description": "trunk value"}, -{"name": "leaves", "description": "leaves. There can be many leaves."}], +"flags": [{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [{"name": "root", "description": "the \"root\" position.", "optionality": "required"}, +{"name": "trunk", "description": "trunk value", "optionality": "required"}, +{"name": "leaves", "description": "leaves. There can be many leaves.", "optionality": "repeating"}], "examples": "", "notes": "", "error_codes": [], @@ -201,12 +233,13 @@ fn help_json_test_optional_positional_args() { assert_help_json_string::( vec![], r###"{ +"name": "test_arg_0", "usage": "test_arg_0 []", "description": "Command with positional args demonstrating last value is optional", -"options": [{"short": "", "long": "--help", "description": "display usage information"}], -"positional": [{"name": "root", "description": "the \"root\" position."}, -{"name": "trunk", "description": "trunk value"}, -{"name": "leaves", "description": "leaves. There can be many leaves."}], +"flags": [{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [{"name": "root", "description": "the \"root\" position.", "optionality": "required"}, +{"name": "trunk", "description": "trunk value", "optionality": "required"}, +{"name": "leaves", "description": "leaves. There can be many leaves.", "optionality": "optional"}], "examples": "", "notes": "", "error_codes": [], @@ -237,12 +270,13 @@ fn help_json_test_default_positional_args() { assert_help_json_string::( vec![], r###"{ +"name": "test_arg_0", "usage": "test_arg_0 []", "description": "Command with positional args demonstrating last value is defaulted.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}], -"positional": [{"name": "root", "description": "the \"root\" position."}, -{"name": "trunk", "description": "trunk value"}, -{"name": "leaves", "description": "leaves. There can be many leaves."}], +"flags": [{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [{"name": "root", "description": "the \"root\" position.", "optionality": "required"}, +{"name": "trunk", "description": "trunk value", "optionality": "required"}, +{"name": "leaves", "description": "leaves. There can be many leaves.", "optionality": "String :: from(\"hello\")"}], "examples": "", "notes": "", "error_codes": [], @@ -290,10 +324,11 @@ fn help_json_test_notes_examples_errors() { assert_help_json_string::( vec![], r###"{ +"name": "test_arg_0", "usage": "test_arg_0 []", "description": "Command with Examples and usage Notes, including error codes.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}], -"positional": [{"name": "files", "description": "the \"root\" position."}], +"flags": [{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [{"name": "files", "description": "the \"root\" position.", "optionality": "repeating"}], "examples": "\n Use the command with 1 file:\n\n `test_arg_0 /path/to/file`\n\n Use it with a \"wildcard\":\n\n `test_arg_0 /path/to/*`\n\n a blank line\n \n and one last line with \"quoted text\".", "notes": "\n These usage notes appear for test_arg_0 and how to best use it.\n The formatting should be preserved.\n one\n two\n three then a blank\n \n and one last line with \"quoted text\".", "error_codes": [{"name": "0", "description": "Success"}, @@ -375,69 +410,58 @@ fn help_json_test_subcommands() { assert_help_json_string::( vec![], r###"{ +"name": "test_arg_0", "usage": "test_arg_0 [--verbose] []", "description": "Top level command with \"subcommands\".", -"options": [{"short": "", "long": "--verbose", "description": "show verbose output"}, -{"short": "", "long": "--help", "description": "display usage information"}], +"flags": [{"short": "", "long": "--verbose", "description": "show verbose output", "arg_name": "", "optionality": "optional"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], "positional": [], "examples": "", "notes": "", "error_codes": [], -"subcommands": [{"name": "one", "description": "Command1 args are used for Command1."}, -{"name": "two", "description": "Command2 args are used for Command2."}, -{"name": "three", "description": "Command3 args are used for Command3 which has no options or arguments."}] -} -"###, - ); - - assert_help_json_string::( - vec!["one"], - r###"{ +"subcommands": [{ +"name": "one", "usage": "test_arg_0 one []", "description": "Command1 args are used for Command1.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}], -"positional": [{"name": "root", "description": "the \"root\" position."}, -{"name": "trunk", "description": "trunk value"}, -{"name": "leaves", "description": "leaves. There can be many leaves."}], +"flags": [{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [{"name": "root", "description": "the \"root\" position.", "optionality": "required"}, +{"name": "trunk", "description": "trunk value", "optionality": "required"}, +{"name": "leaves", "description": "leaves. There can be many leaves.", "optionality": "String :: from(\"hello\")"}], "examples": "", "notes": "", "error_codes": [], "subcommands": [] } -"###, - ); - - assert_help_json_string::( - vec!["two"], - r###"{ +, +{ +"name": "two", "usage": "test_arg_0 two [--power] --required [-s ] [--link ]", "description": "Command2 args are used for Command2.", -"options": [{"short": "", "long": "--power", "description": "should the power be on. \"Quoted value\" should work too."}, -{"short": "", "long": "--required", "description": "option that is required because of no default and not Option<>."}, -{"short": "s", "long": "--speed", "description": "optional speed if not specified it is None."}, -{"short": "", "long": "--link", "description": "repeatable option."}, -{"short": "", "long": "--help", "description": "display usage information"}], +"flags": [{"short": "", "long": "--power", "description": "should the power be on. \"Quoted value\" should work too.", "arg_name": "", "optionality": "optional"}, +{"short": "", "long": "--required", "description": "option that is required because of no default and not Option<>.", "arg_name": "required_flag", "optionality": "required"}, +{"short": "s", "long": "--speed", "description": "optional speed if not specified it is None.", "arg_name": "speed", "optionality": "optional"}, +{"short": "", "long": "--link", "description": "repeatable option.", "arg_name": "url", "optionality": "repeating"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], "positional": [], "examples": "", "notes": "", "error_codes": [], "subcommands": [] } -"###, - ); - - assert_help_json_string::( - vec!["three"], - r###"{ +, +{ +"name": "three", "usage": "test_arg_0 three", "description": "Command3 args are used for Command3 which has no options or arguments.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}], +"flags": [{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], "positional": [], "examples": "", "notes": "", "error_codes": [], "subcommands": [] } +] +} "###, ); } @@ -496,28 +520,43 @@ fn help_json_test_subcommand_notes_examples() { assert_help_json_string::( vec![], r###"{ +"name": "test_arg_0", "usage": "test_arg_0 [--verbose] []", "description": "Top level command with \"subcommands\".", -"options": [{"short": "", "long": "--verbose", "description": "show verbose output"}, -{"short": "", "long": "--help", "description": "display usage information"}], +"flags": [{"short": "", "long": "--verbose", "description": "show verbose output", "arg_name": "", "optionality": "optional"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], "positional": [], "examples": "Top level example", "notes": "Top level note", "error_codes": [{"name": "0", "description": "Top level success"}], -"subcommands": [{"name": "one", "description": "Command1 args are used for subcommand one."}] +"subcommands": [{ +"name": "one", +"usage": "test_arg_0 one []", +"description": "Command1 args are used for subcommand one.", +"flags": [{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [{"name": "root", "description": "the \"root\" position.", "optionality": "required"}, +{"name": "trunk", "description": "trunk value", "optionality": "required"}, +{"name": "leaves", "description": "leaves. There can be many leaves.", "optionality": "String :: from(\"hello\")"}], +"examples": "\"Typical\" usage is `test_arg_0 one`.", +"notes": "test_arg_0 one is used as a subcommand of \"Top level\"", +"error_codes": [{"name": "0", "description": "one level success"}], +"subcommands": [] +} +] } "###, ); - assert_help_json_string::( + assert_help_json_string::( vec!["one"], r###"{ +"name": "one", "usage": "test_arg_0 one []", "description": "Command1 args are used for subcommand one.", -"options": [{"short": "", "long": "--help", "description": "display usage information"}], -"positional": [{"name": "root", "description": "the \"root\" position."}, -{"name": "trunk", "description": "trunk value"}, -{"name": "leaves", "description": "leaves. There can be many leaves."}], +"flags": [{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [{"name": "root", "description": "the \"root\" position.", "optionality": "required"}, +{"name": "trunk", "description": "trunk value", "optionality": "required"}, +{"name": "leaves", "description": "leaves. There can be many leaves.", "optionality": "String :: from(\"hello\")"}], "examples": "\"Typical\" usage is `test_arg_0 one`.", "notes": "test_arg_0 one is used as a subcommand of \"Top level\"", "error_codes": [{"name": "0", "description": "one level success"}], @@ -527,16 +566,6 @@ fn help_json_test_subcommand_notes_examples() { ); } -/// Test that descriptions can start with an initialism despite -/// usually being required to start with a lowercase letter. -#[derive(FromArgs)] -#[allow(unused)] -struct DescriptionStartsWithInitialism { - /// URL fooey - #[argh(option)] - x: u8, -} - #[test] fn help_json_test_example() { #[derive(FromArgs, PartialEq, Debug)] @@ -595,27 +624,81 @@ fn help_json_test_example() { assert_help_json_string::( vec![], r###"{ +"name": "test_arg_0", "usage": "test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] []", "description": "Destroy the contents of with a specific \"method of destruction\".", -"options": [{"short": "f", "long": "--force", "description": "force, ignore minor errors. This description is so long that it wraps to the next line."}, -{"short": "", "long": "--really-really-really-long-name-for-pat", "description": "documentation"}, -{"short": "s", "long": "--scribble", "description": "write repeatedly"}, -{"short": "v", "long": "--verbose", "description": "say more. Defaults to $BLAST_VERBOSE."}, -{"short": "", "long": "--help", "description": "display usage information"}], +"flags": [{"short": "f", "long": "--force", "description": "force, ignore minor errors. This description is so long that it wraps to the next line.", "arg_name": "", "optionality": "optional"}, +{"short": "", "long": "--really-really-really-long-name-for-pat", "description": "documentation", "arg_name": "", "optionality": "optional"}, +{"short": "s", "long": "--scribble", "description": "write repeatedly", "arg_name": "scribble", "optionality": "required"}, +{"short": "v", "long": "--verbose", "description": "say more. Defaults to $BLAST_VERBOSE.", "arg_name": "", "optionality": "optional"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], "positional": [], "examples": "Scribble 'abc' and then run |grind|.\n$ test_arg_0 -s 'abc' grind old.txt taxes.cp", "notes": "Use `test_arg_0 help ` for details on [] for a subcommand.", "error_codes": [{"name": "2", "description": "The blade is too dull."}, {"name": "3", "description": "Out of fuel."}], -"subcommands": [{"name": "blow-up", "description": "explosively separate"}, -{"name": "grind", "description": "make smaller by many small cuts"}] +"subcommands": [{ +"name": "blow-up", +"usage": "test_arg_0 blow-up [--safely]", +"description": "explosively separate", +"flags": [{"short": "", "long": "--safely", "description": "blow up bombs safely", "arg_name": "", "optionality": "optional"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +, +{ +"name": "grind", +"usage": "test_arg_0 grind [--safely]", +"description": "make smaller by many small cuts", +"flags": [{"short": "", "long": "--safely", "description": "wear a visor while grinding", "arg_name": "", "optionality": "optional"}, +{"short": "", "long": "--help", "description": "display usage information", "arg_name": "", "optionality": "optional"}], +"positional": [], +"examples": "", +"notes": "", +"error_codes": [], +"subcommands": [] +} +] } "###, ); } +/* +{ +\"name\": \"test_arg_0\", +\"usage\": \"test_arg_0 [-f] [--really-really-really-long-name-for-pat] -s [-v] []\", +\"description\": \"Destroy the contents of with a specific \\\"method of destruction\\\".\", +\"flags\": [{\"short\": \"f\", \"long\": \"--force\", \"description\": \"force, ignore minor errors. This description is so long that it wraps to the next line.\", \"arg_name\": \"\", \"optionality\": \"optional\"}, +{\"short\": \"\", \"long\": \"--really-really-really-long-name-for-pat\", \"description\": \"documentation\", \"arg_name\": \"\", \"optionality\": \"optional\"}, +{\"short\": \"s\", \"long\": \"--scribble\", \"description\": \"write repeatedly\", \"arg_name\": \"scribble\", \"optionality\": \"required\"}, +{\"short\": \"v\", \"long\": \"--verbose\", \"description\": \"say more. Defaults to $BLAST_VERBOSE.\", \"arg_name\": \"\", \"optionality\": \"optional\"}, +{\"short\": \"\", \"long\": \"--help\", \"description\": \"display usage information\", \"arg_name\": \"\", \"optionality\": \"optional\"}] +,\n\"positional\": [], +\"examples\": \"Scribble 'abc' and then run |grind|.\\n$ test_arg_0 -s 'abc' grind old.txt taxes.cp\",\n\"notes\": \"Use `test_arg_0 help ` for details on [] for a subcommand.\", +\"error_codes\": [{\"name\": \"2\", \"description\": \"The blade is too dull.\", \"optionality\": \"\"},\n{\"name\": \"3\", \"description\": \"Out of fuel.\", \"optionality\": \"\"}], +\"subcommands\": [{\n\"name\": \"blow-up\",\n\"usage\": \"test_arg_0 blow-up [--safely]\", +\"description\": \"explosively separate\", +\"flags\": [{\"short\": \"\", \"long\": \"--safely\", \"description\": \"blow up bombs safely\", \"arg_name\": \"\", \"optionality\": \"optional\"}, +{\"short\": \"\", \"long\": \"--help\", \"description\": \"display usage information\", \"arg_name\": \"\", \"optionality\": \"optional\"}], +\"positional\": [], +\"examples\": \"\", +\"notes\": \"\", +\"error_codes\": [],\n\"subcommands\": []\n}\n,\n{ + \"name\": \"grind\",\n\"usage\": \"test_arg_0 grind [--safely]\",\n\"description\": \"make smaller by many small cuts\", + \"flags\": [{\"short\": \"\", \"long\": \"--safely\", \"description\": \"wear a visor while grinding\", \"arg_name\": \"\", \"optionality\": \"optional\"},\n{\"short\": \"\", \"long\": \"--help\", \"description\": \"display usage information\", \"arg_name\": \"\", \"optionality\": \"optional\"}], +\"positional\": [],\n\"examples\": \"\",\n\"notes\": \"\",\n\"error_codes\": [],\n\"subcommands\": []\n}\n]\n} + +*/ + fn assert_help_json_string(args: Vec<&str>, help_str: &str) { - let actual_value = T::help_json_from_args(&["test_arg_0"], &args) + let mut command_args = vec!["test_arg_0"]; + command_args.extend_from_slice(&args); + let actual_value = T::help_json_from_args(&command_args) .expect("unexpected error getting help_json_from_args"); assert_eq!(help_str, actual_value) } diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index 2fd0c51..44d4eaf 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -1355,24 +1355,24 @@ fn subcommand_does_not_panic() { ); assert_eq!( - SubCommandEnum::help_json_from_args(&[], &["5"]).unwrap_err(), + SubCommandEnum::help_json_from_args(&[]).unwrap_err(), argh::EarlyExit { output: "no subcommand name".into(), status: Err(()) }, ); // Passing unknown subcommand name to an emum assert_eq!( SubCommandEnum::from_args(&["fooey"], &["5"]).unwrap_err(), - argh::EarlyExit { output: "no subcommand matched".into(), status: Err(()) }, + argh::EarlyExit { output: "no subcommand matched fooey".into(), status: Err(()) }, ); assert_eq!( SubCommandEnum::redact_arg_values(&["fooey"], &["5"]).unwrap_err(), - argh::EarlyExit { output: "no subcommand matched".into(), status: Err(()) }, + argh::EarlyExit { output: "no subcommand matched fooey".into(), status: Err(()) }, ); assert_eq!( - SubCommandEnum::help_json_from_args(&["fooey"], &["5"]).unwrap_err(), - argh::EarlyExit { output: "no subcommand matched".into(), status: Err(()) }, + SubCommandEnum::help_json_from_args(&["fooey"]).unwrap_err(), + argh::EarlyExit { output: "no subcommand matched fooey".into(), status: Err(()) }, ); // Passing unknown subcommand name to a struct diff --git a/argh_derive/src/help_json.rs b/argh_derive/src/help_json.rs index 564e828..8a11ef5 100644 --- a/argh_derive/src/help_json.rs +++ b/argh_derive/src/help_json.rs @@ -7,7 +7,7 @@ use { errors::Errors, help::{build_usage_command_line, require_description, HELP_DESCRIPTION, HELP_FLAG}, parse_attrs::{FieldKind, TypeAttrs}, - StructField, + Optionality, StructField, }, proc_macro2::{Span, TokenStream}, quote::quote, @@ -17,11 +17,14 @@ struct OptionHelp { short: String, long: String, description: String, + arg_name: String, + optionality: String, } struct PositionalHelp { name: String, description: String, + optionality: String, } struct HelpJSON { usage: String, @@ -40,25 +43,31 @@ fn option_elements_json(options: &[OptionHelp]) -> String { retval.push_str(",\n"); } retval.push_str(&format!( - "{{\"short\": \"{}\", \"long\": \"{}\", \"description\": \"{}\"}}", + "{{\"short\": \"{}\", \"long\": \"{}\", \"description\": \"{}\", \"arg_name\": \"{}\", \"optionality\": \"{}\"}}", opt.short, opt.long, - escape_json(&opt.description) + escape_json(&opt.description), + opt.arg_name, + escape_json(&opt.optionality) )); } retval } -fn help_elements_json(elements: &[PositionalHelp]) -> String { +fn help_elements_json(elements: &[PositionalHelp], skip_optionality: bool) -> String { let mut retval = String::from(""); for pos in elements { if !retval.is_empty() { retval.push_str(",\n"); } retval.push_str(&format!( - "{{\"name\": \"{}\", \"description\": \"{}\"}}", + "{{\"name\": \"{}\", \"description\": \"{}\"", pos.name, - escape_json(&pos.description) + escape_json(&pos.description), )); + if !skip_optionality { + retval.push_str(&format!(", \"optionality\": \"{}\"", escape_json(&pos.optionality))); + } + retval.push('}'); } retval } @@ -72,9 +81,13 @@ pub(crate) fn help_json( cmd_name_str_array_ident: &syn::Ident, ty_attrs: &TypeAttrs, fields: &[StructField<'_>], - subcommand: Option<&StructField<'_>>, ) -> TokenStream { let mut usage_format_pattern = "{command_name}".to_string(); + + let mut subcommands_iter = + fields.iter().filter(|field| field.kind == FieldKind::SubCommand).fuse(); + let subcommand: Option<&StructField<'_>> = subcommands_iter.next(); + build_usage_command_line(&mut usage_format_pattern, fields, subcommand); let mut help_obj = HelpJSON { @@ -94,12 +107,26 @@ pub(crate) fn help_json( if let Some(desc) = &arg.attrs.description { description = desc.content.value().trim().to_owned(); } - help_obj.positional_args.push(PositionalHelp { name: arg.arg_name(), description }); + let optionality = match &arg.optionality { + Optionality::None => String::from("required"), + Optionality::Optional => String::from("optional"), + Optionality::Repeating => String::from("repeating"), + Optionality::Defaulted(ts) => ts.to_string(), + }; + help_obj.positional_args.push(PositionalHelp { + name: arg.arg_name(), + description, + optionality, + }); } // Add options to the help object. let options = fields.iter().filter(|f| f.long_name.is_some()); for option in options { + let field_kind = match &option.attrs.field_type { + Some(field_type) => field_type.kind, + _ => unreachable!("Field type not set"), + }; let short = match option.attrs.short.as_ref().map(|s| s.value()) { Some(c) => String::from(c), None => String::from(""), @@ -108,17 +135,37 @@ pub(crate) fn help_json( option.long_name.as_ref().expect("missing long name for option"); let description = require_description(errors, option.name.span(), &option.attrs.description, "field"); + + let arg_name = match field_kind { + FieldKind::Option | FieldKind::Positional => match &option.attrs.arg_name { + Some(_) => option.arg_name(), + // None if field_kind != FieldKind::Switch => option.name.to_string(), + None => option.name.to_string(), + }, + FieldKind::Switch | FieldKind::SubCommand => String::from(""), + }; + + let optionality = match &option.optionality { + Optionality::None => String::from("required"), + Optionality::Optional => String::from("optional"), + Optionality::Repeating => String::from("repeating"), + Optionality::Defaulted(ts) => ts.to_string(), + }; help_obj.options.push(OptionHelp { short, long: long_with_leading_dashes.to_owned(), description, + arg_name, + optionality, }); } - // Also include "help" and "help-json" + // Also include "help" help_obj.options.push(OptionHelp { short: String::from(""), long: String::from(HELP_FLAG), description: String::from(HELP_DESCRIPTION), + arg_name: String::from(""), + optionality: String::from("optional"), }); let subcommand_calculation; @@ -130,8 +177,9 @@ pub(crate) fn help_json( if !subcommands.is_empty() { subcommands.push_str(",\n"); } - subcommands.push_str(&format!("{{\"name\": \"{}\", \"description\": \"{}\"}}", - cmd.name, cmd.description)); + let mut command = __cmd_name.to_owned(); + command.push(cmd.name); + subcommands.push_str(&<#subcommand_ty as argh::FromArgs>::help_json_from_args(&command)?); } }; } else { @@ -162,13 +210,14 @@ pub(crate) fn help_json( help_obj.error_codes.push(PositionalHelp { name: code.to_string(), description: text.value().to_string(), + optionality: String::from(""), }); } } let help_options_json = option_elements_json(&help_obj.options); - let help_positional_json = help_elements_json(&help_obj.positional_args); - let help_error_codes_json = help_elements_json(&help_obj.error_codes); + let help_positional_json = help_elements_json(&help_obj.positional_args, false); + let help_error_codes_json = help_elements_json(&help_obj.error_codes, true); let help_description = escape_json(&help_obj.description); let help_examples: TokenStream; @@ -196,16 +245,28 @@ pub(crate) fn help_json( }; } + let name_string: TokenStream; + if let Some(name) = &ty_attrs.name { + name_string = quote! { json_help_string.push_str(#name);}; + } else { + name_string = quote! { + json_help_string.push_str(&#cmd_name_str_array_ident.join(" ")); + }; + } + quote! {{ #subcommand_calculation // Build up the string for json. The name of the command needs to be dereferenced, so it // can't be done in the macro. let mut json_help_string = "{\n".to_string(); + json_help_string.push_str("\"name\": \""); + #name_string; + json_help_string.push_str("\",\n"); let usage_value = format!(#usage_format_pattern,command_name = #cmd_name_str_array_ident.join(" ")); json_help_string.push_str(&format!("\"usage\": \"{}\",\n",usage_value)); json_help_string.push_str(&format!("\"description\": \"{}\",\n", #help_description)); - json_help_string.push_str(&format!("\"options\": [{}],\n", #help_options_json)); + json_help_string.push_str(&format!("\"flags\": [{}],\n", #help_options_json)); json_help_string.push_str(&format!("\"positional\": [{}],\n", #help_positional_json)); json_help_string.push_str("\"examples\": \""); #help_examples; diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index 19414dc..c79ae03 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -278,7 +278,6 @@ fn impl_from_args_struct_from_args<'a>( .last() .map(|field| field.optionality == Optionality::Repeating) .unwrap_or(false); - let flag_output_table = fields.iter().filter_map(|field| { let field_name = &field.field.ident; match field.kind { @@ -492,89 +491,17 @@ fn impl_help_json<'a>( type_attrs: &TypeAttrs, fields: &'a [StructField<'a>], ) -> TokenStream { - let init_fields = declare_local_storage_for_help_json_fields(&fields); - - let positional_fields: Vec<&StructField<'_>> = - fields.iter().filter(|field| field.kind == FieldKind::Positional).collect(); - let positional_field_idents = positional_fields.iter().map(|field| &field.field.ident); - let positional_field_names = positional_fields.iter().map(|field| field.name.to_string()); - let last_positional_is_repeating = positional_fields - .last() - .map(|field| field.optionality == Optionality::Repeating) - .unwrap_or(false); - - let flag_output_table = fields.iter().filter_map(|field| { - let field_name = &field.field.ident; - match field.kind { - FieldKind::Option => Some(quote! { argh::ParseStructOption::Value(&mut #field_name) }), - FieldKind::Switch => Some(quote! { argh::ParseStructOption::Flag(&mut #field_name) }), - FieldKind::SubCommand | FieldKind::Positional => None, - } - }); - - let flag_str_to_output_table_map = flag_str_to_output_table_map_entries(&fields); - - let mut subcommands_iter = - fields.iter().filter(|field| field.kind == FieldKind::SubCommand).fuse(); - - let subcommand: Option<&StructField<'_>> = subcommands_iter.next(); - for dup_subcommand in subcommands_iter { - errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field); - } - let impl_span = Span::call_site(); - let parse_subcommands = if let Some(subcommand) = subcommand { - let ty = subcommand.ty_without_wrapper; - quote_spanned! { impl_span => - Some(argh::ParseStructSubCommand { - subcommands: <#ty as argh::SubCommands>::COMMANDS, - parse_func: &mut |__command, __remaining_args| { - help_string = <#ty as argh::FromArgs>::help_json_from_args(__command, __remaining_args)?; - Ok(()) - }, - }) - } - } else { - quote_spanned! { impl_span => None } - }; - // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); - let help_json = - help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, &fields, subcommand); + let help_json = help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, &fields); let method_impl = quote_spanned! { impl_span => - fn help_json_from_args(__cmd_name: &[&str], __args: &[&str]) + fn help_json_from_args(__cmd_name: &[&str]) -> Result { - let mut help_string : String = #help_json; - - #( #init_fields )* - - argh::parse_struct_args( - __cmd_name, - __args, - argh::ParseStructOptions { - arg_to_slot: &[ #( #flag_str_to_output_table_map ,)* ], - slots: &mut [ #( #flag_output_table, )* ], - }, - argh::ParseStructPositionals { - positionals: &mut [ - #( - argh::ParseStructPositional { - name: #positional_field_names, - slot: &mut #positional_field_idents as &mut argh::ParseValueSlot, - }, - )* - ], - last_is_repeating: #last_positional_is_repeating, - }, - #parse_subcommands, - &|| String::from(""), - )?; - - Ok(help_string) + Ok(String::from(#help_json)) } }; @@ -816,55 +743,6 @@ fn unwrap_redacted_fields<'a>( }) } -/// Declare a local slots to store each field in during parsing. -/// -/// Most fields are stored in `Option` locals. -/// `argh(option)` fields are stored in a `ParseValueSlotTy` along with a -/// function that knows how to decode the appropriate value. -fn declare_local_storage_for_help_json_fields<'a>( - fields: &'a [StructField<'a>], -) -> impl Iterator + 'a { - fields.iter().map(|field| { - let field_name = &field.field.ident; - let field_type = &field.ty_without_wrapper; - - // Wrap field types in `Option` if they aren't already `Option` or `Vec`-wrapped. - let field_slot_type = match field.optionality { - Optionality::Optional | Optionality::Repeating => (&field.field.ty).into_token_stream(), - Optionality::None | Optionality::Defaulted(_) => { - quote! { std::option::Option<#field_type> } - } - }; - - match field.kind { - FieldKind::Option | FieldKind::Positional => { - let from_str_fn = match &field.attrs.from_str_fn { - Some(from_str_fn) => from_str_fn.into_token_stream(), - None => { - quote! { - <#field_type as argh::FromArgValue>::from_arg_value - } - } - }; - - quote! { - let mut #field_name: argh::ParseValueSlotTy<#field_slot_type, #field_type> - = argh::ParseValueSlotTy { - slot: std::default::Default::default(), - parse_func: |_, value| { #from_str_fn(value) }, - }; - } - } - FieldKind::SubCommand => { - quote! {} - } - FieldKind::Switch => { - quote! { let mut #field_name: #field_slot_type = argh::Flag::default(); } - } - } - }) -} - /// Entries of tokens like `("--some-flag-key", 5)` that map from a flag key string /// to an index in the output table. fn flag_str_to_output_table_map_entries<'a>(fields: &'a [StructField<'a>]) -> Vec { @@ -1026,7 +904,7 @@ fn impl_from_args_enum( } )* - Err(argh::EarlyExit::from("no subcommand matched".to_owned())) + Err(argh::EarlyExit::from(format!("no subcommand matched {}",subcommand_name).to_owned())) } fn redact_arg_values(command_name: &[&str], args: &[&str]) -> std::result::Result, argh::EarlyExit> { @@ -1042,10 +920,10 @@ fn impl_from_args_enum( } )* - Err(argh::EarlyExit::from("no subcommand matched".to_owned())) + Err(argh::EarlyExit::from(format!("no subcommand matched {}",subcommand_name).to_owned())) } - fn help_json_from_args(command_name: &[&str], args: &[&str]) -> std::result::Result + fn help_json_from_args(command_name: &[&str]) -> std::result::Result { let subcommand_name = if let Some(subcommand_name) = command_name.last() { *subcommand_name @@ -1055,11 +933,11 @@ fn impl_from_args_enum( #( if subcommand_name == <#variant_ty as argh::SubCommand>::COMMAND.name { - return #variant_ty::help_json_from_args(command_name, args) + return #variant_ty::help_json_from_args(command_name) ; } )* - Err(argh::EarlyExit::from("no subcommand matched".to_owned())) + Err(argh::EarlyExit::from(format!("no subcommand matched {}",subcommand_name).to_owned())) } } From 8df0bf2f8eb6c75958ebc5ff415fadd65fee5011 Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Thu, 30 Dec 2021 15:32:05 -0800 Subject: [PATCH 12/13] Adding README documentation and clippy warnings This also adds a default implementation of help_json_from_args for FromArgs in order to be backwards compatible with non `derived` implmentations of the FromArgs trait. --- README.md | 80 ++++++++++++++++++++++++++++++++---------- argh/src/lib.rs | 4 ++- argh_derive/src/lib.rs | 2 +- 3 files changed, 66 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 3f71234..bb5a463 100644 --- a/README.md +++ b/README.md @@ -174,48 +174,92 @@ struct SubCommandTwo { } ``` -## Attribute Summary +## Attribute summary + ### Type attributes for `argh` The attributes used to configure the argh information for a type are defined in [parse_attrs::TypeAttrs](argh_derive/src/parse_attrs.rs). -* `subcommand` - a subcommand type. This attribute must appear on both enumeration and each struct that +- `subcommand` - a subcommand type. This attribute must appear on both enumeration and each struct that is a variant for the enumerated subcommand. -* `error_code(code, description)` - an error code for the command. This attribute can appear zero +- `error_code(code, description)` - an error code for the command. This attribute can appear zero or more times. -* `examples=` - Formatted text containing examples of how to use this command. This +- `examples=` - Formatted text containing examples of how to use this command. This is an optional attribute. -* `name=` - (required for subcommand variant) the name of the subcommand. -* `notes=` - Formatted text containing usage notes for this command. This +- `name=` - (required for subcommand variant) the name of the subcommand. +- `notes=` - Formatted text containing usage notes for this command. This is an optional attribute. - pub error_codes: Vec<(syn::LitInt, syn::LitStr)>, ### Field attributes for `argh` The attributes used to configure the argh information for a field are defined in [parse_attrs.rs](argh_derive/src/parse_attrs.rs). -* Field kind. This is the first attribute. Valid kinds are: - * `switch` - a boolean flag, its presence on the command sets the field to `true`. - * `option` - a value. This can be a simple type like String, or usize, and enumeration. +- Field kind. This is the first attribute. Valid kinds are: + - `switch` - a boolean flag, its presence on the command sets the field to `true`. + - `option` - a value. This can be a simple type like String, or usize, and enumeration. This can be a scalar or Vec<> for repeated values. - * `subcommand` - a subcommand. The type of this field is an enumeration with a value for each + - `subcommand` - a subcommand. The type of this field is an enumeration with a value for each subcommand. This attribute must appear on both the "top level" field and each struct that is a variant for the enumerated subcommand. - * `positional` - a positional argument. This can be scalar or Vec<>. Only the last positional + - `positional` - a positional argument. This can be scalar or Vec<>. Only the last positional argument can be Option<>, Vec<>, or defaulted. -* `arg_name=` - the name to use for a positional argument in the help or the value of a `option`. +- `arg_name=` - the name to use for a positional argument in the help or the value of a `option`. If not given, the default is the name of the field. -* `default=` - the default value for the `option` or `positional` fields. -* `description=` - the description of the flag or argument. The default value is the doc comment +- `default=` - the default value for the `option` or `positional` fields. +- `description=` - the description of the flag or argument. The default value is the doc comment for the field. -* `from_str_fn` is the name of a custom deserialization function for this field with the signature: +- `from_str_fn` is the name of a custom deserialization function for this field with the signature: `fn(&str) -> Result`. -* `long=` - the long format of the option or switch name. If `long` is not present, the +- `long=` - the long format of the option or switch name. If `long` is not present, the flag name defaults to the field name. -* `short=` - the single character for this flag. If `short` is not present, there is no +- `short=` - the single character for this flag. If `short` is not present, there is no short equivalent flag. +## JSON encoded help + +The `FromArgs` trait implements 2 methods for accessing the usage information in a JSON +encoded format. This is intended to facilitate generating reference documentation based on +the command line arguments. In order to use this capability, you need to use `#[derive(FromArgs)]` +in order to generate the method to construct the JSON string. + +`fn help_json_from_args(command: &[&str]) -> Result` returns JSON +encoded help information, using the `command` paramter in the _usage_ field. Alternatively, +you can use `fn help_json() -> Result` to use the command +name used to invoke the application. + +### JSON structure + +The structure returned contains the following fields: + +- **name** - the name of the command or subcommand. +- **usage** - the command line pattern showing the usage of the command. +- **description** - the description of the command or subcommand. +- **flags** - the array of command line flags each with: + - **short** - the 1 letter option, if specified, otherwise the empty string. + - **long** - the value of the _long_ attribute or the field name. + - **description** - the description of the option. + - **arg_name** - the name of the option argument, or the field name if not + specified. + - **optionality** - the type of usage for this option. Can be _optional_, + _required_, _repeated_, or the default value if the `default` attribute is + specified. +- **positional** - the array of positional arguments each with: + - **name** - the name of the argument + - **description** - the description of the argument + - **optionality** - the type of usage for this option. Can be _optional_, + _required_, _repeated_, or the default value if the `default` attribute is + specified. +- **examples** - a formatted string of example usage of the command, as + specified in the `examples` attribute. +- **notes** - a formatted string of notes about the usage of the command, as + specified in the `examples` attribute. +- **error_codes** - the array of error code information for this command + including: + - **name** - the error code return value. + - **description** - the meaning of the error code. +- **subcommands** - the array of subcommands for this command. + NOTE: This is not an officially supported Google product. diff --git a/argh/src/lib.rs b/argh/src/lib.rs index c6aff75..0b637c9 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -439,7 +439,9 @@ pub trait FromArgs: Sized { /// Returns a JSON encoded string of the usage information. This is intended to /// create a "machine readable" version of the help text to enable reference /// documentation generation. - fn help_json_from_args(command_name: &[&str]) -> Result; + fn help_json_from_args(_: &[&str]) -> Result { + Err(EarlyExit::from(String::from("Not implemented, use #[derive(FromArgs)]"))) + } /// Returns a JSON encoded string of the usage information based on the command line /// found in argv, identical to `::from_env()`. This is intended to diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index c79ae03..7602814 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -495,7 +495,7 @@ fn impl_help_json<'a>( // Identifier referring to a value containing the name of the current command as an `&[&str]`. let cmd_name_str_array_ident = syn::Ident::new("__cmd_name", impl_span); - let help_json = help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, &fields); + let help_json = help_json::help_json(errors, &cmd_name_str_array_ident, type_attrs, fields); let method_impl = quote_spanned! { impl_span => fn help_json_from_args(__cmd_name: &[&str]) From 6027f97aa23bf4fa10851f4c02e89e6ba1e423df Mon Sep 17 00:00:00 2001 From: Clayton Wilkinson Date: Thu, 30 Dec 2021 15:38:10 -0800 Subject: [PATCH 13/13] fixing typo extra blank line. --- argh/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 0b637c9..76f16b7 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -422,7 +422,6 @@ pub trait FromArgs: Sized { /// /// Options: /// --help display usage information - /// /// Commands: /// list list all the classes.