Repository: google/argh
Pull request #167 (2023-07-28 17:56:16)

Adding ArgsInfo derive trait
The ArgsInfo trait exposes the information about the arguments defined using the argh attributes. This is intended to be used for uses that benefit from inspecting the command line arguments exposed via a structured interface for example, help documentation generation and CLI compatibility detection.

sadmac7000: Looks like rustfmt fails. Mind running that then resubmitting?
claywilkinson: I reran `cargo fmt`

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/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/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_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()
+    })
+}