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/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..754eefa 100644 --- a/argh/src/lib.rs +++ b/argh/src/lib.rs @@ -171,11 +171,23 @@ use std::str::FromStr; -pub use argh_derive::FromArgs; +pub use { + 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. @@ -455,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 ab72a40..33f2d47 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,12 +254,139 @@ 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_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! { #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) } + }); + + 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, @@ -348,6 +477,7 @@ fn impl_from_args_struct_from_args<'a>( }, #parse_subcommands, &|| #help, + //&|| ::HELP_INFO.help(#cmd_name_str_array_ident), )?; let mut #missing_requirements_ident = argh::MissingRequirements::default(); @@ -527,6 +657,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 +1042,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, + )*], + }; + } } } 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 new file mode 100644 index 0000000..a4de21f --- /dev/null +++ b/argh_shared/src/help.rs @@ -0,0 +1,298 @@ +// 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}; + +#[cfg(feature = "serde")] +use serde_derive::{Deserialize, Serialize}; + +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 +#[derive(Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct HelpInfo<'a> { + /// TODO + pub description: &'a str, + /// TODO + pub examples: &'a [&'a str], + /// TODO + pub notes: &'a [&'a str], + /// 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: &[&str]) { + if !sections.is_empty() { + out.push_str(SECTION_SEPARATOR); + for section in sections { + let section = section.replace("{command_name}", 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 command_name = command_name.join(" "); + let mut out = format!("Usage: {}", command_name); + + 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)); + } +} + +/// TODO +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct HelpSubCommandsInfo<'a> { + /// TODO + pub optional: bool, + /// TODO + pub commands: &'a [&'a HelpSubCommandInfo<'a>], +} + +/// TODO +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub struct HelpSubCommandInfo<'a> { + /// TODO + pub name: &'a str, + /// TODO + pub info: &'a HelpInfo<'a>, +} + +/// TODO +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum HelpOptionality { + /// TODO + None, + /// TODO + Optional, + /// TODO + Repeating, +} + +impl HelpOptionality { + /// TODO + fn is_required(&self) -> bool { + matches!(self, HelpOptionality::None) + } +} + +/// TODO +#[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +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, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +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, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +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.