Repository: google/argh
Pull request #158 (2023-03-22 21:19:38)

Update to syn-2
Updates argh to the interface used in syn-2.x


Patch:
diff --git a/argh/src/lib.rs b/argh/src/lib.rs
index 4fb6ff5..c419e6a 100644
--- a/argh/src/lib.rs
+++ b/argh/src/lib.rs
@@ -318,11 +318,30 @@
 
 use std::str::FromStr;
 
-pub use argh_derive::FromArgs;
+pub use argh_derive::{ArgsInfo, FromArgs};
 
 /// Information about a particular command used for output.
 pub type CommandInfo = argh_shared::CommandInfo<'static>;
 
+/// Information about the command including the options and arguments.
+pub type CommandInfoWithArgs = argh_shared::CommandInfoWithArgs<'static>;
+
+/// Information about a subcommand.
+pub type SubCommandInfo = argh_shared::SubCommandInfo<'static>;
+
+pub use argh_shared::{ErrorCodeInfo, FlagInfo, FlagInfoKind, Optionality, PositionalInfo};
+
+/// Structured information about the command line arguments.
+pub trait ArgsInfo {
+    /// Returns the argument info.
+    fn get_args_info() -> CommandInfoWithArgs;
+
+    /// Returns the list of subcommands
+    fn get_subcommands() -> Vec<SubCommandInfo> {
+        Self::get_args_info().commands
+    }
+}
+
 /// Types which can be constructed from a set of commandline arguments.
 pub trait FromArgs: Sized {
     /// Construct the type from an input set of arguments.
diff --git a/argh_derive/Cargo.toml b/argh_derive/Cargo.toml
index 750b494..6935b02 100644
--- a/argh_derive/Cargo.toml
+++ b/argh_derive/Cargo.toml
@@ -14,5 +14,5 @@ proc-macro = true
 [dependencies]
 proc-macro2 = "1.0"
 quote = "1.0"
-syn = "1.0"
+syn = "2.0"
 argh_shared = { version = "0.1.10", path = "../argh_shared" }
diff --git a/argh_derive/src/args_info.rs b/argh_derive/src/args_info.rs
new file mode 100644
index 0000000..ae640a7
--- /dev/null
+++ b/argh_derive/src/args_info.rs
@@ -0,0 +1,356 @@
+// Copyright (c) 2023 Google LLC All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+use crate::{
+    enum_only_single_field_unnamed_variants,
+    errors::Errors,
+    help::require_description,
+    parse_attrs::{check_enum_type_attrs, FieldAttrs, FieldKind, TypeAttrs, VariantAttrs},
+    Optionality, StructField,
+};
+use proc_macro2::{Span, TokenStream};
+use quote::{quote, quote_spanned, ToTokens};
+use syn::LitStr;
+
+/// Implement the derive macro for ArgsInfo.
+pub(crate) fn impl_args_info(input: &syn::DeriveInput) -> TokenStream {
+    let errors = &Errors::default();
+
+    // parse the types
+    let type_attrs = &TypeAttrs::parse(errors, input);
+
+    // Based on the type generate the appropriate code.
+    let mut output_tokens = match &input.data {
+        syn::Data::Struct(ds) => {
+            impl_arg_info_struct(errors, &input.ident, type_attrs, &input.generics, ds)
+        }
+        syn::Data::Enum(de) => {
+            impl_arg_info_enum(errors, &input.ident, type_attrs, &input.generics, de)
+        }
+        syn::Data::Union(_) => {
+            errors.err(input, "`#[derive(ArgsInfo)]` cannot be applied to unions");
+            TokenStream::new()
+        }
+    };
+    errors.to_tokens(&mut output_tokens);
+    output_tokens
+}
+
+/// Implement the ArgsInfo trait for a struct annotated with argh attributes.
+fn impl_arg_info_struct(
+    errors: &Errors,
+    name: &syn::Ident,
+    type_attrs: &TypeAttrs,
+    generic_args: &syn::Generics,
+    ds: &syn::DataStruct,
+) -> TokenStream {
+    // Collect the fields, skipping fields that are not supported.
+    let fields = match &ds.fields {
+        syn::Fields::Named(fields) => fields,
+        syn::Fields::Unnamed(_) => {
+            errors.err(
+                &ds.struct_token,
+                "`#![derive(ArgsInfo)]` is not currently supported on tuple structs",
+            );
+            return TokenStream::new();
+        }
+        syn::Fields::Unit => {
+            errors.err(&ds.struct_token, "#![derive(ArgsInfo)]` cannot be applied to unit structs");
+            return TokenStream::new();
+        }
+    };
+
+    // Map the fields into StructField objects.
+    let fields: Vec<_> = fields
+        .named
+        .iter()
+        .filter_map(|field| {
+            let attrs = FieldAttrs::parse(errors, field);
+            StructField::new(errors, field, attrs)
+        })
+        .collect();
+
+    let impl_span = Span::call_site();
+
+    // Generate the implementation of `get_args_info()` for this struct.
+    let args_info = impl_args_info_data(name, errors, type_attrs, &fields);
+
+    // Split out the generics info for the impl declaration.
+    let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl();
+
+    quote_spanned! { impl_span =>
+        #[automatically_derived]
+        impl #impl_generics argh::ArgsInfo for #name #ty_generics #where_clause {
+           fn get_args_info() -> argh::CommandInfoWithArgs {
+            #args_info
+           }
+        }
+    }
+}
+
+/// Implement ArgsInfo for an enum. The enum is a collection of subcommands.
+fn impl_arg_info_enum(
+    errors: &Errors,
+    name: &syn::Ident,
+    type_attrs: &TypeAttrs,
+    generic_args: &syn::Generics,
+    de: &syn::DataEnum,
+) -> TokenStream {
+    // Validate the enum is OK for argh.
+    check_enum_type_attrs(errors, type_attrs, &de.enum_token.span);
+
+    // Ensure that `#[argh(subcommand)]` is present.
+    if type_attrs.is_subcommand.is_none() {
+        errors.err_span(
+            de.enum_token.span,
+            concat!(
+                "`#![derive(ArgsInfo)]` on `enum`s can only be used to enumerate subcommands.\n",
+                "Consider adding `#[argh(subcommand)]` to the `enum` declaration.",
+            ),
+        );
+    }
+
+    // One of the variants can be annotated as providing dynamic subcommands.
+    // We treat this differently since we need to call a function at runtime
+    // to determine the subcommands provided.
+    let mut dynamic_type_and_variant = None;
+
+    // An enum variant like `<name>(<ty>)`. This is used to collect
+    // the type of the variant for each subcommand.
+    struct ArgInfoVariant<'a> {
+        ty: &'a syn::Type,
+    }
+
+    let variants: Vec<ArgInfoVariant<'_>> = de
+        .variants
+        .iter()
+        .filter_map(|variant| {
+            let name = &variant.ident;
+            let ty = enum_only_single_field_unnamed_variants(errors, &variant.fields)?;
+            if VariantAttrs::parse(errors, variant).is_dynamic.is_some() {
+                if dynamic_type_and_variant.is_some() {
+                    errors.err(variant, "Only one variant can have the `dynamic` attribute");
+                }
+                dynamic_type_and_variant = Some((ty, name));
+                None
+            } else {
+                Some(ArgInfoVariant { ty })
+            }
+        })
+        .collect();
+
+    let dynamic_subcommands = if let Some((dynamic_type, _)) = dynamic_type_and_variant {
+        quote! {
+            <#dynamic_type as argh::DynamicSubCommand>::commands().iter()
+            .map(|s|
+         SubCommandInfo {
+                name: s.name,
+                command: CommandInfoWithArgs {
+                    name: s.name,
+                    description: s.description,
+                    ..Default::default()
+                }
+            }).collect()
+        }
+    } else {
+        quote! { vec![]}
+    };
+
+    let variant_ty_info = variants.iter().map(|t| {
+        let ty = t.ty;
+        quote!(
+            argh::SubCommandInfo {
+                name: #ty::get_args_info().name,
+                command: #ty::get_args_info()
+            }
+        )
+    });
+
+    let cmd_name = if let Some(id) = &type_attrs.name {
+        id.clone()
+    } else {
+        LitStr::new("", Span::call_site())
+    };
+
+    let (impl_generics, ty_generics, where_clause) = generic_args.split_for_impl();
+
+    quote! {
+        #[automatically_derived]
+        impl #impl_generics argh::ArgsInfo for #name #ty_generics #where_clause {
+           fn get_args_info() -> argh::CommandInfoWithArgs {
+
+            let mut the_subcommands = vec![#(#variant_ty_info),*];
+            let mut dynamic_commands = #dynamic_subcommands;
+
+            the_subcommands.append(&mut dynamic_commands);
+
+
+            argh::CommandInfoWithArgs {
+                name: #cmd_name,
+               /// A short description of the command's functionality.
+                description: " enum of subcommands",
+                commands: the_subcommands,
+                ..Default::default()
+               }
+           } // end of get_args_ifo
+        }  // end of impl ArgsInfo
+    }
+}
+
+fn impl_args_info_data<'a>(
+    name: &proc_macro2::Ident,
+    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![];
+
+    // Add the implicit --help flag
+    flags.push(quote! {
+        argh::FlagInfo {
+            short: None,
+            long: "--help",
+            description: "display usage information",
+            optionality: argh::Optionality::Optional,
+            kind: argh::FlagInfoKind::Switch,
+            hidden: false
+        }
+    });
+
+    for field in fields {
+        let optionality = match field.optionality {
+            Optionality::None => quote! { argh::Optionality::Required },
+            Optionality::Defaulted(_) => quote! { argh::Optionality::Optional },
+            Optionality::Optional => quote! { argh::Optionality::Optional },
+            Optionality::Repeating if field.attrs.greedy.is_some() => {
+                quote! { argh::Optionality::Greedy }
+            }
+            Optionality::Repeating => quote! { argh::Optionality::Repeating },
+        };
+
+        match field.kind {
+            FieldKind::Positional => {
+                let name = field.positional_arg_name();
+
+                let description = if let Some(desc) = &field.attrs.description {
+                    desc.content.value().trim().to_owned()
+                } else {
+                    String::new()
+                };
+                let hidden = field.attrs.hidden_help;
+
+                positionals.push(quote! {
+                    argh::PositionalInfo {
+                        name: #name,
+                        description: #description,
+                        optionality: #optionality,
+                        hidden: #hidden,
+                    }
+                });
+            }
+            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 = require_description(
+                    errors,
+                    field.name.span(),
+                    &field.attrs.description,
+                    "field",
+                );
+
+                let kind = if field.kind == FieldKind::Switch {
+                    quote! {
+                        argh::FlagInfoKind::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::FlagInfoKind::Option {
+                            arg_name: #arg_name,
+                        }
+                    }
+                };
+
+                let hidden = field.attrs.hidden_help;
+
+                flags.push(quote! {
+                    argh::FlagInfo {
+                        short: #short,
+                        long: #long,
+                        description: #description,
+                        optionality: #optionality,
+                        kind: #kind,
+                        hidden: #hidden,
+                    }
+                });
+            }
+            FieldKind::SubCommand => {}
+        }
+    }
+
+    let empty_str = syn::LitStr::new("", Span::call_site());
+    let type_name = LitStr::new(&name.to_string(), Span::call_site());
+    let subcommand_name = if type_attrs.is_subcommand.is_some() {
+        type_attrs.name.as_ref().unwrap_or_else(|| {
+            errors.err(name, "`#[argh(name = \"...\")]` attribute is required for subcommands");
+            &empty_str
+        })
+    } else {
+        &type_name
+    };
+
+    let subcommand = if let Some(subcommand) = subcommand {
+        let subcommand_ty = subcommand.ty_without_wrapper;
+        quote! {
+            #subcommand_ty::get_subcommands()
+        }
+    } else {
+        quote! {vec![]}
+    };
+
+    let description =
+        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! { argh::ErrorCodeInfo{code:#code, description: #text} }
+    });
+
+    quote_spanned! { impl_span =>
+        argh::CommandInfoWithArgs {
+            name: #subcommand_name,
+            description: #description,
+            examples: &[#( #examples, )*],
+            notes: &[#( #notes, )*],
+            positionals: &[#( #positionals, )*],
+            flags: &[#( #flags, )*],
+            commands: #subcommand,
+            error_codes: &[#( #error_codes, )*],
+        }
+    }
+}
diff --git a/argh_derive/src/errors.rs b/argh_derive/src/errors.rs
index cfad383..fc5b584 100644
--- a/argh_derive/src/errors.rs
+++ b/argh_derive/src/errors.rs
@@ -14,15 +14,15 @@ pub struct Errors {
     errors: RefCell<Vec<syn::Error>>,
 }
 
-/// Produce functions to expect particular variants of `syn::Lit`
+/// Produce functions to expect particular literals in `syn::Expr`
 macro_rules! expect_lit_fn {
     ($(($fn_name:ident, $syn_type:ident, $variant:ident, $lit_name:literal),)*) => {
         $(
-            pub fn $fn_name<'a>(&self, lit: &'a syn::Lit) -> Option<&'a syn::$syn_type> {
-                if let syn::Lit::$variant(inner) = lit {
+            pub fn $fn_name<'a>(&self, e: &'a syn::Expr) -> Option<&'a syn::$syn_type> {
+                if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::$variant(inner), .. }) = e {
                     Some(inner)
                 } else {
-                    self.unexpected_lit($lit_name, lit);
+                    self.unexpected_lit($lit_name, e);
                     None
                 }
             }
@@ -65,28 +65,6 @@ impl Errors {
         self.err_span(first, &["First ", attr_kind, " attribute here"].concat());
     }
 
-    /// Error on literals, expecting attribute syntax.
-    pub fn expect_nested_meta<'a>(&self, nm: &'a syn::NestedMeta) -> Option<&'a syn::Meta> {
-        match nm {
-            syn::NestedMeta::Lit(l) => {
-                self.err(l, "Unexpected literal");
-                None
-            }
-            syn::NestedMeta::Meta(m) => Some(m),
-        }
-    }
-
-    /// Error on attribute syntax, expecting literals
-    pub fn expect_nested_lit<'a>(&self, nm: &'a syn::NestedMeta) -> Option<&'a syn::Lit> {
-        match nm {
-            syn::NestedMeta::Meta(m) => {
-                self.err(m, "Expected literal");
-                None
-            }
-            syn::NestedMeta::Lit(l) => Some(l),
-        }
-    }
-
     expect_lit_fn![
         (expect_lit_str, LitStr, Str, "string"),
         (expect_lit_char, LitChar, Char, "character"),
@@ -99,7 +77,7 @@ impl Errors {
         (expect_meta_name_value, MetaNameValue, NameValue, "name-value pair"),
     ];
 
-    fn unexpected_lit(&self, expected: &str, found: &syn::Lit) {
+    fn unexpected_lit(&self, expected: &str, found: &syn::Expr) {
         fn lit_kind(lit: &syn::Lit) -> &'static str {
             use syn::Lit::{Bool, Byte, ByteStr, Char, Float, Int, Str, Verbatim};
             match lit {
@@ -111,13 +89,21 @@ impl Errors {
                 Float(_) => "float",
                 Bool(_) => "boolean",
                 Verbatim(_) => "unknown (possibly extra-large integer)",
+                _ => "unknown literal kind",
             }
         }
 
-        self.err(
-            found,
-            &["Expected ", expected, " literal, found ", lit_kind(found), " literal"].concat(),
-        )
+        if let syn::Expr::Lit(syn::ExprLit { lit, .. }) = found {
+            self.err(
+                found,
+                &["Expected ", expected, " literal, found ", lit_kind(lit), " literal"].concat(),
+            )
+        } else {
+            self.err(
+                found,
+                &["Expected ", expected, " literal, found non-literal expression."].concat(),
+            )
+        }
     }
 
     fn unexpected_meta(&self, expected: &str, found: &syn::Meta) {
@@ -155,6 +141,17 @@ impl Errors {
     pub fn push(&self, err: syn::Error) {
         self.errors.borrow_mut().push(err);
     }
+
+    /// Convert a `syn::Result` to an `Option`, logging the error if present.
+    pub fn ok<T>(&self, r: syn::Result<T>) -> Option<T> {
+        match r {
+            Ok(v) => Some(v),
+            Err(e) => {
+                self.push(e);
+                None
+            }
+        }
+    }
 }
 
 impl ToTokens for Errors {
diff --git a/argh_derive/src/lib.rs b/argh_derive/src/lib.rs
index 94cc437..8b3b7ee 100644
--- a/argh_derive/src/lib.rs
+++ b/argh_derive/src/lib.rs
@@ -19,6 +19,7 @@ use {
     syn::{spanned::Spanned, GenericArgument, LitStr, PathArguments, Type},
 };
 
+mod args_info;
 mod errors;
 mod help;
 mod parse_attrs;
@@ -31,6 +32,14 @@ pub fn argh_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
     gen.into()
 }
 
+/// Entrypoint for `#[derive(ArgsInfo)]`.
+#[proc_macro_derive(ArgsInfo, attributes(argh))]
+pub fn args_info_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+    let ast = syn::parse_macro_input!(input as syn::DeriveInput);
+    let gen = args_info::impl_args_info(&ast);
+    gen.into()
+}
+
 /// Transform the input into a token stream containing any generated implementations,
 /// as well as all errors that occurred.
 fn impl_from_args(input: &syn::DeriveInput) -> TokenStream {
diff --git a/argh_derive/src/parse_attrs.rs b/argh_derive/src/parse_attrs.rs
index 04dcbdd..10ebcb5 100644
--- a/argh_derive/src/parse_attrs.rs
+++ b/argh_derive/src/parse_attrs.rs
@@ -76,49 +76,47 @@ impl FieldAttrs {
                 continue;
             };
 
-            for meta in &ml.nested {
-                let meta = if let Some(m) = errors.expect_nested_meta(meta) { m } else { continue };
-
+            for meta in ml {
                 let name = meta.path();
                 if name.is_ident("arg_name") {
-                    if let Some(m) = errors.expect_meta_name_value(meta) {
+                    if let Some(m) = errors.expect_meta_name_value(&meta) {
                         this.parse_attr_arg_name(errors, m);
                     }
                 } else if name.is_ident("default") {
-                    if let Some(m) = errors.expect_meta_name_value(meta) {
+                    if let Some(m) = errors.expect_meta_name_value(&meta) {
                         this.parse_attr_default(errors, m);
                     }
                 } else if name.is_ident("description") {
-                    if let Some(m) = errors.expect_meta_name_value(meta) {
+                    if let Some(m) = errors.expect_meta_name_value(&meta) {
                         parse_attr_description(errors, m, &mut this.description);
                     }
                 } else if name.is_ident("from_str_fn") {
-                    if let Some(m) = errors.expect_meta_list(meta) {
+                    if let Some(m) = errors.expect_meta_list(&meta) {
                         this.parse_attr_from_str_fn(errors, m);
                     }
                 } else if name.is_ident("long") {
-                    if let Some(m) = errors.expect_meta_name_value(meta) {
+                    if let Some(m) = errors.expect_meta_name_value(&meta) {
                         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) {
+                    if let Some(m) = errors.expect_meta_name_value(&meta) {
                         this.parse_attr_short(errors, m);
                     }
                 } else if name.is_ident("subcommand") {
                     parse_attr_field_type(
                         errors,
-                        meta,
+                        &meta,
                         FieldKind::SubCommand,
                         &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,
-                        meta,
+                        &meta,
                         FieldKind::Positional,
                         &mut this.field_type,
                     );
@@ -189,7 +187,7 @@ impl FieldAttrs {
     fn parse_attr_short(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
         if let Some(first) = &self.short {
             errors.duplicate_attrs("short", first, m);
-        } else if let Some(lit_char) = errors.expect_lit_char(&m.lit) {
+        } else if let Some(lit_char) = errors.expect_lit_char(&m.value) {
             self.short = Some(lit_char.clone());
             if !lit_char.value().is_ascii() {
                 errors.err(lit_char, "Short names must be ASCII");
@@ -217,16 +215,7 @@ fn parse_attr_fn_name(
         errors.duplicate_attrs(attr_name, first, m);
     }
 
-    if m.nested.len() != 1 {
-        errors.err(&m.nested, "Expected a single argument containing the function name");
-        return;
-    }
-
-    // `unwrap` will not fail because of the call above
-    let nested = m.nested.first().unwrap();
-    if let Some(path) = errors.expect_nested_meta(nested).and_then(|m| errors.expect_meta_word(m)) {
-        *slot = path.get_ident().cloned();
-    }
+    *slot = errors.ok(m.parse_args());
 }
 
 fn parse_attr_field_type(
@@ -246,7 +235,7 @@ fn parse_attr_field_type(
 
 // Whether the attribute is one like `#[<name> ...]`
 fn is_matching_attr(name: &str, attr: &syn::Attribute) -> bool {
-    attr.path.segments.len() == 1 && attr.path.segments[0].ident == name
+    attr.path().segments.len() == 1 && attr.path().segments[0].ident == name
 }
 
 /// Checks for `#[doc ...]`, which is generated by doc comments.
@@ -259,34 +248,18 @@ fn is_argh_attr(attr: &syn::Attribute) -> bool {
     is_matching_attr("argh", attr)
 }
 
-fn attr_to_meta_subtype<R: Clone>(
+/// Filters out non-`#[argh(...)]` attributes and converts to a sequence of `syn::Meta`.
+fn argh_attr_to_meta_list(
     errors: &Errors,
     attr: &syn::Attribute,
-    f: impl FnOnce(&syn::Meta) -> Option<&R>,
-) -> Option<R> {
-    match attr.parse_meta() {
-        Ok(meta) => f(&meta).cloned(),
-        Err(e) => {
-            errors.push(e);
-            None
-        }
-    }
-}
-
-fn attr_to_meta_list(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaList> {
-    attr_to_meta_subtype(errors, attr, |m| errors.expect_meta_list(m))
-}
-
-fn attr_to_meta_name_value(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaNameValue> {
-    attr_to_meta_subtype(errors, attr, |m| errors.expect_meta_name_value(m))
-}
-
-/// Filters out non-`#[argh(...)]` attributes and converts to `syn::MetaList`.
-fn argh_attr_to_meta_list(errors: &Errors, attr: &syn::Attribute) -> Option<syn::MetaList> {
+) -> Option<impl IntoIterator<Item = syn::Meta>> {
     if !is_argh_attr(attr) {
         return None;
     }
-    attr_to_meta_list(errors, attr)
+    let ml = errors.expect_meta_list(&attr.meta)?;
+    errors.ok(ml.parse_args_with(
+        syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated,
+    ))
 }
 
 /// Represents a `#[derive(FromArgs)]` type's top-level attributes.
@@ -317,32 +290,31 @@ impl TypeAttrs {
                 continue;
             };
 
-            for meta in &ml.nested {
-                let meta = if let Some(m) = errors.expect_nested_meta(meta) { m } else { continue };
-
+            for meta in ml {
                 let name = meta.path();
                 if name.is_ident("description") {
-                    if let Some(m) = errors.expect_meta_name_value(meta) {
+                    if let Some(m) = errors.expect_meta_name_value(&meta) {
                         parse_attr_description(errors, m, &mut this.description);
                     }
                 } else if name.is_ident("error_code") {
-                    if let Some(m) = errors.expect_meta_list(meta) {
+                    if let Some(m) = errors.expect_meta_list(&meta) {
                         this.parse_attr_error_code(errors, m);
                     }
                 } else if name.is_ident("example") {
-                    if let Some(m) = errors.expect_meta_name_value(meta) {
+                    if let Some(m) = errors.expect_meta_name_value(&meta) {
                         this.parse_attr_example(errors, m);
                     }
                 } else if name.is_ident("name") {
-                    if let Some(m) = errors.expect_meta_name_value(meta) {
+                    if let Some(m) = errors.expect_meta_name_value(&meta) {
                         this.parse_attr_name(errors, m);
                     }
                 } else if name.is_ident("note") {
-                    if let Some(m) = errors.expect_meta_name_value(meta) {
+                    if let Some(m) = errors.expect_meta_name_value(&meta) {
                         this.parse_attr_note(errors, m);
                     }
                 } else if name.is_ident("subcommand") {
-                    if let Some(ident) = errors.expect_meta_word(meta).and_then(|p| p.get_ident()) {
+                    if let Some(ident) = errors.expect_meta_word(&meta).and_then(|p| p.get_ident())
+                    {
                         this.parse_attr_subcommand(errors, ident);
                     }
                 } else {
@@ -396,20 +368,17 @@ impl TypeAttrs {
     }
 
     fn parse_attr_error_code(&mut self, errors: &Errors, ml: &syn::MetaList) {
-        if ml.nested.len() != 2 {
-            errors.err(&ml, "Expected two arguments, an error number and a string description");
-            return;
-        }
-
-        let err_code = &ml.nested[0];
-        let err_msg = &ml.nested[1];
-
-        let err_code = errors.expect_nested_lit(err_code).and_then(|l| errors.expect_lit_int(l));
-        let err_msg = errors.expect_nested_lit(err_msg).and_then(|l| errors.expect_lit_str(l));
-
-        if let (Some(err_code), Some(err_msg)) = (err_code, err_msg) {
-            self.error_codes.push((err_code.clone(), err_msg.clone()));
-        }
+        errors.ok(ml.parse_args_with(|input: syn::parse::ParseStream| {
+            let err_code = input.parse()?;
+            input.parse::<syn::Token![,]>()?;
+            let err_msg = input.parse()?;
+            if let (Some(err_code), Some(err_msg)) =
+                (errors.expect_lit_int(&err_code), errors.expect_lit_str(&err_msg))
+            {
+                self.error_codes.push((err_code.clone(), err_msg.clone()));
+            }
+            Ok(())
+        }));
     }
 
     fn parse_attr_example(&mut self, errors: &Errors, m: &syn::MetaNameValue) {
@@ -470,15 +439,13 @@ impl VariantAttrs {
                 continue;
             };
 
-            for meta in &ml.nested {
-                let meta = if let Some(m) = errors.expect_nested_meta(meta) { m } else { continue };
-
+            for meta in ml {
                 let name = meta.path();
                 if name.is_ident("dynamic") {
                     if let Some(prev) = this.is_dynamic.as_ref() {
-                        errors.duplicate_attrs("dynamic", prev, meta);
+                        errors.duplicate_attrs("dynamic", prev, &meta);
                     } else {
-                        this.is_dynamic = errors.expect_meta_word(meta).cloned();
+                        this.is_dynamic = errors.expect_meta_word(&meta).cloned();
                     }
                 } else {
                     errors.err(
@@ -515,19 +482,19 @@ fn parse_attr_single_string(
 ) {
     if let Some(first) = slot {
         errors.duplicate_attrs(name, first, m);
-    } else if let Some(lit_str) = errors.expect_lit_str(&m.lit) {
+    } else if let Some(lit_str) = errors.expect_lit_str(&m.value) {
         *slot = Some(lit_str.clone());
     }
 }
 
 fn parse_attr_multi_string(errors: &Errors, m: &syn::MetaNameValue, list: &mut Vec<syn::LitStr>) {
-    if let Some(lit_str) = errors.expect_lit_str(&m.lit) {
+    if let Some(lit_str) = errors.expect_lit_str(&m.value) {
         list.push(lit_str.clone());
     }
 }
 
 fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Description>) {
-    let nv = if let Some(nv) = attr_to_meta_name_value(errors, attr) {
+    let nv = if let Some(nv) = errors.expect_meta_name_value(&attr.meta) {
         nv
     } else {
         return;
@@ -538,7 +505,7 @@ fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Desc
         return;
     }
 
-    if let Some(lit_str) = errors.expect_lit_str(&nv.lit) {
+    if let Some(lit_str) = errors.expect_lit_str(&nv.value) {
         let lit_str = if let Some(previous) = slot {
             let previous = &previous.content;
             let previous_span = previous.span();
@@ -551,7 +518,8 @@ fn parse_attr_doc(errors: &Errors, attr: &syn::Attribute, slot: &mut Option<Desc
 }
 
 fn parse_attr_description(errors: &Errors, m: &syn::MetaNameValue, slot: &mut Option<Description>) {
-    let lit_str = if let Some(lit_str) = errors.expect_lit_str(&m.lit) { lit_str } else { return };
+    let lit_str =
+        if let Some(lit_str) = errors.expect_lit_str(&m.value) { lit_str } else { return };
 
     // Don't allow multiple explicit (non doc-comment) descriptions
     if let Some(description) = slot {
diff --git a/argh_shared/Cargo.toml b/argh_shared/Cargo.toml
index 0f4478a..2ebc3e9 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 = { version = "1", features = ["derive"] }
\ No newline at end of file
diff --git a/argh_shared/src/lib.rs b/argh_shared/src/lib.rs
index c6e7a5c..20383a4 100644
--- a/argh_shared/src/lib.rs
+++ b/argh_shared/src/lib.rs
@@ -14,6 +14,114 @@ pub struct CommandInfo<'a> {
     pub description: &'a str,
 }
 
+/// Information about the command line arguments for a given command.
+#[derive(Debug, Default, PartialEq, Eq, Clone, serde::Serialize)]
+pub struct CommandInfoWithArgs<'a> {
+    /// The name of the command.
+    pub name: &'a str,
+    /// A short description of the command's functionality.
+    pub description: &'a str,
+    /// Examples of usage
+    pub examples: &'a [&'a str],
+    /// Flags
+    pub flags: &'a [FlagInfo<'a>],
+    /// Notes about usage
+    pub notes: &'a [&'a str],
+    /// The subcommands.
+    pub commands: Vec<SubCommandInfo<'a>>,
+    /// Positional args
+    pub positionals: &'a [PositionalInfo<'a>],
+    /// Error code information
+    pub error_codes: &'a [ErrorCodeInfo<'a>],
+}
+
+/// Information about a documented error code.
+#[derive(Debug, PartialEq, Eq, serde::Serialize)]
+pub struct ErrorCodeInfo<'a> {
+    /// The code value.
+    pub code: i32,
+    /// Short description about what this code indicates.
+    pub description: &'a str,
+}
+
+/// Information about positional arguments
+#[derive(Debug, PartialEq, Eq, serde::Serialize)]
+pub struct PositionalInfo<'a> {
+    /// Name of the argument.
+    pub name: &'a str,
+    /// Description of the argument.
+    pub description: &'a str,
+    /// Optionality of the argument.
+    pub optionality: Optionality,
+    /// Visibility in the help for this argument.
+    /// `false` indicates this argument will not appear
+    /// in the help message.
+    pub hidden: bool,
+}
+
+/// Information about a subcommand.
+/// Dynamic subcommands do not implement
+/// get_args_info(), so the command field
+/// only contains the name and description.
+#[derive(Debug, Default, PartialEq, Eq, Clone, serde::Serialize)]
+pub struct SubCommandInfo<'a> {
+    /// The subcommand name.
+    pub name: &'a str,
+    /// The information about the subcommand.
+    pub command: CommandInfoWithArgs<'a>,
+}
+
+/// Information about a flag or option.
+#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
+pub struct FlagInfo<'a> {
+    /// The kind of flag.
+    pub kind: FlagInfoKind<'a>,
+    /// The optionality of the flag.
+    pub optionality: Optionality,
+    /// The long string of the flag.
+    pub long: &'a str,
+    /// The single character short indicator
+    /// for trhis flag.
+    pub short: Option<char>,
+    /// The description of the flag.
+    pub description: &'a str,
+    /// Visibility in the help for this argument.
+    /// `false` indicates this argument will not appear
+    /// in the help message.
+    pub hidden: bool,
+}
+
+/// The kind of flags.
+#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
+pub enum FlagInfoKind<'a> {
+    /// switch represents a boolean flag,
+    #[default]
+    Switch,
+    /// option is a flag that also has an associated
+    /// value. This value is named `arg_name`.
+    Option { arg_name: &'a str },
+}
+
+/// The optionality defines the requirments related
+/// to the presence of the argument on the command line.
+#[derive(Debug, Default, PartialEq, Eq, serde::Serialize)]
+pub enum Optionality {
+    /// Required indicates the argument is required
+    /// exactly once.
+    #[default]
+    Required,
+    /// Optional indicates the argument may or may not
+    /// be present.
+    Optional,
+    /// Repeating indicates the argument may appear zero
+    /// or more times.
+    Repeating,
+    /// Greedy is used for positional arguments which
+    /// capture the all command line input upto the next flag or
+    /// the end of the input.
+    Greedy,
+}
+
 pub const INDENT: &str = "  ";
 const DESCRIPTION_INDENT: usize = 20;
 const WRAP_WIDTH: usize = 80;


Test patch:
diff --git a/argh/tests/args_info_tests.rs b/argh/tests/args_info_tests.rs
new file mode 100644
index 0000000..1fc0730
--- /dev/null
+++ b/argh/tests/args_info_tests.rs
@@ -0,0 +1,1029 @@
+#![cfg(test)]
+// Copyright (c) 2023 Google LLC All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+use argh::{
+    ArgsInfo, CommandInfoWithArgs, ErrorCodeInfo, FlagInfo, FlagInfoKind, FromArgs, Optionality,
+    PositionalInfo, SubCommandInfo,
+};
+
+fn assert_args_info<T: ArgsInfo>(expected: &CommandInfoWithArgs) {
+    let actual_value = T::get_args_info();
+    assert_eq!(expected, &actual_value)
+}
+
+const HELP_FLAG: FlagInfo<'_> = FlagInfo {
+    kind: FlagInfoKind::Switch,
+    optionality: Optionality::Optional,
+    long: "--help",
+    short: None,
+    description: "display usage information",
+    hidden: false,
+};
+
+/// Tests that exercise the JSON output for help text.
+#[test]
+fn args_info_test_subcommand() {
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    /// Top-level command.
+    struct TopLevel {
+        #[argh(subcommand)]
+        nested: MySubCommandEnum,
+    }
+
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    #[argh(subcommand)]
+    enum MySubCommandEnum {
+        One(SubCommandOne),
+        Two(SubCommandTwo),
+    }
+
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    /// First subcommand.
+    #[argh(subcommand, name = "one")]
+    struct SubCommandOne {
+        #[argh(option)]
+        /// how many x
+        x: usize,
+    }
+
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    /// Second subcommand.
+    #[argh(subcommand, name = "two")]
+    struct SubCommandTwo {
+        #[argh(switch)]
+        /// whether to fooey
+        fooey: bool,
+    }
+
+    let command_one = CommandInfoWithArgs {
+        name: "one",
+        description: "First subcommand.",
+        flags: &[
+            HELP_FLAG,
+            FlagInfo {
+                kind: FlagInfoKind::Option { arg_name: "x" },
+                optionality: Optionality::Required,
+                long: "--x",
+                short: None,
+                description: "how many x",
+                hidden: false,
+            },
+        ],
+        ..Default::default()
+    };
+
+    assert_args_info::<TopLevel>(&CommandInfoWithArgs {
+        name: "TopLevel",
+        description: "Top-level command.",
+        examples: &[],
+        flags: &[HELP_FLAG],
+        notes: &[],
+        positionals: &[],
+        error_codes: &[],
+        commands: vec![
+            SubCommandInfo { name: "one", command: command_one.clone() },
+            SubCommandInfo {
+                name: "two",
+                command: CommandInfoWithArgs {
+                    name: "two",
+                    description: "Second subcommand.",
+                    flags: &[
+                        HELP_FLAG,
+                        FlagInfo {
+                            kind: FlagInfoKind::Switch,
+                            optionality: Optionality::Optional,
+                            long: "--fooey",
+                            short: None,
+                            description: "whether to fooey",
+                            hidden: false,
+                        },
+                    ],
+                    ..Default::default()
+                },
+            },
+        ],
+    });
+
+    assert_args_info::<SubCommandOne>(&command_one);
+}
+
+#[test]
+fn args_info_test_multiline_doc_comment() {
+    #[derive(FromArgs, ArgsInfo)]
+    /// Short description
+    struct Cmd {
+        #[argh(switch)]
+        /// a switch with a description
+        /// that is spread across
+        /// a number of
+        /// lines of comments.
+        _s: bool,
+    }
+    assert_args_info::<Cmd>(
+            &CommandInfoWithArgs {
+                name: "Cmd",
+                description: "Short description",
+                flags: &[HELP_FLAG,
+                FlagInfo {
+                    kind: FlagInfoKind::Switch,
+                    optionality: Optionality::Optional,
+                    long: "--s",
+                    short: None,
+                    description: "a switch with a description that is spread across a number of lines of comments.",
+                    hidden:false
+                }
+                ],
+           ..Default::default()
+            });
+}
+
+#[test]
+fn args_info_test_basic_args() {
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    /// Basic command args demonstrating multiple types and cardinality. "With quotes"
+    struct Basic {
+        /// should the power be on. "Quoted value" should work too.
+        #[argh(switch)]
+        power: bool,
+
+        /// option that is required because of no default and not Option<>.
+        #[argh(option, long = "required")]
+        required_flag: String,
+
+        /// optional speed if not specified it is None.
+        #[argh(option, short = 's')]
+        speed: Option<u8>,
+
+        /// repeatable option.
+        #[argh(option, arg_name = "url")]
+        link: Vec<String>,
+    }
+    assert_args_info::<Basic>(&CommandInfoWithArgs {
+        name: "Basic",
+        description:
+            "Basic command args demonstrating multiple types and cardinality. \"With quotes\"",
+        flags: &[
+            FlagInfo {
+                kind: FlagInfoKind::Switch,
+                optionality: Optionality::Optional,
+                long: "--help",
+                short: None,
+                description: "display usage information",
+                hidden: false,
+            },
+            FlagInfo {
+                kind: FlagInfoKind::Switch,
+                optionality: Optionality::Optional,
+                long: "--power",
+                short: None,
+                description: "should the power be on. \"Quoted value\" should work too.",
+                hidden: false,
+            },
+            FlagInfo {
+                kind: FlagInfoKind::Option { arg_name: "required" },
+                optionality: Optionality::Required,
+                long: "--required",
+                short: None,
+                description: "option that is required because of no default and not Option<>.",
+                hidden: false,
+            },
+            FlagInfo {
+                kind: FlagInfoKind::Option { arg_name: "speed" },
+                optionality: Optionality::Optional,
+                long: "--speed",
+                short: Some('s'),
+                description: "optional speed if not specified it is None.",
+                hidden: false,
+            },
+            FlagInfo {
+                kind: FlagInfoKind::Option { arg_name: "url" },
+                optionality: Optionality::Repeating,
+                long: "--link",
+                short: None,
+                description: "repeatable option.",
+                hidden: false,
+            },
+        ],
+        ..Default::default()
+    });
+}
+
+#[test]
+fn args_info_test_positional_args() {
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    /// Command with positional args demonstrating. "With quotes"
+    struct Positional {
+        /// the "root" position.
+        #[argh(positional, arg_name = "root")]
+        root_value: String,
+
+        /// trunk value
+        #[argh(positional)]
+        trunk: String,
+
+        /// leaves. There can be many leaves.
+        #[argh(positional)]
+        leaves: Vec<String>,
+    }
+    assert_args_info::<Positional>(&CommandInfoWithArgs {
+        name: "Positional",
+        description: "Command with positional args demonstrating. \"With quotes\"",
+        flags: &[HELP_FLAG],
+        positionals: &[
+            PositionalInfo {
+                name: "root",
+                description: "the \"root\" position.",
+                optionality: Optionality::Required,
+                hidden: false,
+            },
+            PositionalInfo {
+                name: "trunk",
+                description: "trunk value",
+                optionality: Optionality::Required,
+                hidden: false,
+            },
+            PositionalInfo {
+                name: "leaves",
+                description: "leaves. There can be many leaves.",
+                optionality: Optionality::Repeating,
+                hidden: false,
+            },
+        ],
+
+        ..Default::default()
+    });
+}
+
+#[test]
+fn args_info_test_optional_positional_args() {
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    /// Command with positional args demonstrating last value is optional
+    struct Positional {
+        /// the "root" position.
+        #[argh(positional, arg_name = "root")]
+        root_value: String,
+
+        /// trunk value
+        #[argh(positional)]
+        trunk: String,
+
+        /// leaves. There can be an optional leaves.
+        #[argh(positional)]
+        leaves: Option<String>,
+    }
+    assert_args_info::<Positional>(&CommandInfoWithArgs {
+        name: "Positional",
+        description: "Command with positional args demonstrating last value is optional",
+        flags: &[FlagInfo {
+            kind: FlagInfoKind::Switch,
+            optionality: Optionality::Optional,
+            long: "--help",
+            short: None,
+            description: "display usage information",
+            hidden: false,
+        }],
+        positionals: &[
+            PositionalInfo {
+                name: "root",
+                description: "the \"root\" position.",
+                optionality: Optionality::Required,
+                hidden: false,
+            },
+            PositionalInfo {
+                name: "trunk",
+                description: "trunk value",
+                optionality: Optionality::Required,
+                hidden: false,
+            },
+            PositionalInfo {
+                name: "leaves",
+                description: "leaves. There can be an optional leaves.",
+                optionality: Optionality::Optional,
+                hidden: false,
+            },
+        ],
+
+        ..Default::default()
+    });
+}
+
+#[test]
+fn args_info_test_default_positional_args() {
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    /// Command with positional args demonstrating last value is defaulted.
+    struct Positional {
+        /// the "root" position.
+        #[argh(positional, arg_name = "root")]
+        root_value: String,
+
+        /// trunk value
+        #[argh(positional)]
+        trunk: String,
+
+        /// leaves. There can be one leaf, defaults to hello.
+        #[argh(positional, default = "String::from(\"hello\")")]
+        leaves: String,
+    }
+    assert_args_info::<Positional>(&CommandInfoWithArgs {
+        name: "Positional",
+        description: "Command with positional args demonstrating last value is defaulted.",
+        flags: &[HELP_FLAG],
+        positionals: &[
+            PositionalInfo {
+                name: "root",
+                description: "the \"root\" position.",
+                optionality: Optionality::Required,
+                hidden: false,
+            },
+            PositionalInfo {
+                name: "trunk",
+                description: "trunk value",
+                optionality: Optionality::Required,
+                hidden: false,
+            },
+            PositionalInfo {
+                name: "leaves",
+                description: "leaves. There can be one leaf, defaults to hello.",
+                optionality: Optionality::Optional,
+                hidden: false,
+            },
+        ],
+
+        ..Default::default()
+    });
+}
+
+#[test]
+fn args_info_test_notes_examples_errors() {
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    /// Command with Examples and usage Notes, including error codes.
+    #[argh(
+        note = r##"
+    These usage notes appear for {command_name} and how to best use it.
+    The formatting should be preserved.
+    one
+    two
+    three then a blank
+    
+    and one last line with "quoted text"."##,
+        example = r##"
+    Use the command with 1 file:
+    `{command_name} /path/to/file`
+    Use it with a "wildcard":
+    `{command_name} /path/to/*`
+     a blank line
+    
+    and one last line with "quoted text"."##,
+        error_code(0, "Success"),
+        error_code(1, "General Error"),
+        error_code(2, "Some error with \"quotes\"")
+    )]
+    struct NotesExamplesErrors {
+        /// the "root" position.
+        #[argh(positional, arg_name = "files")]
+        fields: Vec<std::path::PathBuf>,
+    }
+    assert_args_info::<NotesExamplesErrors>(
+            &CommandInfoWithArgs {
+                name: "NotesExamplesErrors",
+                description: "Command with Examples and usage Notes, including error codes.",
+                examples: &["\n    Use the command with 1 file:\n    `{command_name} /path/to/file`\n    Use it with a \"wildcard\":\n    `{command_name} /path/to/*`\n     a blank line\n    \n    and one last line with \"quoted text\"."],
+                flags: &[HELP_FLAG
+                ],
+                positionals: &[
+                    PositionalInfo{
+                        name: "files",
+                        description: "the \"root\" position.",
+                        optionality: Optionality::Repeating,
+                        hidden:false
+                    }
+                ],
+                notes: &["\n    These usage notes appear for {command_name} and how to best use it.\n    The formatting should be preserved.\n    one\n    two\n    three then a blank\n    \n    and one last line with \"quoted text\"."],
+                error_codes: & [ErrorCodeInfo { code: 0, description: "Success" }, ErrorCodeInfo { code: 1, description: "General Error" }, ErrorCodeInfo { code: 2, description: "Some error with \"quotes\"" }],
+                ..Default::default()
+            });
+}
+
+#[test]
+fn args_info_test_subcommands() {
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    ///Top level command with "subcommands".
+    struct TopLevel {
+        /// show verbose output
+        #[argh(switch)]
+        verbose: bool,
+
+        /// this doc comment does not appear anywhere.
+        #[argh(subcommand)]
+        cmd: SubcommandEnum,
+    }
+
+    #[derive(FromArgs, ArgsInfo)]
+    #[argh(subcommand)]
+    /// Doc comments for subcommand enums does not appear in the help text.
+    enum SubcommandEnum {
+        Command1(Command1Args),
+        Command2(Command2Args),
+        Command3(Command3Args),
+    }
+
+    /// Command1 args are used for Command1.
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    #[argh(subcommand, name = "one")]
+    struct Command1Args {
+        /// the "root" position.
+        #[argh(positional, arg_name = "root")]
+        root_value: String,
+
+        /// trunk value
+        #[argh(positional)]
+        trunk: String,
+
+        /// leaves. There can be zero leaves, defaults to hello.
+        #[argh(positional, default = "String::from(\"hello\")")]
+        leaves: String,
+    }
+    /// Command2 args are used for Command2.
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    #[argh(subcommand, name = "two")]
+    struct Command2Args {
+        /// should the power be on. "Quoted value" should work too.
+        #[argh(switch)]
+        power: bool,
+
+        /// option that is required because of no default and not Option<>.
+        #[argh(option, long = "required")]
+        required_flag: String,
+
+        /// optional speed if not specified it is None.
+        #[argh(option, short = 's')]
+        speed: Option<u8>,
+
+        /// repeatable option.
+        #[argh(option, arg_name = "url")]
+        link: Vec<String>,
+    }
+    /// Command3 args are used for Command3 which has no options or arguments.
+    #[derive(FromArgs, ArgsInfo)]
+    #[argh(subcommand, name = "three")]
+    struct Command3Args {}
+
+    assert_args_info::<TopLevel>(&CommandInfoWithArgs {
+        name: "TopLevel",
+        description: "Top level command with \"subcommands\".",
+        flags: &[
+            HELP_FLAG,
+            FlagInfo {
+                kind: FlagInfoKind::Switch,
+                optionality: Optionality::Optional,
+                long: "--verbose",
+                short: None,
+                description: "show verbose output",
+                hidden: false,
+            },
+        ],
+        positionals: &[],
+        commands: vec![
+            SubCommandInfo {
+                name: "one",
+                command: CommandInfoWithArgs {
+                    name: "one",
+                    description: "Command1 args are used for Command1.",
+                    flags: &[HELP_FLAG],
+                    positionals: &[
+                        PositionalInfo {
+                            name: "root",
+                            description: "the \"root\" position.",
+                            optionality: Optionality::Required,
+                            hidden: false,
+                        },
+                        PositionalInfo {
+                            name: "trunk",
+                            description: "trunk value",
+                            optionality: Optionality::Required,
+                            hidden: false,
+                        },
+                        PositionalInfo {
+                            name: "leaves",
+                            description: "leaves. There can be zero leaves, defaults to hello.",
+                            optionality: Optionality::Optional,
+                            hidden: false,
+                        },
+                    ],
+                    ..Default::default()
+                },
+            },
+            SubCommandInfo {
+                name: "two",
+                command: CommandInfoWithArgs {
+                    name: "two",
+                    description: "Command2 args are used for Command2.",
+                    flags: &[
+                        HELP_FLAG,
+                        FlagInfo {
+                            kind: FlagInfoKind::Switch,
+                            optionality: Optionality::Optional,
+                            long: "--power",
+                            short: None,
+                            description:
+                                "should the power be on. \"Quoted value\" should work too.",
+                            hidden: false,
+                        },
+                        FlagInfo {
+                            kind: FlagInfoKind::Option { arg_name: "required" },
+                            optionality: Optionality::Required,
+                            long: "--required",
+                            short: None,
+                            description:
+                                "option that is required because of no default and not Option<>.",
+                            hidden: false,
+                        },
+                        FlagInfo {
+                            kind: FlagInfoKind::Option { arg_name: "speed" },
+                            optionality: Optionality::Optional,
+                            long: "--speed",
+                            short: Some('s'),
+                            description: "optional speed if not specified it is None.",
+                            hidden: false,
+                        },
+                        FlagInfo {
+                            kind: FlagInfoKind::Option { arg_name: "url" },
+                            optionality: Optionality::Repeating,
+                            long: "--link",
+                            short: None,
+                            description: "repeatable option.",
+                            hidden: false,
+                        },
+                    ],
+                    ..Default::default()
+                },
+            },
+            SubCommandInfo {
+                name: "three",
+                command: CommandInfoWithArgs {
+                    name: "three",
+                    description:
+                        "Command3 args are used for Command3 which has no options or arguments.",
+                    flags: &[HELP_FLAG],
+                    positionals: &[],
+                    ..Default::default()
+                },
+            },
+        ],
+        ..Default::default()
+    });
+}
+
+#[test]
+fn args_info_test_subcommand_notes_examples() {
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    ///Top level command with "subcommands".
+    #[argh(
+        note = "Top level note",
+        example = "Top level example",
+        error_code(0, "Top level success")
+    )]
+    struct TopLevel {
+        /// this doc comment does not appear anywhere.
+        #[argh(subcommand)]
+        cmd: SubcommandEnum,
+    }
+
+    #[derive(FromArgs, ArgsInfo)]
+    #[argh(subcommand)]
+    /// Doc comments for subcommand enums does not appear in the help text.
+    enum SubcommandEnum {
+        Command1(Command1Args),
+    }
+
+    /// Command1 args are used for subcommand one.
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    #[argh(
+        subcommand,
+        name = "one",
+        note = "{command_name} is used as a subcommand of \"Top level\"",
+        example = "\"Typical\" usage is `{command_name}`.",
+        error_code(0, "one level success")
+    )]
+    struct Command1Args {
+        /// the "root" position.
+        #[argh(positional, arg_name = "root")]
+        root_value: String,
+
+        /// trunk value
+        #[argh(positional)]
+        trunk: String,
+
+        /// leaves. There can be many leaves.
+        #[argh(positional)]
+        leaves: Vec<String>,
+    }
+
+    let command_one = CommandInfoWithArgs {
+        name: "one",
+        description: "Command1 args are used for subcommand one.",
+        error_codes: &[ErrorCodeInfo { code: 0, description: "one level success" }],
+        examples: &["\"Typical\" usage is `{command_name}`."],
+        flags: &[HELP_FLAG],
+        notes: &["{command_name} is used as a subcommand of \"Top level\""],
+        positionals: &[
+            PositionalInfo {
+                name: "root",
+                description: "the \"root\" position.",
+                optionality: Optionality::Required,
+                hidden: false,
+            },
+            PositionalInfo {
+                name: "trunk",
+                description: "trunk value",
+                optionality: Optionality::Required,
+                hidden: false,
+            },
+            PositionalInfo {
+                name: "leaves",
+                description: "leaves. There can be many leaves.",
+                optionality: Optionality::Repeating,
+                hidden: false,
+            },
+        ],
+        ..Default::default()
+    };
+
+    assert_args_info::<TopLevel>(&CommandInfoWithArgs {
+        name: "TopLevel",
+        description: "Top level command with \"subcommands\".",
+        error_codes: &[ErrorCodeInfo { code: 0, description: "Top level success" }],
+        examples: &["Top level example"],
+        flags: &[HELP_FLAG],
+        notes: &["Top level note"],
+        commands: vec![SubCommandInfo { name: "one", command: command_one.clone() }],
+        ..Default::default()
+    });
+
+    assert_args_info::<Command1Args>(&command_one);
+}
+
+#[test]
+fn args_info_test_example() {
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    #[argh(
+        description = "Destroy the contents of <file> with a specific \"method of destruction\".",
+        example = "Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp",
+        note = "Use `{command_name} help <command>` for details on [<args>] for a subcommand.",
+        error_code(2, "The blade is too dull."),
+        error_code(3, "Out of fuel.")
+    )]
+    struct HelpExample {
+        /// force, ignore minor errors. This description is so long that it wraps to the next line.
+        #[argh(switch, short = 'f')]
+        force: bool,
+
+        /// documentation
+        #[argh(switch)]
+        really_really_really_long_name_for_pat: bool,
+
+        /// write <scribble> repeatedly
+        #[argh(option, short = 's')]
+        scribble: String,
+
+        /// say more. Defaults to $BLAST_VERBOSE.
+        #[argh(switch, short = 'v')]
+        verbose: bool,
+
+        #[argh(subcommand)]
+        command: HelpExampleSubCommands,
+    }
+
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    #[argh(subcommand)]
+    enum HelpExampleSubCommands {
+        BlowUp(BlowUp),
+        Grind(GrindCommand),
+    }
+
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    #[argh(subcommand, name = "blow-up")]
+    /// explosively separate
+    struct BlowUp {
+        /// blow up bombs safely
+        #[argh(switch)]
+        safely: bool,
+    }
+
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    #[argh(subcommand, name = "grind", description = "make smaller by many small cuts")]
+    struct GrindCommand {
+        /// wear a visor while grinding
+        #[argh(switch)]
+        safely: bool,
+    }
+
+    assert_args_info::<HelpExample>(
+            &CommandInfoWithArgs {
+                name: "HelpExample",
+                description: "Destroy the contents of <file> with a specific \"method of destruction\".",
+                examples: &["Scribble 'abc' and then run |grind|.\n$ {command_name} -s 'abc' grind old.txt taxes.cp"],
+                flags: &[HELP_FLAG,
+                FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--force", short: Some('f'), description: "force, ignore minor errors. This description is so long that it wraps to the next line.",
+                hidden:false },
+                FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--really-really-really-long-name-for-pat", short: None, description: "documentation",
+                hidden:false },
+                FlagInfo { kind: FlagInfoKind::Option { arg_name: "scribble"},
+                 optionality: Optionality::Required, long: "--scribble", short: Some('s'), description: "write <scribble> repeatedly",
+                 hidden:false },
+                  FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--verbose", short: Some('v'), description: "say more. Defaults to $BLAST_VERBOSE.",
+                  hidden:false }
+                ],
+                notes: &["Use `{command_name} help <command>` for details on [<args>] for a subcommand."],
+                commands: vec![
+                    SubCommandInfo { name: "blow-up",
+                 command: CommandInfoWithArgs { name: "blow-up",
+                  description: "explosively separate", 
+                  flags:& [HELP_FLAG,
+                   FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--safely", short: None, description: "blow up bombs safely",
+                   hidden:false }
+                   ],
+                ..Default::default()
+             } },
+              SubCommandInfo {
+                 name: "grind",
+                 command: CommandInfoWithArgs {
+                     name: "grind",
+                     description: "make smaller by many small cuts",
+                     flags: &[HELP_FLAG,
+                      FlagInfo { kind: FlagInfoKind::Switch, optionality: Optionality::Optional, long: "--safely", short: None, description: "wear a visor while grinding" ,hidden:false}],
+                      ..Default::default()
+                     }
+                }],
+                error_codes: &[ErrorCodeInfo { code: 2, description: "The blade is too dull." }, ErrorCodeInfo { code: 3, description: "Out of fuel." }],
+                ..Default::default()
+            }
+            );
+}
+
+#[test]
+fn positional_greedy() {
+    #[allow(dead_code)]
+    #[derive(FromArgs, ArgsInfo)]
+    /// Woot
+    struct LastRepeatingGreedy {
+        #[argh(positional)]
+        /// fooey
+        pub a: u32,
+        #[argh(switch)]
+        /// woo
+        pub b: bool,
+        #[argh(option)]
+        /// stuff
+        pub c: Option<String>,
+        #[argh(positional, greedy)]
+        /// fooey
+        pub d: Vec<String>,
+    }
+    assert_args_info::<LastRepeatingGreedy>(&CommandInfoWithArgs {
+        name: "LastRepeatingGreedy",
+        description: "Woot",
+        flags: &[
+            HELP_FLAG,
+            FlagInfo {
+                kind: FlagInfoKind::Switch,
+                optionality: Optionality::Optional,
+                long: "--b",
+                short: None,
+                description: "woo",
+                hidden: false,
+            },
+            FlagInfo {
+                kind: FlagInfoKind::Option { arg_name: "c" },
+                optionality: Optionality::Optional,
+                long: "--c",
+                short: None,
+                description: "stuff",
+                hidden: false,
+            },
+        ],
+        positionals: &[
+            PositionalInfo {
+                name: "a",
+                description: "fooey",
+                optionality: Optionality::Required,
+                hidden: false,
+            },
+            PositionalInfo {
+                name: "d",
+                description: "fooey",
+                optionality: Optionality::Greedy,
+                hidden: false,
+            },
+        ],
+        ..Default::default()
+    });
+}
+
+#[test]
+fn hidden_help_attribute() {
+    #[derive(FromArgs, ArgsInfo)]
+    /// Short description
+    struct Cmd {
+        /// this one should be hidden
+        #[argh(positional, hidden_help)]
+        _one: String,
+        #[argh(positional)]
+        /// this one is real
+        _two: String,
+        /// this one should be hidden
+        #[argh(option, hidden_help)]
+        _three: String,
+    }
+
+    assert_args_info::<Cmd>(&CommandInfoWithArgs {
+        name: "Cmd",
+        description: "Short description",
+        flags: &[
+            HELP_FLAG,
+            FlagInfo {
+                kind: FlagInfoKind::Option { arg_name: "three" },
+                optionality: Optionality::Required,
+                long: "--three",
+                short: None,
+                description: "this one should be hidden",
+                hidden: true,
+            },
+        ],
+        positionals: &[
+            PositionalInfo {
+                name: "one",
+                description: "this one should be hidden",
+                optionality: Optionality::Required,
+                hidden: true,
+            },
+            PositionalInfo {
+                name: "two",
+                description: "this one is real",
+                optionality: Optionality::Required,
+                hidden: false,
+            },
+        ],
+        ..Default::default()
+    });
+}
+
+#[test]
+fn test_dynamic_subcommand() {
+    #[derive(PartialEq, Debug)]
+    struct DynamicSubCommandImpl {
+        got: String,
+    }
+
+    impl argh::DynamicSubCommand for DynamicSubCommandImpl {
+        fn commands() -> &'static [&'static argh::CommandInfo] {
+            &[
+                &argh::CommandInfo { name: "three", description: "Third command" },
+                &argh::CommandInfo { name: "four", description: "Fourth command" },
+                &argh::CommandInfo { name: "five", description: "Fifth command" },
+            ]
+        }
+
+        fn try_redact_arg_values(
+            _command_name: &[&str],
+            _args: &[&str],
+        ) -> Option<Result<Vec<String>, argh::EarlyExit>> {
+            Some(Err(argh::EarlyExit::from("Test should not redact".to_owned())))
+        }
+
+        fn try_from_args(
+            command_name: &[&str],
+            args: &[&str],
+        ) -> Option<Result<DynamicSubCommandImpl, argh::EarlyExit>> {
+            let command_name = match command_name.last() {
+                Some(x) => *x,
+                None => return Some(Err(argh::EarlyExit::from("No command".to_owned()))),
+            };
+            let description = Self::commands().iter().find(|x| x.name == command_name)?.description;
+            if args.len() > 1 {
+                Some(Err(argh::EarlyExit::from("Too many arguments".to_owned())))
+            } else if let Some(arg) = args.first() {
+                Some(Ok(DynamicSubCommandImpl { got: format!("{} got {:?}", description, arg) }))
+            } else {
+                Some(Err(argh::EarlyExit::from("Not enough arguments".to_owned())))
+            }
+        }
+    }
+
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    /// Top-level command.
+    struct TopLevel {
+        #[argh(subcommand)]
+        nested: MySubCommandEnum,
+    }
+
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    #[argh(subcommand)]
+    enum MySubCommandEnum {
+        One(SubCommandOne),
+        Two(SubCommandTwo),
+        #[argh(dynamic)]
+        ThreeFourFive(DynamicSubCommandImpl),
+    }
+
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    /// First subcommand.
+    #[argh(subcommand, name = "one")]
+    struct SubCommandOne {
+        #[argh(option)]
+        /// how many x
+        x: usize,
+    }
+
+    #[derive(FromArgs, ArgsInfo, PartialEq, Debug)]
+    /// Second subcommand.
+    #[argh(subcommand, name = "two")]
+    struct SubCommandTwo {
+        #[argh(switch)]
+        /// whether to fooey
+        fooey: bool,
+    }
+
+    assert_args_info::<TopLevel>(&CommandInfoWithArgs {
+        name: "TopLevel",
+        description: "Top-level command.",
+        flags: &[HELP_FLAG],
+        commands: vec![
+            SubCommandInfo {
+                name: "one",
+                command: CommandInfoWithArgs {
+                    name: "one",
+                    description: "First subcommand.",
+                    flags: &[
+                        HELP_FLAG,
+                        FlagInfo {
+                            kind: FlagInfoKind::Option { arg_name: "x" },
+                            optionality: Optionality::Required,
+                            long: "--x",
+                            short: None,
+                            description: "how many x",
+                            hidden: false,
+                        },
+                    ],
+                    ..Default::default()
+                },
+            },
+            SubCommandInfo {
+                name: "two",
+                command: CommandInfoWithArgs {
+                    name: "two",
+                    description: "Second subcommand.",
+                    flags: &[
+                        HELP_FLAG,
+                        FlagInfo {
+                            kind: FlagInfoKind::Switch,
+                            optionality: Optionality::Optional,
+                            long: "--fooey",
+                            short: None,
+                            description: "whether to fooey",
+                            hidden: false,
+                        },
+                    ],
+                    ..Default::default()
+                },
+            },
+            SubCommandInfo {
+                name: "three",
+                command: CommandInfoWithArgs {
+                    name: "three",
+                    description: "Third command",
+                    ..Default::default()
+                },
+            },
+            SubCommandInfo {
+                name: "four",
+                command: CommandInfoWithArgs {
+                    name: "four",
+                    description: "Fourth command",
+                    ..Default::default()
+                },
+            },
+            SubCommandInfo {
+                name: "five",
+                command: CommandInfoWithArgs {
+                    name: "five",
+                    description: "Fifth command",
+                    ..Default::default()
+                },
+            },
+        ],
+        ..Default::default()
+    })
+}