Repository: pacak/bpaf
Pull request #321 (2023-11-28 13:12:36)

Release 0.9.7
- updated documentation a bit
- support for `#[bpaf(ignore_rustdoc)]`
- support for `descr`, `header`, `footer` and `help` top level annotation for `command` and `options`

Now you can tell `bpaf` to ignore rust doc comments

```rust
#[derive(Debug, Clone, Bpaf)]
/// This gets ignored
#[bpaf(options, ignore_rustdoc)]
struct Options {
        #[bpaf(ignore_rustdoc, help("use this help instead")]
        /// help  // <- this gets ignored
        foo: String
}
```


```rust
/// those are options // <- this gets ignored in favor of explicit descr
#[bpaf(command, descr("descr"), header("header"), footer("footer"), help("help"))]
struct Opt;
```


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 8829f0c..8ff0a10 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bpaf"
-version = "0.9.6"
+version = "0.9.7"
 edition = "2021"
 categories = ["command-line-interface"]
 description = "A simple Command Line Argument Parser with parser combinators"
@@ -19,7 +19,7 @@ include = [
 
 
 [dependencies]
-bpaf_derive = { path = "./bpaf_derive", version = "=0.5.6", optional = true }
+bpaf_derive = { path = "./bpaf_derive", version = "=0.5.7", optional = true }
 owo-colors = { version = "3.5.0", default-features = false, optional = true }
 supports-color = { version = "2.0.0", optional = true }
 
diff --git a/Changelog.md b/Changelog.md
index bb39840..1b11452 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,5 +1,9 @@
 # Change Log
 
+## bpaf [0.9.7], bpaf_derive [0.5.7] - 2023-11-28
+- updated documentation
+- support for `#[bpaf(ignore_rustdoc)]`
+
 ## bpaf [0.9.6], bpaf_derive [0.5.6] - 2023-10-30
 - make sure env-only arguments and flags are working
 - support raw identifiers in derive macro (#282)
diff --git a/bpaf_derive/Cargo.toml b/bpaf_derive/Cargo.toml
index ee3c07e..8b2ce04 100644
--- a/bpaf_derive/Cargo.toml
+++ b/bpaf_derive/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "bpaf_derive"
-version = "0.5.6"
+version = "0.5.7"
 edition = "2018"
 categories = ["command-line-interface"]
 description = "Derive macros for bpaf Command Line Argument Parser"
diff --git a/bpaf_derive/src/attrs.rs b/bpaf_derive/src/attrs.rs
index 9ee23ef..ee4780c 100644
--- a/bpaf_derive/src/attrs.rs
+++ b/bpaf_derive/src/attrs.rs
@@ -373,6 +373,8 @@ pub(crate) struct FieldAttrs {
 
     /// help specified by help(xxx)
     pub help: Vec<CustomHelp>,
+
+    pub(crate) ignore_rustdoc: bool,
 }
 
 impl Name {
@@ -564,7 +566,9 @@ impl Parse for FieldAttrs {
         loop {
             let fork = input.fork();
             let kw = input.parse::<Ident>()?;
-            if let Some(name) = Name::parse(input, &kw)? {
+            if kw == "ignore_rustdoc" {
+                res.ignore_rustdoc = true;
+            } else if let Some(name) = Name::parse(input, &kw)? {
                 res.naming.push(name);
             } else if let Some(cons) = Consumer::parse(input, &kw)? {
                 res.consumer.push(cons);
diff --git a/bpaf_derive/src/help.rs b/bpaf_derive/src/help.rs
index 88ce4e3..437ef0d 100644
--- a/bpaf_derive/src/help.rs
+++ b/bpaf_derive/src/help.rs
@@ -1,6 +1,9 @@
 use proc_macro2::TokenStream;
 use quote::ToTokens;
-use syn::Expr;
+use syn::{
+    parse::{Parse, ParseStream},
+    Expr, Result,
+};
 
 #[derive(Debug)]
 pub(crate) enum Help {
@@ -22,3 +25,9 @@ impl From<&str> for Help {
         Help::Doc(value.to_string())
     }
 }
+
+impl Parse for Help {
+    fn parse(input: ParseStream) -> Result<Self> {
+        Ok(Help::Custom(input.parse()?))
+    }
+}
diff --git a/bpaf_derive/src/named_field.rs b/bpaf_derive/src/named_field.rs
index 01a6e19..f8893b3 100644
--- a/bpaf_derive/src/named_field.rs
+++ b/bpaf_derive/src/named_field.rs
@@ -168,10 +168,14 @@ impl StructField {
 
     #[allow(clippy::too_many_lines)]
     pub(crate) fn make(name: Option<Ident>, ty: Type, attrs: &[Attribute]) -> Result<Self> {
-        let (field_attrs, help) = parse_bpaf_doc_attrs::<FieldAttrs>(attrs)?;
+        let (field_attrs, mut help) = parse_bpaf_doc_attrs::<FieldAttrs>(attrs)?;
 
         let mut field_attrs = field_attrs.unwrap_or_default();
 
+        if field_attrs.ignore_rustdoc {
+            help = None;
+        }
+
         let derived_consumer = field_attrs.consumer.is_empty();
 
         let mut cons = match field_attrs.consumer.pop() {
@@ -252,7 +256,7 @@ impl StructField {
         | Consumer::Positional { ty, .. }
         | Consumer::Any { ty, .. } = &mut cons
         {
-            if matches!(ty, None) {
+            if ty.is_none() {
                 match &shape {
                     Shape::Optional(t) | Shape::Multiple(t) | Shape::Direct(t) => {
                         *ty = Some(t.clone());
diff --git a/bpaf_derive/src/td.rs b/bpaf_derive/src/td.rs
index 8306797..741e4a6 100644
--- a/bpaf_derive/src/td.rs
+++ b/bpaf_derive/src/td.rs
@@ -13,11 +13,41 @@ use syn::{
 //  some flags are valid in different modes but other than that order matters and structure does
 //  not
 
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Default)]
+pub(crate) struct CommandCfg {
+    pub(crate) name: Option<LitStr>,
+    pub(crate) long: Vec<LitStr>,
+    pub(crate) short: Vec<LitChar>,
+    pub(crate) help: Option<Help>,
+}
+
+#[derive(Debug, Default)]
+pub(crate) struct OptionsCfg {
+    pub(crate) cargo_helper: Option<LitStr>,
+    pub(crate) descr: Option<Help>,
+    pub(crate) footer: Option<Help>,
+    pub(crate) header: Option<Help>,
+    pub(crate) usage: Option<Box<Expr>>,
+    pub(crate) version: Option<Box<Expr>>,
+}
+
+#[derive(Debug, Default)]
+pub(crate) struct ParserCfg {
+    pub(crate) group_help: Option<Help>,
+}
+
+#[derive(Debug)]
 pub(crate) enum Mode {
-    Command,
-    Options,
-    Parser,
+    Command {
+        command: CommandCfg,
+        options: OptionsCfg,
+    },
+    Options {
+        options: OptionsCfg,
+    },
+    Parser {
+        parser: ParserCfg,
+    },
 }
 
 #[derive(Debug)]
@@ -55,9 +85,12 @@ pub(crate) struct TopInfo {
     pub(crate) custom_name: Option<Ident>,
     /// add .boxed() at the end
     pub(crate) boxed: bool,
+    /// don't convert rustdoc to group_help, help, etc.
+    pub(crate) ignore_rustdoc: bool,
 
+    pub(crate) adjacent: bool,
     pub(crate) mode: Mode,
-    pub(crate) attrs: Vec<TopAttr>,
+    pub(crate) attrs: Vec<PostDecor>,
 }
 
 impl Default for TopInfo {
@@ -66,8 +99,12 @@ impl Default for TopInfo {
             private: false,
             custom_name: None,
             boxed: false,
-            mode: Mode::Parser,
+            adjacent: false,
+            mode: Mode::Parser {
+                parser: Default::default(),
+            },
             attrs: Vec::new(),
+            ignore_rustdoc: false,
         }
     }
 }
@@ -78,56 +115,47 @@ const TOP_NEED_OPTIONS: &str =
 const TOP_NEED_COMMAND: &str =
     "You need to add `command` annotation at the beginning to use this one";
 
-#[derive(Debug)]
-pub(crate) enum TopAttr {
-    CargoHelper(LitStr),   // <- parsing
-    Version(Box<Expr>),    // <- top only
-    Adjacent,              // generic
-    NamedCommand(LitStr),  // generic
-    UnnamedCommand,        // <- parsing
-    CommandShort(LitChar), //
-    CommandLong(LitStr),   // <- command
-    Usage(Box<Expr>),      // command or top
-    ToOptions,             // options
-    Descr(Help),           // options
-    Header(Help),          // options
-    Footer(Help),          // options
-    PostDecor(PostDecor),
-}
+const TOP_NEED_PARSER: &str = "This annotation can't be used with either `options` or `command`";
 
-impl ToTokens for TopAttr {
-    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
-        match self {
-            Self::ToOptions => quote!(to_options()),
-            Self::CargoHelper(_) | Self::UnnamedCommand => unreachable!(),
-            Self::Version(v) => quote!(version(#v)),
-            Self::Adjacent => quote!(adjacent()),
-            Self::NamedCommand(n) => quote!(command(#n)),
-            Self::CommandShort(n) => quote!(short(#n)),
-            Self::CommandLong(n) => quote!(long(#n)),
-            Self::Usage(u) => quote!(usage(#u)),
-            Self::Descr(d) => quote!(descr(#d)),
-            Self::Header(d) => quote!(header(#d)),
-            Self::Footer(d) => quote!(footer(#d)),
-            Self::PostDecor(pd) => return pd.to_tokens(tokens),
+fn with_options(
+    kw: &Ident,
+    cfg: Option<&mut OptionsCfg>,
+    f: impl FnOnce(&mut OptionsCfg),
+) -> Result<()> {
+    match cfg {
+        Some(cfg) => {
+            f(cfg);
+            Ok(())
         }
-        .to_tokens(tokens);
+        None => Err(Error::new_spanned(kw, TOP_NEED_OPTIONS)),
     }
 }
 
-fn options(kw: &Ident, mode: Mode) -> Result<()> {
-    if matches!(mode, Mode::Options) {
-        Ok(())
-    } else {
-        Err(Error::new_spanned(kw, TOP_NEED_OPTIONS))
+fn with_command(
+    kw: &Ident,
+    cfg: Option<&mut CommandCfg>,
+    f: impl FnOnce(&mut CommandCfg),
+) -> Result<()> {
+    match cfg {
+        Some(cfg) => {
+            f(cfg);
+            Ok(())
+        }
+        None => Err(Error::new_spanned(kw, TOP_NEED_COMMAND)),
     }
 }
 
-fn command(kw: &Ident, mode: Mode) -> Result<()> {
-    if matches!(mode, Mode::Command) {
-        Ok(())
-    } else {
-        Err(Error::new_spanned(kw, TOP_NEED_COMMAND))
+fn with_parser(
+    kw: &Ident,
+    cfg: Option<&mut ParserCfg>,
+    f: impl FnOnce(&mut ParserCfg),
+) -> Result<()> {
+    match cfg {
+        Some(cfg) => {
+            f(cfg);
+            Ok(())
+        }
+        None => Err(Error::new_spanned(kw, TOP_NEED_PARSER)),
     }
 }
 
@@ -136,73 +164,83 @@ impl Parse for TopInfo {
         let mut private = false;
         let mut custom_name = None;
         let mut boxed = false;
-        let mode = {
-            let first = input.fork().parse::<Ident>()?;
-            if first == "options" {
-                Mode::Options
-            } else if first == "command" {
-                Mode::Command
-            } else {
-                Mode::Parser
-            }
-        };
-
+        let mut ignore_rustdoc = false;
+        let mut command = None;
+        let mut options = None;
+        let mut parser = Some(ParserCfg::default());
+        let mut adjacent = false;
         let mut attrs = Vec::new();
-
+        let mut first = true;
         loop {
             let kw = input.parse::<Ident>()?;
-            if kw == "private" {
+
+            if first && kw == "options" {
+                let mut cfg = OptionsCfg::default();
+                if let Some(helper) = parse_opt_arg(input)? {
+                    cfg.cargo_helper = Some(helper);
+                }
+                options = Some(cfg);
+                parser = None;
+            } else if first && kw == "command" {
+                let mut cfg = CommandCfg::default();
+                if let Some(name) = parse_opt_arg(input)? {
+                    cfg.name = Some(name);
+                }
+                options = Some(OptionsCfg::default());
+                command = Some(cfg);
+                parser = None;
+            } else if kw == "private" {
                 private = true;
             } else if kw == "generate" {
                 custom_name = parse_arg(input)?;
             } else if kw == "options" {
-                if !matches!(mode, Mode::Options) {
-                    return Err(Error::new_spanned(
-                        kw,
-                        "This annotation must be first: try `#[bpaf(options, ...`",
-                    ));
-                }
-                if let Some(helper) = parse_opt_arg(input)? {
-                    attrs.push(TopAttr::CargoHelper(helper));
-                }
-                attrs.push(TopAttr::ToOptions);
+                return Err(Error::new_spanned(
+                    kw,
+                    "This annotation must be first and used only once: try `#[bpaf(options, ...`",
+                ));
             } else if kw == "command" {
-                if !matches!(mode, Mode::Command) {
-                    return Err(Error::new_spanned(
-                        kw,
-                        "This annotation must be first: try `#[bpaf(command, ...`",
-                    ));
-                }
-                attrs.push(if let Some(name) = parse_opt_arg(input)? {
-                    TopAttr::NamedCommand(name)
-                } else {
-                    TopAttr::UnnamedCommand
-                });
-            } else if kw == "version" {
-                options(&kw, mode)?;
-                attrs.push(TopAttr::Version(
-                    parse_opt_arg(input)?
-                        .unwrap_or_else(|| parse_quote!(env!("CARGO_PKG_VERSION"))),
+                return Err(Error::new_spanned(
+                    kw,
+                    "This annotation must be first: try `#[bpaf(command, ...`",
                 ));
+            } else if kw == "version" {
+                let version = parse_opt_arg(input)?
+                    .unwrap_or_else(|| parse_quote!(env!("CARGO_PKG_VERSION")));
+                with_options(&kw, options.as_mut(), |cfg| cfg.version = Some(version))?;
             } else if kw == "boxed" {
                 boxed = true;
             } else if kw == "adjacent" {
-                attrs.push(TopAttr::Adjacent);
+                adjacent = true;
             } else if kw == "short" {
-                command(&kw, mode)?;
-                attrs.push(TopAttr::CommandShort(parse_arg(input)?));
+                let short = parse_arg(input)?;
+                with_command(&kw, command.as_mut(), |cfg| cfg.short.push(short))?;
             } else if kw == "long" {
-                command(&kw, mode)?;
-                attrs.push(TopAttr::CommandLong(parse_arg(input)?));
+                let long = parse_arg(input)?;
+                with_command(&kw, command.as_mut(), |cfg| cfg.long.push(long))?;
             } else if kw == "header" {
-                attrs.push(TopAttr::Header(Help::Custom(parse_arg(input)?)));
+                let header = parse_arg(input)?;
+                with_options(&kw, options.as_mut(), |cfg| cfg.header = Some(header))?;
             } else if kw == "footer" {
-                attrs.push(TopAttr::Footer(Help::Custom(parse_arg(input)?)));
+                let footer = parse_arg(input)?;
+                with_options(&kw, options.as_mut(), |opt| opt.footer = Some(footer))?;
             } else if kw == "usage" {
-                options(&kw, mode).or_else(|_| command(&kw, mode))?;
-                attrs.push(TopAttr::Usage(parse_arg(input)?));
+                let usage = parse_arg(input)?;
+                with_options(&kw, options.as_mut(), |opt| opt.usage = Some(usage))?;
+            } else if kw == "group_help" {
+                let group_help = parse_arg(input)?;
+                with_parser(&kw, parser.as_mut(), |opt| {
+                    opt.group_help = Some(group_help)
+                })?;
+            } else if kw == "ignore_rustdoc" {
+                ignore_rustdoc = true;
+            } else if kw == "descr" {
+                let descr = parse_arg(input)?;
+                with_options(&kw, options.as_mut(), |opt| opt.descr = Some(descr))?;
+            } else if kw == "help" {
+                let help = parse_arg(input)?;
+                with_command(&kw, command.as_mut(), |cfg| cfg.help = Some(help))?;
             } else if let Some(pd) = PostDecor::parse(input, &kw)? {
-                attrs.push(TopAttr::PostDecor(pd));
+                attrs.push(pd);
             } else {
                 return Err(Error::new_spanned(
                     kw,
@@ -217,12 +255,23 @@ impl Parse for TopInfo {
             if input.is_empty() {
                 break;
             }
+            first = false;
         }
 
+        let mode = match (options, command) {
+            (Some(options), Some(command)) => Mode::Command { command, options },
+            (Some(options), None) => Mode::Options { options },
+            _ => Mode::Parser {
+                parser: parser.unwrap_or_default(),
+            },
+        };
+
         Ok(TopInfo {
+            ignore_rustdoc,
             private,
             custom_name,
             boxed,
+            adjacent,
             mode,
             attrs,
         })
@@ -284,9 +333,9 @@ impl Parse for Ed {
             } else if kw == "usage" {
                 attrs.push(EAttr::Usage(parse_arg(input)?));
             } else if kw == "header" {
-                attrs.push(EAttr::Header(Help::Custom(parse_arg(input)?)));
+                attrs.push(EAttr::Header(parse_arg(input)?));
             } else if kw == "footer" {
-                attrs.push(EAttr::Footer(Help::Custom(parse_arg(input)?)));
+                attrs.push(EAttr::Footer(parse_arg(input)?));
             } else if kw == "env" {
                 attrs.push(EAttr::Env(parse_arg(input)?));
             } else {
diff --git a/bpaf_derive/src/top.rs b/bpaf_derive/src/top.rs
index c09bc5a..a8f3f92 100644
--- a/bpaf_derive/src/top.rs
+++ b/bpaf_derive/src/top.rs
@@ -12,7 +12,7 @@ use crate::{
     attrs::{parse_bpaf_doc_attrs, EnumPrefix, PostDecor, StrictName},
     field::StructField,
     help::Help,
-    td::{EAttr, Ed, Mode, TopAttr, TopInfo},
+    td::{CommandCfg, EAttr, Ed, Mode, OptionsCfg, ParserCfg, TopInfo},
     utils::{to_kebab_case, to_snake_case, LineIter},
 };
 
@@ -34,8 +34,8 @@ pub(crate) struct Top {
     body: Body,
     mode: Mode,
     boxed: bool,
-    cargo_helper: Option<LitStr>,
-    attrs: Vec<TopAttr>,
+    adjacent: bool,
+    attrs: Vec<PostDecor>,
 }
 
 fn ident_to_long(ident: &Ident) -> LitStr {
@@ -57,79 +57,48 @@ impl Parse for Top {
             private,
             custom_name,
             boxed,
-            mode,
-            attrs: td_attrs,
+            mut mode,
+            attrs,
+            ignore_rustdoc,
+            adjacent,
         } = top_decor.unwrap_or_default();
 
+        if ignore_rustdoc {
+            help = None;
+        }
         let vis = input.parse::<Visibility>()?;
 
         let mut body = Body::parse(input)?;
         let ty = body.ty();
-        let mut cargo_helper = None;
-        let mut attrs = Vec::with_capacity(td_attrs.len());
-        let mut options_ix = None;
-        for attr in td_attrs {
-            match attr {
-                TopAttr::CargoHelper(h) => {
-                    cargo_helper = Some(h);
-                }
-                TopAttr::ToOptions => {
-                    options_ix = Some(attrs.len());
-                    attrs.push(attr);
-
-                    if let Some(h) = std::mem::take(&mut help) {
-                        split_help_into(h, &mut attrs);
-                    }
-                }
-                TopAttr::Usage(_)
-                | TopAttr::Version(_)
-                | TopAttr::Header(_)
-                | TopAttr::Footer(_)
-                | TopAttr::Adjacent
-                | TopAttr::CommandShort(_)
-                | TopAttr::CommandLong(_) => {
-                    attrs.push(attr);
-                }
-                TopAttr::NamedCommand(ref n) => {
-                    body.set_named_command(n.span())?;
-                    attrs.push(TopAttr::ToOptions);
 
-                    if let Some(h) = std::mem::take(&mut help) {
-                        split_help_into(h, &mut attrs);
-                    }
+        if let Mode::Command { command, .. } = &mut mode {
+            if let Some(name) = &command.name {
+                body.set_named_command(name.span())?;
+            } else {
+                body.set_unnamed_command()?;
+                command.name = Some(ident_to_long(&ty));
+            }
+        }
 
-                    attrs.push(attr);
+        if let Some(help) = help.take() {
+            match &mut mode {
+                Mode::Command {
+                    command: _,
+                    options,
+                } => {
+                    split_options_help(help, options);
                 }
-                TopAttr::UnnamedCommand => {
-                    body.set_unnamed_command();
-                    attrs.push(TopAttr::ToOptions);
-
-                    if let Some(h) = std::mem::take(&mut help) {
-                        attrs.push(TopAttr::Descr(h));
-                    }
-                    attrs.push(TopAttr::NamedCommand(ident_to_long(&ty)));
+                Mode::Options { options } => {
+                    split_options_help(help, options);
                 }
-
-                TopAttr::Descr(_) => unreachable!(),
-                TopAttr::PostDecor(_) => {
-                    if let Some(ix) = options_ix {
-                        attrs.insert(ix, attr);
-                    } else {
-                        attrs.push(attr);
+                Mode::Parser { parser } => {
+                    if parser.group_help.is_none() {
+                        parser.group_help = Some(help);
                     }
                 }
             }
         }
 
-        if let Some(h) = std::mem::take(&mut help) {
-            let doc = match h {
-                Help::Custom(c) => c,
-                Help::Doc(d) => parse_quote!(#d),
-            };
-            let span = Span::call_site();
-            attrs.push(TopAttr::PostDecor(PostDecor::GroupHelp { doc, span }));
-        }
-
         Ok(Top {
             vis: if private { Visibility::Inherited } else { vis },
             mode,
@@ -140,26 +109,34 @@ impl Parse for Top {
             attrs,
             body,
             boxed,
-            cargo_helper,
+            adjacent,
         })
     }
 }
 
-fn split_help_into(h: Help, attrs: &mut Vec<TopAttr>) {
+fn split_options_help(h: Help, opts: &mut OptionsCfg) {
     match &h {
-        Help::Custom(_) => attrs.push(TopAttr::Descr(h)),
+        Help::Custom(_) => {
+            if opts.descr.is_none() {
+                opts.descr = Some(h);
+            }
+        }
         Help::Doc(c) => {
             let mut chunks = LineIter::from(c.as_str());
             if let Some(s) = chunks.next() {
-                attrs.push(TopAttr::Descr(Help::Doc(s)));
+                if opts.descr.is_none() {
+                    opts.descr = Some(Help::Doc(s));
+                }
             }
             if let Some(s) = chunks.next() {
-                if !s.is_empty() {
-                    attrs.push(TopAttr::Header(Help::Doc(s)));
+                if !s.is_empty() && opts.header.is_none() {
+                    opts.header = Some(Help::Doc(s));
                 }
             }
             if let Some(s) = chunks.rest() {
-                attrs.push(TopAttr::Footer(Help::Doc(s)));
+                if opts.footer.is_none() {
+                    opts.footer = Some(Help::Doc(s));
+                }
             }
         }
     }
@@ -195,37 +172,115 @@ impl ToTokens for Top {
             mode,
             attrs,
             boxed,
-            cargo_helper,
+            adjacent,
         } = self;
+        let boxed = if *boxed { quote!(.boxed()) } else { quote!() };
+        let adjacent = if *adjacent {
+            quote!(.adjacent())
+        } else {
+            quote!()
+        };
 
-        if matches!(mode, Mode::Options) {
-            let body = match cargo_helper {
-                Some(cargo) => quote!(::bpaf::cargo_helper(#cargo, #body)),
-                None => quote!(#body),
-            };
-
-            quote! {
-                #vis fn #generate() -> ::bpaf::OptionParser<#ty> {
-                    #[allow(unused_imports)]
-                    use ::bpaf::Parser;
-                    #body
-                    #(.#attrs)*
+        match mode {
+            Mode::Command { command, options } => {
+                let OptionsCfg {
+                    cargo_helper: _,
+                    usage,
+                    version,
+                    descr,
+                    footer,
+                    header,
+                } = options;
+
+                let version = version.as_ref().map(|v| quote!(.version(#v)));
+                let usage = usage.as_ref().map(|v| quote!(.usage(#v)));
+                let descr = descr.as_ref().map(|v| quote!(.descr(#v)));
+                let footer = footer.as_ref().map(|v| quote!(.footer(#v)));
+                let header = header.as_ref().map(|v| quote!(.header(#v)));
+
+                let CommandCfg {
+                    name,
+                    long,
+                    short,
+                    help,
+                } = command;
+                let name = name.as_ref().expect("Internal bpaf_derive error: Command name was not set! This is a bug, please report it.");
+                let long = long.iter().map(|v| quote!(.long(#v)));
+                let short = short.iter().map(|v| quote!(.short(#v)));
+                let help = help.as_ref().map(|v| quote!(.help(#v)));
+                quote! {
+                    #vis fn #generate() -> impl ::bpaf::Parser<#ty> {
+
+                        #[allow(unused_imports)]
+                        use ::bpaf::Parser;
+                        #body
+                        #(.#attrs)*
+                        .to_options()
+                        #version
+                        #descr
+                        #header
+                        #footer
+                        #usage
+                        .command(#name)
+                        #(#short)*
+                        #(#long)*
+                        #help
+                        #adjacent
+                        #boxed
+                    }
                 }
             }
-        } else {
-            let boxed = if *boxed { quote!(.boxed()) } else { quote!() };
-            quote! {
-                #vis fn #generate() -> impl ::bpaf::Parser<#ty> {
-                    #[allow(unused_imports)]
-                    use ::bpaf::Parser;
-
-                    #body
-                    #(.#attrs)*
-                    #boxed
+            Mode::Options { options } => {
+                let OptionsCfg {
+                    cargo_helper,
+                    usage,
+                    version,
+                    descr,
+                    footer,
+                    header,
+                } = options;
+                let body = match cargo_helper {
+                    Some(cargo) => quote!(::bpaf::cargo_helper(#cargo, #body)),
+                    None => quote!(#body),
+                };
+
+                let version = version.as_ref().map(|v| quote!(.version(#v)));
+                let usage = usage.as_ref().map(|v| quote!(.usage(#v)));
+                let descr = descr.as_ref().map(|v| quote!(.descr(#v)));
+                let footer = footer.as_ref().map(|v| quote!(.footer(#v)));
+                let header = header.as_ref().map(|v| quote!(.header(#v)));
+                quote! {
+                    #vis fn #generate() -> ::bpaf::OptionParser<#ty> {
+                        #[allow(unused_imports)]
+                        use ::bpaf::Parser;
+                        #body
+                        #(.#attrs)*
+                        .to_options()
+                        #version
+                        #descr
+                        #header
+                        #footer
+                        #usage
+                    }
+                }
+            }
+            Mode::Parser { parser } => {
+                let ParserCfg { group_help } = &parser;
+                let group_help = group_help.as_ref().map(|v| quote!(.group_help(#v)));
+                quote! {
+                    #vis fn #generate() -> impl ::bpaf::Parser<#ty> {
+                        #[allow(unused_imports)]
+                        use ::bpaf::Parser;
+                        #body
+                        #group_help
+                        #adjacent
+                        #(.#attrs)*
+                        #boxed
+                    }
                 }
             }
         }
-        .to_tokens(tokens);
+        .to_tokens(tokens)
     }
 }
 
@@ -260,16 +315,6 @@ impl Parse for Body {
                 .into_iter()
                 .filter_map(|p| p.resolve(&name).transpose())
                 .collect::<Result<Vec<_>>>()?;
-
-            //            let mut branches
-            /*
-                        for b in branches.iter_mut() {
-                            b.postprocess_with_name(&name);
-                        }
-
-                        let branches = branches.into_iter().filter(|b| !b.skip).collect::<Vec<_>>();
-            */
-
             Ok(Self::Alternatives(name, branches))
         } else {
             Err(input.error("Only structs and enums are supported"))
@@ -301,15 +346,27 @@ impl Body {
         }
     }
 
-    fn set_unnamed_command(&mut self) {
+    fn set_unnamed_command(&mut self) -> Result<()> {
         match self {
-            Body::Single(b) => b.set_unnamed_command(),
-            Body::Alternatives(_, _branches) => {
-                //                for branch in branches {
-                todo!();
-                //                    branch.set_unnamed_command()?;
-                //                }
-                //Ok(())
+            Body::Single(b) => {
+                b.set_unnamed_command();
+                Ok(())
+            }
+            Body::Alternatives(_name, _branches) => {
+                /*
+                for branch in branches {
+                    if !branch
+                        .attrs
+                        .iter()
+                        .any(|attr| matches!(attr, EAttr::ToOptions))
+                    {
+                        let name = ident_to_long(&branch.branch.ident);
+                        branch.attrs.insert(0, EAttr::NamedCommand(name));
+                        branch.attrs.insert(0, EAttr::ToOptions);
+                        branch.branch.set_unnamed_command();
+                    }
+                }*/
+                Ok(())
             }
         }
     }


Test patch:
diff --git a/bpaf_derive/src/field_tests.rs b/bpaf_derive/src/field_tests.rs
index 1184532..45a37f5 100644
--- a/bpaf_derive/src/field_tests.rs
+++ b/bpaf_derive/src/field_tests.rs
@@ -699,6 +699,19 @@ fn any_field_1() {
     assert_eq!(input.to_token_stream().to_string(), output.to_string());
 }
 
+#[test]
+fn unnamed_field_with_ignore_rustdoc() {
+    let input: UnnamedField = parse_quote! {
+        #[bpaf(any("FOO", Some), ignore_rustdoc)]
+        /// help
+        String
+    };
+    let output = quote! {
+        ::bpaf::any::<String, _, _>("FOO", Some)
+    };
+    assert_eq!(input.to_token_stream().to_string(), output.to_string());
+}
+
 #[test]
 fn any_field_2() {
     let input: UnnamedField = parse_quote! {
@@ -802,6 +815,32 @@ fn unit_fields_are_required() {
     assert_eq!(input.to_token_stream().to_string(), output.to_string());
 }
 
+#[test]
+fn ignore_rustdoc_without_help() {
+    let input: NamedField = parse_quote! {
+        /// help
+        #[bpaf(ignore_rustdoc)]
+        name: ()
+    };
+    let output = quote! {
+        ::bpaf::long("name").req_flag(())
+    };
+    assert_eq!(input.to_token_stream().to_string(), output.to_string());
+}
+
+#[test]
+fn ignore_rustdoc_with_help() {
+    let input: NamedField = parse_quote! {
+        /// help
+        #[bpaf(help("custom help"), ignore_rustdoc)]
+        name: ()
+    };
+    let output = quote! {
+        ::bpaf::long("name").help("custom help").req_flag(())
+    };
+    assert_eq!(input.to_token_stream().to_string(), output.to_string());
+}
+
 #[test]
 fn unit_fields_are_required_custom_help() {
     let input: NamedField = parse_quote! {
diff --git a/bpaf_derive/src/top_tests.rs b/bpaf_derive/src/top_tests.rs
index 02e6691..7d4c159 100644
--- a/bpaf_derive/src/top_tests.rs
+++ b/bpaf_derive/src/top_tests.rs
@@ -89,8 +89,6 @@ fn top_struct_options1() {
                 }
                 .to_options()
                 .descr("those are options")
-                .header("header")
-                .footer("footer")
                 .header(h)
                 .footer(f)
         }
@@ -145,22 +143,19 @@ fn struct_options2() {
 }
 
 #[test]
-fn struct_command() {
+fn struct_command_no_decor() {
     let input: Top = parse_quote! {
         /// those are options
         #[bpaf(command)]
-        struct Opt { foo: bool, }
+        struct Opt;
     };
 
     let expected = quote! {
         fn opt() -> impl ::bpaf::Parser<Opt> {
             #[allow (unused_imports)]
             use ::bpaf::Parser;
-            {
-                let foo = ::bpaf::long("foo").switch();
-                ::bpaf::construct!(Opt {foo, })
-            }
-            .to_options()
+            ::bpaf::pure(Opt)
+                .to_options()
                 .descr("those are options")
                 .command("opt")
         }
@@ -168,6 +163,30 @@ fn struct_command() {
     assert_eq!(input.to_token_stream().to_string(), expected.to_string());
 }
 
+#[test]
+fn struct_command_decor() {
+    let input: Top = parse_quote! {
+        /// those are options
+        #[bpaf(command, descr(descr), header(header), footer(footer), help(help))]
+        struct Opt;
+    };
+
+    let expected = quote! {
+        fn opt() -> impl ::bpaf::Parser<Opt> {
+            #[allow (unused_imports)]
+            use ::bpaf::Parser;
+            ::bpaf::pure(Opt)
+                .to_options()
+                .descr(descr)
+                .header(header)
+                .footer(footer)
+                .command("opt")
+                .help(help)
+        }
+    };
+    assert_eq!(input.to_token_stream().to_string(), expected.to_string());
+}
+
 #[test]
 fn struct_command_short() {
     let input: Top = parse_quote! {
@@ -197,12 +216,42 @@ fn struct_command_short() {
 #[should_panic(expected = "Can't construct a parser from empty enum")]
 #[test]
 fn empty_enum() {
-    let _: Top = parse_quote! {
+    let x: Top = parse_quote! {
         enum Opt { }
     };
+    todo!("{:?}", x);
 }
 */
 
+#[test]
+fn unnamed_command_enum() {
+    let input: Top = parse_quote! {
+        #[bpaf(command)]
+        enum Opts {
+            #[bpaf(command("alpha1"))]
+            Alpha,
+            Beta,
+            Gamma,
+        }
+    };
+
+    let expected = quote! {
+        fn opts() -> impl ::bpaf::Parser<Opts> {
+            #[allow(unused_imports)]
+            use ::bpaf::Parser;
+            {
+                let alt0 = ::bpaf::pure(Opts::Alpha).to_options().command("alpha1");
+                let alt1 = ::bpaf::long("beta").req_flag(Opts::Beta);
+                let alt2 = ::bpaf::long("gamma").req_flag(Opts::Gamma);
+                ::bpaf::construct!([alt0, alt1, alt2,])
+            }
+            .to_options()
+            .command("opts")
+        }
+    };
+    assert_eq!(input.to_token_stream().to_string(), expected.to_string());
+}
+
 #[test]
 fn enum_markdownish() {
     let input: Top = parse_quote! {
@@ -1014,6 +1063,63 @@ fn single_unit_adjacent_command() {
     assert_eq!(top.to_token_stream().to_string(), expected.to_string());
 }
 
+#[test]
+fn ingore_doc_comment_top_level_1() {
+    let top: Top = parse_quote! {
+        #[derive(Debug, Clone, Bpaf)]
+        /// present
+        #[bpaf(ignore_rustdoc)]
+        enum Mode {
+            /// intel help
+            Intel,
+            /// att help
+            Att,
+        }
+    };
+
+    let expected = quote! {
+        fn mode() -> impl ::bpaf::Parser<Mode> {
+            #[allow(unused_imports)]
+            use ::bpaf::Parser;
+            {
+                let alt0 = ::bpaf::long("intel").help("intel help").req_flag(Mode::Intel);
+                let alt1 = ::bpaf::long("att").help("att help").req_flag(Mode::Att);
+                ::bpaf::construct!([alt0, alt1, ])
+            }
+        }
+    };
+    assert_eq!(top.to_token_stream().to_string(), expected.to_string());
+}
+
+#[test]
+fn ingore_doc_comment_top_level_2() {
+    let top: Top = parse_quote! {
+        #[derive(Debug, Clone, Bpaf)]
+        #[bpaf(options, ignore_rustdoc)]
+        /// present
+        enum Mode {
+            /// intel help
+            Intel,
+            /// att help
+            Att,
+        }
+    };
+
+    let expected = quote! {
+        fn mode() -> ::bpaf::OptionParser<Mode> {
+            #[allow(unused_imports)]
+            use ::bpaf::Parser;
+            {
+                let alt0 = ::bpaf::long("intel").help("intel help").req_flag(Mode::Intel);
+                let alt1 = ::bpaf::long("att").help("att help").req_flag(Mode::Att);
+                ::bpaf::construct!([alt0, alt1,])
+            }
+            .to_options()
+        }
+    };
+    assert_eq!(top.to_token_stream().to_string(), expected.to_string());
+}
+
 #[test]
 fn top_comment_is_group_help_enum() {
     let top: Top = parse_quote! {