diff --git a/argh/examples/help_text_example.rs b/argh/examples/help_text_example.rs new file mode 100644 index 0000000..eb5a1d4 --- /dev/null +++ b/argh/examples/help_text_example.rs @@ -0,0 +1,66 @@ +use argh::{FromArgs, TopLevelCommand}; + +#[derive(FromArgs)] +/// Defines a rectangle +#[argh(verbose_error, help_triggers("-h", "--help"))] +pub struct Rectangle { + #[argh(option, short = 'w')] + /// width; 23 if omitted + pub width: Option, + + #[argh(option, short = 'h')] + /// height; 42 if omitted + pub height: Option, + + #[argh(switch)] + /// print extended help and exit + pub long_help: bool, + + #[argh(help_text)] + pub usage: Option, +} + +impl Rectangle { + fn check(&mut self) -> Result<(), String> { + if self.width.is_none() { + self.width = Some(23); + } + if self.height.is_none() { + self.height = Some(42); + } + let w64: u64 = self.width.unwrap().into(); + let h64: u64 = self.height.unwrap().into(); + let area = w64 * h64; + if area > 0xFFFFFFFF { + Err(String::from("You asked for too big a rectangle")) + } else { + return Ok(()); + } + } +} + +fn main() { + let mut rect: Rectangle = argh::from_env(); + if let Err(msg) = rect.check() { + rect.report_error_and_exit(&msg) + } + if rect.long_help { + println!( + "{}\n\n{}", + rect.usage.unwrap(), + "Definition: + In Euclidean plane geometry, a rectangle is a quadrilateral with + four right angles. It can also be defined as: an equiangular + quadrilateral, since equiangular means that all of its angles are + equal (360°/4 = 90°); or a parallelogram containing a right angle. + A rectangle with four sides of equal length is a square. The term + “oblong” is used to refer to a non-square rectangle. + + According to Wikipedia as of mid April 2024", + ) + } else { + let w = rect.width.unwrap(); + let h = rect.height.unwrap(); + println!("Rectangle area is: {}={}x{}", w * h, w, h); + } +} diff --git a/argh/src/lib.rs b/argh/src/lib.rs index d56d1f0..48ddfe6 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -603,10 +603,95 @@ pub trait FromArgs: Sized { fn redact_arg_values(_command_name: &[&str], _args: &[&str]) -> Result, EarlyExit> { Ok(vec!["<>".into()]) } + + #[doc(hidden)] + fn cook_help_text(_command_name: &[&str]) -> Option { + None + } } /// A top-level `FromArgs` implementation that is not a subcommand. -pub trait TopLevelCommand: FromArgs {} +pub trait TopLevelCommand: FromArgs { + /// Prints error message and usage informaton to STDERR and exits with elevated + /// exit code. Handy when arguments combination needs to be checked for + /// consistency. Use together with ‘verbose_error’ attribute to garmonize internal + /// error reporting style. + /// + /// # Example + /// + /// ```rust + /// use argh::{FromArgs, TopLevelCommand}; + /// + /// #[derive(FromArgs)] + /// /// Defines a rectangle + /// #[argh(verbose_error, help_triggers("-h", "--help"))] + /// pub struct Rectangle { + /// #[argh(option, short = 'w')] + /// /// width; 23 if omitted + /// pub width: Option, + /// + /// #[argh(option, short = 'h')] + /// /// height; 42 if omitted + /// pub height: Option, + /// + /// #[argh(switch)] + /// /// print extended help and exit + /// pub long_help: bool, + /// + /// #[argh(help_text)] + /// pub usage: Option, + /// } + /// + /// impl Rectangle { + /// fn check(&mut self) -> Result<(), String> { + /// if self.width.is_none() { + /// self.width = Some(23); + /// } + /// if self.height.is_none() { + /// self.height = Some(42); + /// } + /// let w64: u64 = self.width.unwrap().into(); + /// let h64: u64 = self.height.unwrap().into(); + /// let area = w64 * h64; + /// if area > 0xFFFFFFFF { + /// Err(String::from("You asked for too big a rectangle")) + /// } else { + /// return Ok(()); + /// } + /// } + /// } + /// + /// fn main() { + /// let mut rect: Rectangle = argh::from_env(); + /// if let Err(msg) = rect.check() { + /// rect.report_error_and_exit(&msg) + /// } + /// if rect.long_help { + /// println!( + /// "{}\n\n{}", + /// rect.usage.unwrap(), + /// "Definition: + /// In Euclidean plane geometry, a rectangle is a quadrilateral with + /// four right angles. It can also be defined as: an equiangular + /// quadrilateral, since equiangular means that all of its angles are + /// equal (360°/4 = 90°); or a parallelogram containing a right angle. + /// A rectangle with four sides of equal length is a square. The term + /// “oblong” is used to refer to a non-square rectangle. + /// + /// According to Wikipedia as of mid April 2024", + /// ) + /// } else { + /// let w = rect.width.unwrap(); + /// let h = rect.height.unwrap(); + /// println!("Rectangle area is: {}={}x{}", w * h, w, h); + /// } + /// } + /// ``` + fn report_error_and_exit(&self, msg: &str); + + #[doc(hidden)] + fn cook_error_report(_bin_name: &str, msg: &str) -> String; +} /// A `FromArgs` implementation that can parse into one or more subcommands. pub trait SubCommands: FromArgs { @@ -713,7 +798,7 @@ pub fn from_env() -> T { 0 } Err(()) => { - eprintln!("{}\nRun {} --help for more information.", early_exit.output, cmd); + eprint!("{}", T::cook_error_report(cmd, &early_exit.output)); 1 } }) @@ -739,7 +824,7 @@ pub fn cargo_from_env() -> T { 0 } Err(()) => { - eprintln!("{}\nRun --help for more information.", early_exit.output); + eprint!("{}", T::cook_error_report(cmd, &early_exit.output)); 1 } }) @@ -988,8 +1073,16 @@ impl<'a> ParseStructOptions<'a> { .ok_or_else(|| ["No value provided for option '", arg, "'.\n"].concat())?; *remaining_args = &remaining_args[1..]; pvs.fill_slot(arg, value).map_err(|s| { - ["Error parsing option '", arg, "' with value '", value, "': ", &s, "\n"] - .concat() + [ + "Cannot parse option '", + arg, + "' with value '", + value, + "': ", + &s, + "\n" + ] + .concat() })?; } } @@ -1079,7 +1172,7 @@ impl<'a> ParseStructPositional<'a> { fn parse(&mut self, arg: &str) -> Result<(), EarlyExit> { self.slot.fill_slot("", arg).map_err(|s| { [ - "Error parsing positional argument '", + "Cannot parse positional argument '", self.name, "' with value '", arg, diff --git a/argh/tests/lib.rs b/argh/tests/lib.rs index 8119917..51bc7f4 100644 --- a/argh/tests/lib.rs +++ b/argh/tests/lib.rs @@ -11,7 +11,7 @@ clippy::unwrap_in_result )] -use {argh::FromArgs, std::fmt::Debug}; +use {argh::{FromArgs, TopLevelCommand}, std::fmt::Debug}; #[test] fn basic_example() { @@ -104,6 +104,56 @@ Options: ); } +#[allow(dead_code)] +#[derive(FromArgs, Debug)] +/// Depth options +#[argh(verbose_error)] +struct Depth { + /// how deep the rabbit hole is + #[argh(option)] + depth: usize, + + #[argh(help_text)] + help_text: Option, +} + +#[test] +fn help_text_pseudo_argument() { + let depth = Depth::from_args(&["cmdname"], &["--depth", "23"]).expect("failed at help text"); + let help_text = depth.help_text.expect("None in place of help text"); + + assert_eq!( + help_text, + "Usage: cmdname --depth + +Depth options + +Options: + --depth how deep the rabbit hole is + --help, help display usage information +", + ); +} + +#[test] +fn verbose_error_report() { + // NB: Internal error messaages end with one LF. + let error_report = Depth::cook_error_report("cmdname", "\n"); + assert_eq!( + error_report, + "Error: + +Usage: cmdname --depth + +Depth options + +Options: + --depth how deep the rabbit hole is + --help, help display usage information +", + ); +} + #[test] fn nested_from_str_example() { #[derive(FromArgs)] @@ -474,7 +524,7 @@ mod options { assert_output(&["-n", "5"], Parsed { n: 5 }); assert_error::( &["-n", "x"], - r###"Error parsing option '-n' with value 'x': invalid digit found in string + r###"Cannot parse option '-n' with value 'x': invalid digit found in string "###, ); } @@ -723,7 +773,7 @@ Options: assert_output(&["5"], Parsed { n: 5 }); assert_error::( &["x"], - r###"Error parsing positional argument 'n' with value 'x': invalid digit found in string + r###"Cannot parse positional argument 'n' with value 'x': invalid digit found in string "###, ); } diff --git a/argh_derive/src/args_info.rs b/argh_derive/src/args_info.rs index 969acf0..4f613c5 100644 --- a/argh_derive/src/args_info.rs +++ b/argh_derive/src/args_info.rs @@ -312,7 +312,8 @@ fn impl_args_info_data<'a>( } }); } - FieldKind::SubCommand => {} + FieldKind::SubCommand + | FieldKind::HelpText => {} } } diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs index 3f52331..438d74e 100644 --- a/argh_derive/src/help.rs +++ b/argh_derive/src/help.rs @@ -130,10 +130,10 @@ pub(crate) fn help( format_lit.push('\n'); - quote! { { + quote! { #subcommand_calculation - format!(#format_lit, command_name = #cmd_name_str_array_ident.join(" "), #subcommand_format_arg) - } } + Some(format!(#format_lit, command_name = #cmd_name_str_array_ident.join(" "), #subcommand_format_arg)) + } } /// A section composed of exactly just the literals provided to the program. @@ -190,7 +190,10 @@ fn option_usage(out: &mut String, field: &StructField<'_>) { } match field.kind { - FieldKind::SubCommand | FieldKind::Positional => unreachable!(), // don't have long_name + FieldKind::SubCommand + | FieldKind::HelpText + | FieldKind::Positional + => unreachable!("subcommand, help_text and positional have no long names"), FieldKind::Switch => {} FieldKind::Option => { out.push_str(" <"); diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index 4663935..3b310bc 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -128,7 +128,7 @@ impl<'a> StructField<'a> { field, concat!( "Missing `argh` field kind attribute.\n", - "Expected one of: `switch`, `option`, `remaining`, `subcommand`, `positional`", + "Expected one of: `switch`, `option`, `remaining`, `subcommand`, `positional`, `help_text`", ), ); return None; @@ -190,6 +190,10 @@ impl<'a> StructField<'a> { if inner.is_some() { Optionality::Optional } else { Optionality::None }; ty_without_wrapper = inner.unwrap_or(&field.ty); } + FieldKind::HelpText => { + optionality = Optionality::Optional; + ty_without_wrapper = &field.ty; + } } // Determine the "long" name of options and switches. @@ -207,7 +211,9 @@ impl<'a> StructField<'a> { let long_name = format!("--{}", long_name); Some(long_name) } - FieldKind::SubCommand | FieldKind::Positional => None, + FieldKind::SubCommand + | FieldKind::HelpText + | FieldKind::Positional => None, }; Some(StructField { field, attrs, kind, optionality, ty_without_wrapper, name, long_name }) @@ -287,12 +293,15 @@ fn impl_from_args_struct( let impl_span = Span::call_site(); - let from_args_method = impl_from_args_struct_from_args(errors, type_attrs, &fields); + let from_args_method = + impl_from_args_struct_from_args(errors, type_attrs, &fields); let redact_arg_values_method = impl_from_args_struct_redact_arg_values(errors, type_attrs, &fields); - let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs, generic_args); + let cook_help_text_method = impl_cook_help_text(errors, type_attrs, &fields); + + let top_or_sub_cmd_impl = top_or_sub_cmd_impl(errors, name, type_attrs, generic_args, &fields); let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl(); let trait_impl = quote_spanned! { impl_span => @@ -301,6 +310,8 @@ fn impl_from_args_struct( #from_args_method #redact_arg_values_method + + #cook_help_text_method } #top_or_sub_cmd_impl @@ -309,6 +320,119 @@ fn impl_from_args_struct( trait_impl } +fn impl_cook_help_text<'a>( + errors: &Errors, + type_attrs: &TypeAttrs, + fields: &'a [StructField<'a>], +) -> TokenStream { + let mut subcommands_iter = + fields + .iter() + .filter( + |field| + field.kind == FieldKind::SubCommand + ).fuse(); + + let subcommand: Option<&StructField<'_>> = subcommands_iter.next(); + + let impl_span = Span::call_site(); + + let help_triggers = get_help_triggers(type_attrs); + + let help = if cfg!(feature = "help") { + // 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, + ); + help::help( + errors, // a + cmd_name_str_array_ident, // l + type_attrs, // a + fields, // a + subcommand, // l? + &help_triggers, // l + ) + } else { + quote! { String::new() } + }; + + let method_impl = quote! { + fn cook_help_text(__cmd_name: &[&str]) -> Option { + #help + } + }; + + method_impl +} + +fn impl_report_error( + type_attrs: &TypeAttrs, +) -> TokenStream { + let method_impl = if type_attrs.verbose_error { + quote! { + fn cook_error_report(_bin_name: &str, msg: &str) -> String { + let help_text = Self::cook_help_text(&[_bin_name]); + String::from(format!( + "Error: {}\n{}", + msg, + help_text.unwrap_or(String::from("Run with --help for more information.")), + )) + } + } + } else { + quote! { + fn cook_error_report(_bin_name: &str, msg: &str) -> String { + String::from(format!( + "{}\nRun {} --help for more information.", + msg, + _bin_name, + )) + } + } + }; + + method_impl +} + +fn impl_report_error_and_exit<'a>( + fields: &'a [StructField<'a>], +) -> TokenStream { + let method_impl = if let Some(field) = fields.iter().find( + |&field| + field.kind == FieldKind::HelpText + ) { + let field_name = &field.field.ident; + quote! { + fn report_error_and_exit(&self, msg: &str){ + let help_text = if let Some(help_text) = &self.#field_name { + &help_text + } else { + "Run with --help for more information." + }; + eprintln!( + "Error: {}\n\n{}", + msg, + &help_text, + ); + std::process::exit(1); + } + } + } else { + quote! { + fn report_error_and_exit(&self, msg: &str){ + eprintln!( + "Error: {}\n\nRun with --help for more information.", + msg, + ); + std::process::exit(1); + } + } + }; + + method_impl +} + fn impl_from_args_struct_from_args<'a>( errors: &Errors, type_attrs: &TypeAttrs, @@ -334,7 +458,9 @@ fn impl_from_args_struct_from_args<'a>( 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, + FieldKind::SubCommand + | FieldKind::HelpText + | FieldKind::Positional => None, } }); @@ -374,14 +500,6 @@ fn impl_from_args_struct_from_args<'a>( let help_triggers = get_help_triggers(type_attrs); - let help = if cfg!(feature = "help") { - // 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); - help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand, &help_triggers) - } else { - quote! { String::new() } - }; - let method_impl = quote_spanned! { impl_span => fn from_args(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result @@ -411,7 +529,10 @@ fn impl_from_args_struct_from_args<'a>( last_is_greedy: #last_positional_is_greedy, }, #parse_subcommands, - &|| #help, + &|| { + Self::cook_help_text(__cmd_name) + .unwrap_or(String::from("Help is not available.")) + }, )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); @@ -478,7 +599,9 @@ fn impl_from_args_struct_redact_arg_values<'a>( 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, + FieldKind::SubCommand + | FieldKind::HelpText + | FieldKind::Positional => None, } }); @@ -524,14 +647,6 @@ fn impl_from_args_struct_redact_arg_values<'a>( let help_triggers = get_help_triggers(type_attrs); - let help = if cfg!(feature = "help") { - // 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); - help::help(errors, cmd_name_str_array_ident, type_attrs, fields, subcommand, &help_triggers) - } else { - quote! { String::new() } - }; - let method_impl = quote_spanned! { impl_span => fn redact_arg_values(__cmd_name: &[&str], __args: &[&str]) -> std::result::Result, argh::EarlyExit> { #( #init_fields )* @@ -557,7 +672,10 @@ fn impl_from_args_struct_redact_arg_values<'a>( last_is_greedy: #last_positional_is_greedy, }, #redact_subcommands, - &|| #help, + &|| { + Self::cook_help_text(__cmd_name) + .unwrap_or(String::from("Help is not available.")) + }, )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); @@ -637,11 +755,12 @@ fn ensure_unique_names(errors: &Errors, fields: &[StructField<'_>]) { } /// Implement `argh::TopLevelCommand` or `argh::SubCommand` as appropriate. -fn top_or_sub_cmd_impl( +fn top_or_sub_cmd_impl<'a>( errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttrs, generic_args: &syn::Generics, + fields: &'a [StructField<'a>], ) -> TokenStream { let description = if cfg!(feature = "help") { help::require_description(errors, name.span(), &type_attrs.description, "type") @@ -651,9 +770,14 @@ fn top_or_sub_cmd_impl( let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl(); if type_attrs.is_subcommand.is_none() { // Not a subcommand + let report_error_method = impl_report_error(type_attrs); + let report_error_and_exit_method = impl_report_error_and_exit(fields); quote! { #[automatically_derived] - impl #impl_generics argh::TopLevelCommand for #name #ty_generics #where_clause {} + impl #impl_generics argh::TopLevelCommand for #name #ty_generics #where_clause { + #report_error_method + #report_error_and_exit_method + } } } else { let empty_str = syn::LitStr::new("", Span::call_site()); @@ -721,6 +845,9 @@ fn declare_local_storage_for_from_args_fields<'a>( FieldKind::Switch => { quote! { let mut #field_name: #field_slot_type = argh::Flag::default(); } } + FieldKind::HelpText => { + quote! { let mut #field_name: #field_slot_type = Self::cook_help_text(__cmd_name); } + } } }) } @@ -751,6 +878,9 @@ fn unwrap_from_args_fields<'a>( Optionality::Optional | Optionality::Repeating => field_name.into_token_stream(), Optionality::Defaulted(_) | Optionality::DefaultedRepeating(_) => unreachable!(), }, + FieldKind::HelpText => { + quote! { #field_name } + } } }) } @@ -763,16 +893,16 @@ fn unwrap_from_args_fields<'a>( fn declare_local_storage_for_redacted_fields<'a>( fields: &'a [StructField<'a>], ) -> impl Iterator + 'a { - fields.iter().map(|field| { + fields.iter().filter_map(|field| { let field_name = &field.field.ident; match field.kind { FieldKind::Switch => { - quote! { + Some(quote! { let mut #field_name = argh::RedactFlag { slot: None, }; - } + }) } FieldKind::Option => { let field_slot_type = match field.optionality { @@ -787,13 +917,13 @@ fn declare_local_storage_for_redacted_fields<'a>( } }; - quote! { + Some(quote! { let mut #field_name: argh::ParseValueSlotTy::<#field_slot_type, String> = argh::ParseValueSlotTy { slot: std::default::Default::default(), parse_func: |arg, _| { Ok(arg.to_owned()) }, }; - } + }) } FieldKind::Positional => { let field_slot_type = match field.optionality { @@ -809,17 +939,18 @@ fn declare_local_storage_for_redacted_fields<'a>( }; let arg_name = field.positional_arg_name(); - quote! { + Some(quote! { let mut #field_name: argh::ParseValueSlotTy::<#field_slot_type, String> = argh::ParseValueSlotTy { slot: std::default::Default::default(), parse_func: |_, _| { Ok(#arg_name.to_owned()) }, }; - } + }) } FieldKind::SubCommand => { - quote! { let mut #field_name: std::option::Option> = None; } + Some(quote! { let mut #field_name: std::option::Option> = None; }) } + FieldKind::HelpText => None } }) } @@ -828,50 +959,51 @@ fn declare_local_storage_for_redacted_fields<'a>( fn unwrap_redacted_fields<'a>( fields: &'a [StructField<'a>], ) -> impl Iterator + 'a { - fields.iter().map(|field| { + fields.iter().filter_map(|field| { let field_name = field.name; match field.kind { FieldKind::Switch => { - quote! { + Some(quote! { if let Some(__field_name) = #field_name.slot { __redacted.push(__field_name); } - } + }) } FieldKind::Option => match field.optionality { Optionality::Repeating => { - quote! { + Some(quote! { __redacted.extend(#field_name.slot.into_iter()); - } + }) } Optionality::DefaultedRepeating(_) => { - quote! { + Some(quote! { if let Some(__field_name) = #field_name.slot { __redacted.extend(__field_name.into_iter()); } - } + }) } Optionality::None | Optionality::Optional | Optionality::Defaulted(_) => { - quote! { + Some(quote! { if let Some(__field_name) = #field_name.slot { __redacted.push(__field_name); } - } + }) } - }, + } FieldKind::Positional => { - quote! { + Some(quote! { __redacted.extend(#field_name.slot.into_iter()); - } + }) } FieldKind::SubCommand => { - quote! { + Some(quote! { if let Some(__subcommand_args) = #field_name { __redacted.extend(__subcommand_args.into_iter()); } - } + }) } + FieldKind::HelpText => None } }) } @@ -939,6 +1071,7 @@ fn append_missing_requirements<'a>( } } } + FieldKind::HelpText => unreachable!("help_text is always optional") } }) } diff --git a/argh_derive/src/parse_attrs.rs b/argh_derive/src/parse_attrs.rs index 39d2fba..4d7b15c 100644 --- a/argh_derive/src/parse_attrs.rs +++ b/argh_derive/src/parse_attrs.rs @@ -22,6 +22,7 @@ pub struct FieldAttrs { pub arg_name: Option, pub greedy: Option, pub hidden_help: bool, + pub verbose_error: bool, } /// The purpose of a particular field on a `#![derive(FromArgs)]` struct. @@ -41,6 +42,8 @@ pub enum FieldKind { /// They are parsed in declaration order, and only the last positional /// argument in a type may be an `Option`, `Vec`, or have a default value. Positional, + /// Pseudo-argument to store generated help message. + HelpText, } /// The type of a field on a `#![derive(FromArgs)]` struct. @@ -101,7 +104,12 @@ impl FieldAttrs { this.parse_attr_long(errors, m); } } else if name.is_ident("option") { - parse_attr_field_type(errors, &meta, FieldKind::Option, &mut this.field_type); + parse_attr_field_type( + errors, + &meta, + FieldKind::Option, + &mut this.field_type + ); } else if name.is_ident("short") { if let Some(m) = errors.expect_meta_name_value(&meta) { this.parse_attr_short(errors, m); @@ -114,7 +122,12 @@ impl FieldAttrs { &mut this.field_type, ); } else if name.is_ident("switch") { - parse_attr_field_type(errors, &meta, FieldKind::Switch, &mut this.field_type); + parse_attr_field_type( + errors, + &meta, + FieldKind::Switch, + &mut this.field_type + ); } else if name.is_ident("positional") { parse_attr_field_type( errors, @@ -126,13 +139,20 @@ impl FieldAttrs { this.greedy = Some(name.clone()); } else if name.is_ident("hidden_help") { this.hidden_help = true; + } else if name.is_ident("help_text") { + parse_attr_field_type( + errors, + &meta, + FieldKind::HelpText, + &mut this.field_type, + ); } else { errors.err( &meta, concat!( "Invalid field-level `argh` attribute\n", "Expected one of: `arg_name`, `default`, `description`, `from_str_fn`, `greedy`, ", - "`long`, `option`, `short`, `subcommand`, `switch`, `hidden_help`", + "`long`, `option`, `short`, `subcommand`, `switch`, `hidden_help`, `help_text`", ), ); } @@ -142,7 +162,9 @@ impl FieldAttrs { if let (Some(default), Some(field_type)) = (&this.default, &this.field_type) { match field_type.kind { FieldKind::Option | FieldKind::Positional => {} - FieldKind::SubCommand | FieldKind::Switch => errors.err( + FieldKind::SubCommand + | FieldKind::HelpText + | FieldKind::Switch => errors.err( default, "`default` may only be specified on `#[argh(option)]` \ or `#[argh(positional)]` fields", @@ -275,6 +297,7 @@ pub struct TypeAttrs { pub error_codes: Vec<(syn::LitInt, syn::LitStr)>, /// Arguments that trigger printing of the help message pub help_triggers: Option>, + pub verbose_error: bool, } impl TypeAttrs { @@ -325,6 +348,8 @@ impl TypeAttrs { if let Some(m) = errors.expect_meta_list(&meta) { Self::parse_help_triggers(m, errors, &mut this); } + } else if name.is_ident("verbose_error") { + this.verbose_error = true; } else { errors.err( &meta, @@ -630,8 +655,16 @@ fn parse_attr_description(errors: &Errors, m: &syn::MetaNameValue, slot: &mut Op /// Checks that a `#![derive(FromArgs)]` enum has an `#[argh(subcommand)]` /// attribute and that it does not have any other type-level `#[argh(...)]` attributes. pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: &Span) { - let TypeAttrs { is_subcommand, name, description, examples, notes, error_codes, help_triggers } = - type_attrs; + let TypeAttrs { + is_subcommand, + name, + description, + examples, + notes, + error_codes, + help_triggers, + verbose_error, + } = type_attrs; // Ensure that `#[argh(subcommand)]` is present. if is_subcommand.is_none() { @@ -667,6 +700,9 @@ pub fn check_enum_type_attrs(errors: &Errors, type_attrs: &TypeAttrs, type_span: err_unused_enum_attr(errors, trigger); } } + if *verbose_error { + err_unused_enum_attr(errors, verbose_error); + } } fn err_unused_enum_attr(errors: &Errors, location: &impl syn::spanned::Spanned) {