From 7ee987879c5259c5be11f274d9c0d1433659d192 Mon Sep 17 00:00:00 2001 From: Erick Tryzelaar Date: Fri, 4 Feb 2022 16:33:49 -0800 Subject: [PATCH 1/4] Prototype out a help trait --- argh/src/help.rs | 330 +++++++++++++++++++++++++++++++++++++++++ argh/src/lib.rs | 10 +- argh_derive/src/lib.rs | 159 +++++++++++++++++++- 3 files changed, 495 insertions(+), 4 deletions(-) create mode 100644 argh/src/help.rs diff --git a/argh/src/help.rs b/argh/src/help.rs new file mode 100644 index 0000000..ab437a3 --- /dev/null +++ b/argh/src/help.rs @@ -0,0 +1,330 @@ +// Copyright (c) 2022 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. + +//! TODO + +use { + argh_shared::{write_description, CommandInfo, INDENT}, + std::fmt, +}; + +const SECTION_SEPARATOR: &str = "\n\n"; + +const HELP_FLAG: HelpFlagInfo = HelpFlagInfo { + short: None, + long: "--help", + description: "display usage information", + optionality: HelpOptionality::Optional, + kind: HelpFieldKind::Switch, +}; + +/// TODO +pub trait Help { + /// TODO + const HELP_INFO: &'static HelpInfo; +} + +/// TODO +pub trait HelpSubCommands { + /// TODO + const HELP_INFO: &'static HelpSubCommandsInfo; +} + +/// TODO +pub trait HelpSubCommand { + /// TODO + const HELP_INFO: &'static HelpSubCommandInfo; +} + +impl HelpSubCommands for T { + /// TODO + const HELP_INFO: &'static HelpSubCommandsInfo = + &HelpSubCommandsInfo { optional: false, commands: &[::HELP_INFO] }; +} + +/// TODO +pub struct HelpInfo { + /// TODO + pub description: &'static str, + /// TODO + pub examples: &'static [fn(&[&str]) -> String], + /// TODO + pub notes: &'static [fn(&[&str]) -> String], + /// TODO + pub flags: &'static [&'static HelpFlagInfo], + /// TODO + pub positionals: &'static [&'static HelpPositionalInfo], + /// TODO + pub subcommand: Option<&'static HelpSubCommandsInfo>, + /// TODO + pub error_codes: &'static [(isize, &'static str)], +} + +fn help_section( + out: &mut String, + command_name: &[&str], + heading: &str, + sections: &[fn(&[&str]) -> String], +) { + if !sections.is_empty() { + out.push_str(SECTION_SEPARATOR); + for section_fn in sections { + let section = section_fn(command_name); + + out.push_str(heading); + for line in section.split('\n') { + out.push('\n'); + out.push_str(INDENT); + out.push_str(line); + } + } + } +} + +impl HelpInfo { + /// TODO + pub fn help(&self, command_name: &[&str]) -> String { + let mut out = format!("Usage: {}", command_name.join(" ")); + + for positional in self.positionals { + out.push(' '); + positional.help_usage(&mut out); + } + + for flag in self.flags { + out.push(' '); + flag.help_usage(&mut out); + } + + if let Some(subcommand) = &self.subcommand { + out.push(' '); + if subcommand.optional { + out.push('['); + } + out.push_str(""); + if subcommand.optional { + out.push(']'); + } + out.push_str(" []"); + } + + out.push_str(SECTION_SEPARATOR); + + out.push_str(self.description); + + if !self.positionals.is_empty() { + out.push_str(SECTION_SEPARATOR); + out.push_str("Positional Arguments:"); + for positional in self.positionals { + positional.help_description(&mut out); + } + } + + out.push_str(SECTION_SEPARATOR); + out.push_str("Options:"); + for flag in self.flags { + flag.help_description(&mut out); + } + + // Also include "help" + HELP_FLAG.help_description(&mut out); + + if let Some(subcommand) = &self.subcommand { + out.push_str(SECTION_SEPARATOR); + out.push_str("Commands:"); + for cmd in subcommand.commands { + let info = CommandInfo { name: cmd.name, description: cmd.info.description }; + write_description(&mut out, &info); + } + } + + help_section(&mut out, command_name, "Examples:", self.examples); + + help_section(&mut out, command_name, "Notes:", self.notes); + + if !self.error_codes.is_empty() { + out.push_str(SECTION_SEPARATOR); + out.push_str("Error codes:"); + write_error_codes(&mut out, self.error_codes); + } + + out.push('\n'); + + out + } +} + +fn write_error_codes(out: &mut String, error_codes: &[(isize, &str)]) { + for (code, text) in error_codes { + out.push('\n'); + out.push_str(INDENT); + out.push_str(&format!("{} {}", code, text)); + } +} + +impl fmt::Debug for HelpInfo { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let examples = self.examples.iter().map(|f| f(&["{command_name}"])).collect::>(); + let notes = self.notes.iter().map(|f| f(&["{command_name}"])).collect::>(); + f.debug_struct("HelpInfo") + .field("description", &self.description) + .field("examples", &examples) + .field("notes", ¬es) + .field("flags", &self.flags) + .field("positionals", &self.positionals) + .field("subcommand", &self.subcommand) + .field("error_codes", &self.error_codes) + .finish() + } +} + +/// TODO +#[derive(Debug)] +pub struct HelpSubCommandsInfo { + /// TODO + pub optional: bool, + /// TODO + pub commands: &'static [&'static HelpSubCommandInfo], +} + +/// TODO +#[derive(Debug)] +pub struct HelpSubCommandInfo { + /// TODO + pub name: &'static str, + /// TODO + pub info: &'static HelpInfo, +} + +/// TODO +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum HelpOptionality { + /// TODO + None, + /// TODO + Optional, + /// TODO + Repeating, +} + +impl HelpOptionality { + /// TODO + fn is_required(&self) -> bool { + matches!(self, HelpOptionality::None) + } +} + +/// TODO +#[derive(Debug)] +pub struct HelpPositionalInfo { + /// TODO + pub name: &'static str, + /// TODO + pub description: &'static str, + /// TODO + pub optionality: HelpOptionality, +} + +impl HelpPositionalInfo { + /// TODO + pub fn help_usage(&self, out: &mut String) { + if !self.optionality.is_required() { + out.push('['); + } + + out.push('<'); + out.push_str(self.name); + + if self.optionality == HelpOptionality::Repeating { + out.push_str("..."); + } + + out.push('>'); + + if !self.optionality.is_required() { + out.push(']'); + } + } + + /// TODO + pub fn help_description(&self, out: &mut String) { + let info = CommandInfo { name: self.name, description: self.description }; + write_description(out, &info); + } +} + +/// TODO +#[derive(Debug)] +pub struct HelpFlagInfo { + /// TODO + pub short: Option, + /// TODO + pub long: &'static str, + /// TODO + pub description: &'static str, + /// TODO + pub optionality: HelpOptionality, + /// TODO + pub kind: HelpFieldKind, +} + +/// TODO +#[derive(Debug)] +pub enum HelpFieldKind { + /// TODO + Switch, + /// TODO + Option { + /// TODO + arg_name: &'static str, + }, +} + +impl HelpFlagInfo { + /// TODO + pub fn help_usage(&self, out: &mut String) { + if !self.optionality.is_required() { + out.push('['); + } + + if let Some(short) = self.short { + out.push('-'); + out.push(short); + } else { + out.push_str(self.long); + } + + match self.kind { + HelpFieldKind::Switch => {} + HelpFieldKind::Option { arg_name } => { + out.push_str(" <"); + out.push_str(arg_name); + + if self.optionality == HelpOptionality::Repeating { + out.push_str("..."); + } + + out.push('>'); + } + } + + if !self.optionality.is_required() { + out.push(']'); + } + } + + /// TODO + pub fn help_description(&self, out: &mut String) { + let mut name = String::new(); + if let Some(short) = self.short { + name.push('-'); + name.push(short); + name.push_str(", "); + } + name.push_str(self.long); + + let info = CommandInfo { name: &name, description: self.description }; + write_description(out, &info); + } +} diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 1bd6ba9..32da102 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -171,7 +171,15 @@ use std::str::FromStr; -pub use argh_derive::FromArgs; +mod help; + +pub use { + crate::help::{ + Help, HelpFieldKind, HelpFlagInfo, HelpInfo, HelpOptionality, HelpPositionalInfo, + HelpSubCommand, HelpSubCommandInfo, HelpSubCommands, HelpSubCommandsInfo, + }, + argh_derive::FromArgs, +}; /// Information about a particular command used for output. pub type CommandInfo = argh_shared::CommandInfo<'static>; diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index ab72a40..64ce20a 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -242,6 +242,8 @@ fn impl_from_args_struct( let redact_arg_values_method = impl_from_args_struct_redact_arg_values(errors, type_attrs, &fields); + let help_info = impl_help(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 => @@ -252,16 +254,149 @@ fn impl_from_args_struct( #redact_arg_values_method } + #[automatically_derived] + impl argh::Help for #name { + const HELP_INFO: &'static argh::HelpInfo = #help_info; + } + #top_or_sub_cmd_impl }; trait_impl } -fn impl_from_args_struct_from_args<'a>( +fn impl_help<'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(); + for dup_subcommand in subcommands_iter { + errors.duplicate_attrs("subcommand", subcommand.unwrap().field, dup_subcommand.field); + } + + let impl_span = Span::call_site(); + + let mut positionals = vec![]; + let mut flags = vec![]; + + for field in fields { + let optionality = match field.optionality { + Optionality::None => quote! { argh::HelpOptionality::None }, + Optionality::Defaulted(_) => quote! { argh::HelpOptionality::None }, + Optionality::Optional => quote! { argh::HelpOptionality::Optional }, + Optionality::Repeating => quote! { argh::HelpOptionality::Repeating }, + }; + + match field.kind { + FieldKind::Positional => { + let name = field.arg_name(); + + let description = if let Some(desc) = &field.attrs.description { + desc.content.value().trim().to_owned() + } else { + String::new() + }; + + positionals.push(quote! { + &argh::HelpPositionalInfo { + name: #name, + description: #description, + optionality: #optionality, + } + }); + } + FieldKind::Switch | FieldKind::Option => { + let short = if let Some(short) = &field.attrs.short { + quote! { Some(#short) } + } else { + quote! { None } + }; + + let long = field.long_name.as_ref().expect("missing long name for option"); + + let description = help::require_description( + errors, + field.name.span(), + &field.attrs.description, + "field", + ); + + let kind = if field.kind == FieldKind::Switch { + quote! { + argh::HelpFieldKind::Switch + } + } else { + let arg_name = if let Some(arg_name) = &field.attrs.arg_name { + quote! { #arg_name } + } else { + let arg_name = long.trim_start_matches("--"); + quote! { #arg_name } + }; + + quote! { + argh::HelpFieldKind::Option { + arg_name: #arg_name, + } + } + }; + + flags.push(quote! { + &argh::HelpFlagInfo { + short: #short, + long: #long, + description: #description, + optionality: #optionality, + kind: #kind, + } + }); + } + FieldKind::SubCommand => {} + } + } + + let subcommand = if let Some(subcommand) = subcommand { + let subcommand_ty = subcommand.ty_without_wrapper; + quote! { Some(<#subcommand_ty as argh::HelpSubCommands>::HELP_INFO) } + } else { + quote! { None } + }; + + let description = + help::require_description(errors, Span::call_site(), &type_attrs.description, "type"); + let examples = type_attrs + .examples + .iter() + .map(|e| quote! { |command_name| format!(#e, command_name = command_name.join(" ")) }); + let notes = type_attrs + .notes + .iter() + .map(|e| quote! { |command_name| format!(#e, command_name = command_name.join(" ")) }); + + let error_codes = type_attrs.error_codes.iter().map(|(code, text)| { + quote! { (#code, #text) } + }); + + quote_spanned! { impl_span => + &argh::HelpInfo { + description: #description, + examples: &[#( #examples, )*], + notes: &[#( #notes, )*], + positionals: &[#( #positionals, )*], + flags: &[#( #flags, )*], + subcommand: #subcommand, + error_codes: &[#( #error_codes, )*], + } + } +} + +fn impl_from_args_struct_from_args<'a>( + errors: &Errors, + _type_attrs: &TypeAttrs, + fields: &'a [StructField<'a>], ) -> TokenStream { let init_fields = declare_local_storage_for_from_args_fields(fields); let unwrap_fields = unwrap_from_args_fields(fields); @@ -318,7 +453,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]) @@ -347,7 +482,8 @@ fn impl_from_args_struct_from_args<'a>( last_is_repeating: #last_positional_is_repeating, }, #parse_subcommands, - &|| #help, + //&|| #help, + &|| ::HELP_INFO.help(#cmd_name_str_array_ident), )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); @@ -527,6 +663,14 @@ fn top_or_sub_cmd_impl(errors: &Errors, name: &syn::Ident, type_attrs: &TypeAttr description: #description, }; } + + #[automatically_derived] + impl argh::HelpSubCommand for #name { + const HELP_INFO: &'static argh::HelpSubCommandInfo = &argh::HelpSubCommandInfo { + name: #subcommand_name, + info: <#name as argh::Help>::HELP_INFO, + }; + } } } } @@ -904,6 +1048,15 @@ fn impl_from_args_enum( <#variant_ty as argh::SubCommand>::COMMAND, )*]; } + + impl argh::HelpSubCommands for #name { + const HELP_INFO: &'static argh::HelpSubCommandsInfo = &argh::HelpSubCommandsInfo { + optional: false, + commands: &[#( + <#variant_ty as argh::HelpSubCommand>::HELP_INFO, + )*], + }; + } } } From 25e4930f8091d1dab580e3fa5eecccf531280c2f Mon Sep 17 00:00:00 2001 From: Erick Tryzelaar Date: Tue, 8 Feb 2022 11:12:09 -0800 Subject: [PATCH 2/4] Try to reduce some of the duplication --- argh/src/lib.rs | 40 +++++- argh_derive/src/help.rs | 211 +++++++++++++++------------ argh_derive/src/lib.rs | 16 +-- argh_shared/src/help.rs | 310 ++++++++++++++++++++++++++++++++++++++++ argh_shared/src/lib.rs | 7 + 5 files changed, 476 insertions(+), 108 deletions(-) create mode 100644 argh_shared/src/help.rs diff --git a/argh/src/lib.rs b/argh/src/lib.rs index 32da102..754eefa 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -171,19 +171,23 @@ use std::str::FromStr; -mod help; - pub use { - crate::help::{ - Help, HelpFieldKind, HelpFlagInfo, HelpInfo, HelpOptionality, HelpPositionalInfo, - HelpSubCommand, HelpSubCommandInfo, HelpSubCommands, HelpSubCommandsInfo, - }, argh_derive::FromArgs, + argh_shared::{HelpFieldKind, HelpFlagInfo, HelpOptionality, HelpPositionalInfo}, }; /// Information about a particular command used for output. pub type CommandInfo = argh_shared::CommandInfo<'static>; +/// TODO +pub type HelpInfo = argh_shared::HelpInfo<'static>; + +/// TODO +pub type HelpSubCommandsInfo = argh_shared::HelpSubCommandsInfo<'static>; + +/// TODO +pub type HelpSubCommandInfo = argh_shared::HelpSubCommandInfo<'static>; + /// Types which can be constructed from a set of commandline arguments. pub trait FromArgs: Sized { /// Construct the type from an input set of arguments. @@ -463,6 +467,30 @@ impl SubCommands for T { const COMMANDS: &'static [&'static CommandInfo] = &[T::COMMAND]; } +/// TODO +pub trait Help { + /// TODO + const HELP_INFO: &'static HelpInfo; +} + +/// TODO +pub trait HelpSubCommands { + /// TODO + const HELP_INFO: &'static HelpSubCommandsInfo; +} + +/// TODO +pub trait HelpSubCommand { + /// TODO + const HELP_INFO: &'static HelpSubCommandInfo; +} + +impl HelpSubCommands for T { + /// TODO + const HELP_INFO: &'static HelpSubCommandsInfo = + &HelpSubCommandsInfo { optional: false, commands: &[::HELP_INFO] }; +} + /// Information to display to the user about why a `FromArgs` construction exited early. /// /// This can occur due to either failed parsing or a flag like `--help`. diff --git a/argh_derive/src/help.rs b/argh_derive/src/help.rs index c295825..4fd718c 100644 --- a/argh_derive/src/help.rs +++ b/argh_derive/src/help.rs @@ -15,6 +15,94 @@ use { const SECTION_SEPARATOR: &str = "\n\n"; +struct PositionalInfo { + name: String, + description: String, + optionality: argh_shared::HelpOptionality, +} + +impl PositionalInfo { + fn new(field: &StructField) -> Self { + let name = field.arg_name(); + let mut description = String::from(""); + if let Some(desc) = &field.attrs.description { + description = desc.content.value().trim().to_owned(); + } + + Self { name, description, optionality: to_help_optional(&field.optionality) } + } + + fn as_help_info(&self) -> argh_shared::HelpPositionalInfo { + argh_shared::HelpPositionalInfo { + name: &self.name, + description: &self.description, + optionality: self.optionality, + } + } +} +struct FlagInfo { + short: Option, + long: String, + description: String, + optionality: argh_shared::HelpOptionality, + kind: HelpFieldKind, +} + +enum HelpFieldKind { + Switch, + Option { arg_name: String }, +} + +impl FlagInfo { + fn new(errors: &Errors, field: &StructField) -> Self { + let short = field.attrs.short.as_ref().map(|s| s.value()); + + let long = field.long_name.as_ref().expect("missing long name for option").to_owned(); + + let description = + require_description(errors, field.name.span(), &field.attrs.description, "field"); + + let kind = if field.kind == FieldKind::Switch { + HelpFieldKind::Switch + } else { + let arg_name = if let Some(arg_name) = &field.attrs.arg_name { + arg_name.value() + } else { + long.trim_start_matches("--").to_owned() + }; + HelpFieldKind::Option { arg_name } + }; + + Self { short, long, description, optionality: to_help_optional(&field.optionality), kind } + } + + fn as_help_info(&self) -> argh_shared::HelpFlagInfo { + let kind = match &self.kind { + HelpFieldKind::Switch => argh_shared::HelpFieldKind::Switch, + HelpFieldKind::Option { arg_name } => { + argh_shared::HelpFieldKind::Option { arg_name: arg_name.as_str() } + } + }; + + argh_shared::HelpFlagInfo { + short: self.short, + long: &self.long, + description: &self.description, + optionality: self.optionality, + kind, + } + } +} + +fn to_help_optional(optionality: &Optionality) -> argh_shared::HelpOptionality { + match optionality { + Optionality::None => argh_shared::HelpOptionality::None, + Optionality::Defaulted(_) => argh_shared::HelpOptionality::None, + Optionality::Optional => argh_shared::HelpOptionality::Optional, + Optionality::Repeating => argh_shared::HelpOptionality::Repeating, + } +} + /// Returns a `TokenStream` generating a `String` help message. /// /// Note: `fields` entries with `is_subcommand.is_some()` will be ignored @@ -28,18 +116,36 @@ pub(crate) fn help( ) -> TokenStream { let mut format_lit = "Usage: {command_name}".to_string(); - let positional = fields.iter().filter(|f| f.kind == FieldKind::Positional); + let positionals = fields + .iter() + .filter_map(|f| { + if f.kind == FieldKind::Positional { + Some(PositionalInfo::new(f)) + } else { + None + } + }) + .collect::>(); + + let positionals = positionals.iter().map(|o| o.as_help_info()).collect::>(); + + let flags = fields + .iter() + .filter_map(|f| if f.long_name.is_some() { Some(FlagInfo::new(errors, f)) } else { None }) + .collect::>(); + + let flags = flags.iter().map(|o| o.as_help_info()).collect::>(); + let mut has_positional = false; - for arg in positional.clone() { + for arg in &positionals { has_positional = true; format_lit.push(' '); - positional_usage(&mut format_lit, arg); + arg.help_usage(&mut format_lit); } - let options = fields.iter().filter(|f| f.long_name.is_some()); - for option in options.clone() { + for flag in &flags { format_lit.push(' '); - option_usage(&mut format_lit, option); + flag.help_usage(&mut format_lit); } if let Some(subcommand) = subcommand { @@ -62,16 +168,17 @@ pub(crate) fn help( if has_positional { format_lit.push_str(SECTION_SEPARATOR); format_lit.push_str("Positional Arguments:"); - for arg in positional { - positional_description(&mut format_lit, arg); + for field in positionals { + field.help_description(&mut format_lit); } } format_lit.push_str(SECTION_SEPARATOR); format_lit.push_str("Options:"); - for option in options { - option_description(errors, &mut format_lit, option); + for flag in flags { + flag.help_description(&mut format_lit); } + // Also include "help" option_description_format(&mut format_lit, None, "--help", "display usage information"); @@ -130,61 +237,6 @@ fn lits_section(out: &mut String, heading: &str, lits: &[syn::LitStr]) { } } -/// Add positional arguments like `[...]` to a help format string. -fn positional_usage(out: &mut String, field: &StructField<'_>) { - if !field.optionality.is_required() { - out.push('['); - } - out.push('<'); - let name = field.arg_name(); - out.push_str(&name); - if field.optionality == Optionality::Repeating { - out.push_str("..."); - } - out.push('>'); - if !field.optionality.is_required() { - out.push(']'); - } -} - -/// Add options like `[-f ]` to a help format string. -/// This function must only be called on options (things with `long_name.is_some()`) -fn option_usage(out: &mut String, field: &StructField<'_>) { - // bookend with `[` and `]` if optional - if !field.optionality.is_required() { - out.push('['); - } - - let long_name = field.long_name.as_ref().expect("missing long name for option"); - if let Some(short) = field.attrs.short.as_ref() { - out.push('-'); - out.push(short.value()); - } else { - out.push_str(long_name); - } - - match field.kind { - FieldKind::SubCommand | FieldKind::Positional => unreachable!(), // don't have long_name - FieldKind::Switch => {} - FieldKind::Option => { - out.push_str(" <"); - if let Some(arg_name) = &field.attrs.arg_name { - out.push_str(&arg_name.value()); - } else { - out.push_str(long_name.trim_start_matches("--")); - } - if field.optionality == Optionality::Repeating { - out.push_str("..."); - } - out.push('>'); - } - } - - if !field.optionality.is_required() { - out.push(']'); - } -} - // TODO(cramertj) make it so this is only called at least once per object so // as to avoid creating multiple errors. pub fn require_description( @@ -206,35 +258,6 @@ Add a doc comment or an `#[argh(description = \"...\")]` attribute.", }) } -/// Describes a positional argument like this: -/// hello positional argument description -fn positional_description(out: &mut String, field: &StructField<'_>) { - let field_name = field.arg_name(); - - let mut description = String::from(""); - if let Some(desc) = &field.attrs.description { - description = desc.content.value().trim().to_owned(); - } - positional_description_format(out, &field_name, &description) -} - -fn positional_description_format(out: &mut String, name: &str, description: &str) { - let info = argh_shared::CommandInfo { name: &*name, description }; - argh_shared::write_description(out, &info); -} - -/// Describes an option like this: -/// -f, --force force, ignore minor errors. This description -/// is so long that it wraps to the next line. -fn option_description(errors: &Errors, out: &mut String, field: &StructField<'_>) { - let short = field.attrs.short.as_ref().map(|s| s.value()); - let long_with_leading_dashes = field.long_name.as_ref().expect("missing long name for option"); - let description = - require_description(errors, field.name.span(), &field.attrs.description, "field"); - - option_description_format(out, short, long_with_leading_dashes, &description) -} - fn option_description_format( out: &mut String, short: Option, diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index 64ce20a..0210287 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -256,7 +256,7 @@ fn impl_from_args_struct( #[automatically_derived] impl argh::Help for #name { - const HELP_INFO: &'static argh::HelpInfo = #help_info; + const HELP_INFO: &'static argh::HelpInfo = &#help_info; } #top_or_sub_cmd_impl @@ -302,7 +302,7 @@ fn impl_help<'a>( }; positionals.push(quote! { - &argh::HelpPositionalInfo { + argh::HelpPositionalInfo { name: #name, description: #description, optionality: #optionality, @@ -345,7 +345,7 @@ fn impl_help<'a>( }; flags.push(quote! { - &argh::HelpFlagInfo { + argh::HelpFlagInfo { short: #short, long: #long, description: #description, @@ -381,7 +381,7 @@ fn impl_help<'a>( }); quote_spanned! { impl_span => - &argh::HelpInfo { + argh::HelpInfo { description: #description, examples: &[#( #examples, )*], notes: &[#( #notes, )*], @@ -395,7 +395,7 @@ fn impl_help<'a>( fn impl_from_args_struct_from_args<'a>( errors: &Errors, - _type_attrs: &TypeAttrs, + type_attrs: &TypeAttrs, fields: &'a [StructField<'a>], ) -> TokenStream { let init_fields = declare_local_storage_for_from_args_fields(fields); @@ -453,7 +453,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]) @@ -482,8 +482,8 @@ fn impl_from_args_struct_from_args<'a>( last_is_repeating: #last_positional_is_repeating, }, #parse_subcommands, - //&|| #help, - &|| ::HELP_INFO.help(#cmd_name_str_array_ident), + &|| #help, + //&|| ::HELP_INFO.help(#cmd_name_str_array_ident), )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); diff --git a/argh_shared/src/help.rs b/argh_shared/src/help.rs new file mode 100644 index 0000000..0cf0298 --- /dev/null +++ b/argh_shared/src/help.rs @@ -0,0 +1,310 @@ +// Copyright (c) 2022 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. + +//! TODO + +use { + super::{write_description, CommandInfo, INDENT}, + std::fmt, +}; + +const SECTION_SEPARATOR: &str = "\n\n"; + +const HELP_FLAG: HelpFlagInfo = HelpFlagInfo { + short: None, + long: "--help", + description: "display usage information", + optionality: HelpOptionality::Optional, + kind: HelpFieldKind::Switch, +}; + +/// TODO +pub struct HelpInfo<'a> { + /// TODO + pub description: &'a str, + /// TODO + pub examples: &'a [fn(&[&str]) -> String], + /// TODO + pub notes: &'a [fn(&[&str]) -> String], + /// TODO + pub flags: &'a [HelpFlagInfo<'a>], + /// TODO + pub positionals: &'a [HelpPositionalInfo<'a>], + /// TODO + pub subcommand: Option<&'a HelpSubCommandsInfo<'a>>, + /// TODO + pub error_codes: &'a [(isize, &'a str)], +} + +fn help_section( + out: &mut String, + command_name: &[&str], + heading: &str, + sections: &[fn(&[&str]) -> String], +) { + if !sections.is_empty() { + out.push_str(SECTION_SEPARATOR); + for section_fn in sections { + let section = section_fn(command_name); + + out.push_str(heading); + for line in section.split('\n') { + out.push('\n'); + out.push_str(INDENT); + out.push_str(line); + } + } + } +} + +impl<'a> HelpInfo<'a> { + /// TODO + pub fn help(&self, command_name: &[&str]) -> String { + let mut out = format!("Usage: {}", command_name.join(" ")); + + for positional in self.positionals { + out.push(' '); + positional.help_usage(&mut out); + } + + for flag in self.flags { + out.push(' '); + flag.help_usage(&mut out); + } + + if let Some(subcommand) = &self.subcommand { + out.push(' '); + if subcommand.optional { + out.push('['); + } + out.push_str(""); + if subcommand.optional { + out.push(']'); + } + out.push_str(" []"); + } + + out.push_str(SECTION_SEPARATOR); + + out.push_str(self.description); + + if !self.positionals.is_empty() { + out.push_str(SECTION_SEPARATOR); + out.push_str("Positional Arguments:"); + for positional in self.positionals { + positional.help_description(&mut out); + } + } + + out.push_str(SECTION_SEPARATOR); + out.push_str("Options:"); + for flag in self.flags { + flag.help_description(&mut out); + } + + // Also include "help" + HELP_FLAG.help_description(&mut out); + + if let Some(subcommand) = &self.subcommand { + out.push_str(SECTION_SEPARATOR); + out.push_str("Commands:"); + for cmd in subcommand.commands { + let info = CommandInfo { name: cmd.name, description: cmd.info.description }; + write_description(&mut out, &info); + } + } + + help_section(&mut out, command_name, "Examples:", self.examples); + + help_section(&mut out, command_name, "Notes:", self.notes); + + if !self.error_codes.is_empty() { + out.push_str(SECTION_SEPARATOR); + out.push_str("Error codes:"); + write_error_codes(&mut out, self.error_codes); + } + + out.push('\n'); + + out + } +} + +fn write_error_codes(out: &mut String, error_codes: &[(isize, &str)]) { + for (code, text) in error_codes { + out.push('\n'); + out.push_str(INDENT); + out.push_str(&format!("{} {}", code, text)); + } +} + +impl<'a> fmt::Debug for HelpInfo<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let examples = self.examples.iter().map(|f| f(&["{command_name}"])).collect::>(); + let notes = self.notes.iter().map(|f| f(&["{command_name}"])).collect::>(); + f.debug_struct("HelpInfo") + .field("description", &self.description) + .field("examples", &examples) + .field("notes", ¬es) + .field("flags", &self.flags) + .field("positionals", &self.positionals) + .field("subcommand", &self.subcommand) + .field("error_codes", &self.error_codes) + .finish() + } +} + +/// TODO +#[derive(Debug)] +pub struct HelpSubCommandsInfo<'a> { + /// TODO + pub optional: bool, + /// TODO + pub commands: &'a [&'a HelpSubCommandInfo<'a>], +} + +/// TODO +#[derive(Debug)] +pub struct HelpSubCommandInfo<'a> { + /// TODO + pub name: &'a str, + /// TODO + pub info: &'a HelpInfo<'a>, +} + +/// TODO +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum HelpOptionality { + /// TODO + None, + /// TODO + Optional, + /// TODO + Repeating, +} + +impl HelpOptionality { + /// TODO + fn is_required(&self) -> bool { + matches!(self, HelpOptionality::None) + } +} + +/// TODO +#[derive(Debug)] +pub struct HelpPositionalInfo<'a> { + /// TODO + pub name: &'a str, + /// TODO + pub description: &'a str, + /// TODO + pub optionality: HelpOptionality, +} + +impl<'a> HelpPositionalInfo<'a> { + /// Add positional arguments like `[...]` to a help format string. + pub fn help_usage(&self, out: &mut String) { + if !self.optionality.is_required() { + out.push('['); + } + + out.push('<'); + out.push_str(self.name); + + if self.optionality == HelpOptionality::Repeating { + out.push_str("..."); + } + + out.push('>'); + + if !self.optionality.is_required() { + out.push(']'); + } + } + + /// Describes a positional argument like this: + /// hello positional argument description + pub fn help_description(&self, out: &mut String) { + let info = CommandInfo { name: self.name, description: self.description }; + write_description(out, &info); + } +} + +/// TODO +#[derive(Debug)] +pub struct HelpFlagInfo<'a> { + /// TODO + pub short: Option, + /// TODO + pub long: &'a str, + /// TODO + pub description: &'a str, + /// TODO + pub optionality: HelpOptionality, + /// TODO + pub kind: HelpFieldKind<'a>, +} + +/// TODO +#[derive(Debug)] +pub enum HelpFieldKind<'a> { + /// TODO + Switch, + /// TODO + Option { + /// TODO + arg_name: &'a str, + }, +} + +impl<'a> HelpFlagInfo<'a> { + /// Add options like `[-f ]` to a help format string. + /// This function must only be called on options (things with `long_name.is_some()`) + pub fn help_usage(&self, out: &mut String) { + if !self.optionality.is_required() { + out.push('['); + } + + if let Some(short) = self.short { + out.push('-'); + out.push(short); + } else { + out.push_str(self.long); + } + + match self.kind { + HelpFieldKind::Switch => {} + HelpFieldKind::Option { arg_name } => { + out.push_str(" <"); + out.push_str(arg_name); + + if self.optionality == HelpOptionality::Repeating { + out.push_str("..."); + } + + out.push('>'); + } + } + + if !self.optionality.is_required() { + out.push(']'); + } + } + + /// Describes an option like this: + /// -f, --force force, ignore minor errors. This description + /// is so long that it wraps to the next line. + pub fn help_description(&self, out: &mut String) { + let mut name = String::new(); + if let Some(short) = self.short { + name.push('-'); + name.push(short); + name.push_str(", "); + } + name.push_str(self.long); + + let info = CommandInfo { name: &name, description: self.description }; + write_description(out, &info); + } +} diff --git a/argh_shared/src/lib.rs b/argh_shared/src/lib.rs index c6e7a5c..d25ad0e 100644 --- a/argh_shared/src/lib.rs +++ b/argh_shared/src/lib.rs @@ -6,6 +6,13 @@ //! //! This library is intended only for internal use by these two crates. +mod help; + +pub use crate::help::{ + HelpFieldKind, HelpFlagInfo, HelpInfo, HelpOptionality, HelpPositionalInfo, HelpSubCommandInfo, + HelpSubCommandsInfo, +}; + /// Information about a particular command used for output. pub struct CommandInfo<'a> { /// The name of the command. From 60e0f4cb024ee792c8ee06b705bd56c9781581fb Mon Sep 17 00:00:00 2001 From: Erick Tryzelaar Date: Tue, 8 Feb 2022 11:33:09 -0800 Subject: [PATCH 3/4] Write a hacky command_name substitutor --- argh_derive/src/lib.rs | 10 ++------ argh_shared/src/help.rs | 54 ++++++++++++----------------------------- 2 files changed, 18 insertions(+), 46 deletions(-) diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs index 0210287..33f2d47 100644 --- a/argh_derive/src/lib.rs +++ b/argh_derive/src/lib.rs @@ -367,14 +367,8 @@ fn impl_help<'a>( let description = help::require_description(errors, Span::call_site(), &type_attrs.description, "type"); - let examples = type_attrs - .examples - .iter() - .map(|e| quote! { |command_name| format!(#e, command_name = command_name.join(" ")) }); - let notes = type_attrs - .notes - .iter() - .map(|e| quote! { |command_name| format!(#e, command_name = command_name.join(" ")) }); + let examples = type_attrs.examples.iter().map(|e| quote! { #e }); + let notes = type_attrs.notes.iter().map(|e| quote! { #e }); let error_codes = type_attrs.error_codes.iter().map(|(code, text)| { quote! { (#code, #text) } diff --git a/argh_shared/src/help.rs b/argh_shared/src/help.rs index 0cf0298..cc1cf6e 100644 --- a/argh_shared/src/help.rs +++ b/argh_shared/src/help.rs @@ -4,10 +4,7 @@ //! TODO -use { - super::{write_description, CommandInfo, INDENT}, - std::fmt, -}; +use super::{write_description, CommandInfo, INDENT}; const SECTION_SEPARATOR: &str = "\n\n"; @@ -20,13 +17,14 @@ const HELP_FLAG: HelpFlagInfo = HelpFlagInfo { }; /// TODO +#[derive(Clone, Debug, PartialEq, Eq)] pub struct HelpInfo<'a> { /// TODO pub description: &'a str, /// TODO - pub examples: &'a [fn(&[&str]) -> String], + pub examples: &'a [&'a str], /// TODO - pub notes: &'a [fn(&[&str]) -> String], + pub notes: &'a [&'a str], /// TODO pub flags: &'a [HelpFlagInfo<'a>], /// TODO @@ -37,16 +35,11 @@ pub struct HelpInfo<'a> { pub error_codes: &'a [(isize, &'a str)], } -fn help_section( - out: &mut String, - command_name: &[&str], - heading: &str, - sections: &[fn(&[&str]) -> String], -) { +fn help_section(out: &mut String, command_name: &str, heading: &str, sections: &[&str]) { if !sections.is_empty() { out.push_str(SECTION_SEPARATOR); - for section_fn in sections { - let section = section_fn(command_name); + for section in sections { + let section = section.replace("{command_name}", command_name); out.push_str(heading); for line in section.split('\n') { @@ -61,7 +54,8 @@ fn help_section( impl<'a> HelpInfo<'a> { /// TODO pub fn help(&self, command_name: &[&str]) -> String { - let mut out = format!("Usage: {}", command_name.join(" ")); + let command_name = command_name.join(" "); + let mut out = format!("Usage: {}", command_name); for positional in self.positionals { out.push(' '); @@ -115,9 +109,9 @@ impl<'a> HelpInfo<'a> { } } - help_section(&mut out, command_name, "Examples:", self.examples); + help_section(&mut out, &command_name, "Examples:", self.examples); - help_section(&mut out, command_name, "Notes:", self.notes); + help_section(&mut out, &command_name, "Notes:", self.notes); if !self.error_codes.is_empty() { out.push_str(SECTION_SEPARATOR); @@ -139,24 +133,8 @@ fn write_error_codes(out: &mut String, error_codes: &[(isize, &str)]) { } } -impl<'a> fmt::Debug for HelpInfo<'a> { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let examples = self.examples.iter().map(|f| f(&["{command_name}"])).collect::>(); - let notes = self.notes.iter().map(|f| f(&["{command_name}"])).collect::>(); - f.debug_struct("HelpInfo") - .field("description", &self.description) - .field("examples", &examples) - .field("notes", ¬es) - .field("flags", &self.flags) - .field("positionals", &self.positionals) - .field("subcommand", &self.subcommand) - .field("error_codes", &self.error_codes) - .finish() - } -} - /// TODO -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct HelpSubCommandsInfo<'a> { /// TODO pub optional: bool, @@ -165,7 +143,7 @@ pub struct HelpSubCommandsInfo<'a> { } /// TODO -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct HelpSubCommandInfo<'a> { /// TODO pub name: &'a str, @@ -192,7 +170,7 @@ impl HelpOptionality { } /// TODO -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct HelpPositionalInfo<'a> { /// TODO pub name: &'a str, @@ -232,7 +210,7 @@ impl<'a> HelpPositionalInfo<'a> { } /// TODO -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct HelpFlagInfo<'a> { /// TODO pub short: Option, @@ -247,7 +225,7 @@ pub struct HelpFlagInfo<'a> { } /// TODO -#[derive(Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum HelpFieldKind<'a> { /// TODO Switch, From b8b27f25fe1d98727c92a13db521723448784cbf Mon Sep 17 00:00:00 2001 From: Erick Tryzelaar Date: Tue, 8 Feb 2022 11:33:25 -0800 Subject: [PATCH 4/4] Add optional serde serialization for help --- argh/Cargo.toml | 3 +++ argh_shared/Cargo.toml | 3 +++ argh_shared/src/help.rs | 10 ++++++++++ 3 files changed, 16 insertions(+) diff --git a/argh/Cargo.toml b/argh/Cargo.toml index ec92149..da240f0 100644 --- a/argh/Cargo.toml +++ b/argh/Cargo.toml @@ -12,3 +12,6 @@ readme = "README.md" [dependencies] argh_shared = { version = "0.1.7", path = "../argh_shared" } argh_derive = { version = "0.1.7", path = "../argh_derive" } + +[features] +serde = [ "argh_shared/serde_derive" ] diff --git a/argh_shared/Cargo.toml b/argh_shared/Cargo.toml index 1f7e694..11b25c8 100644 --- a/argh_shared/Cargo.toml +++ b/argh_shared/Cargo.toml @@ -7,3 +7,6 @@ license = "BSD-3-Clause" description = "Derive-based argument parsing optimized for code size" repository = "https://github.com/google/argh" readme = "README.md" + +[dependencies] +serde_derive = { version = "1", optional = true } diff --git a/argh_shared/src/help.rs b/argh_shared/src/help.rs index cc1cf6e..a4de21f 100644 --- a/argh_shared/src/help.rs +++ b/argh_shared/src/help.rs @@ -6,6 +6,9 @@ use super::{write_description, CommandInfo, INDENT}; +#[cfg(feature = "serde")] +use serde_derive::{Deserialize, Serialize}; + const SECTION_SEPARATOR: &str = "\n\n"; const HELP_FLAG: HelpFlagInfo = HelpFlagInfo { @@ -18,6 +21,7 @@ const HELP_FLAG: HelpFlagInfo = HelpFlagInfo { /// TODO #[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct HelpInfo<'a> { /// TODO pub description: &'a str, @@ -135,6 +139,7 @@ fn write_error_codes(out: &mut String, error_codes: &[(isize, &str)]) { /// TODO #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct HelpSubCommandsInfo<'a> { /// TODO pub optional: bool, @@ -144,6 +149,7 @@ pub struct HelpSubCommandsInfo<'a> { /// TODO #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct HelpSubCommandInfo<'a> { /// TODO pub name: &'a str, @@ -153,6 +159,7 @@ pub struct HelpSubCommandInfo<'a> { /// TODO #[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum HelpOptionality { /// TODO None, @@ -171,6 +178,7 @@ impl HelpOptionality { /// TODO #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct HelpPositionalInfo<'a> { /// TODO pub name: &'a str, @@ -211,6 +219,7 @@ impl<'a> HelpPositionalInfo<'a> { /// TODO #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct HelpFlagInfo<'a> { /// TODO pub short: Option, @@ -226,6 +235,7 @@ pub struct HelpFlagInfo<'a> { /// TODO #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum HelpFieldKind<'a> { /// TODO Switch,