{"id":"081fe0fd7b3956c0","repo":"clap-rs/clap","slug":"cannot-flatten-an-option-args-with-grou","errorCode":null,"errorMessage":"cannot `#[flatten]` an `Option<Args>` with `#[group(skip)]`","messagePattern":"cannot `#\\[flatten\\]` an `Option<Args>` with `#\\[group\\(skip\\)\\]`","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"clap_derive/src/derives/args.rs","lineNumber":233,"sourceCode":"\n                Some(quote! {\n                    let #app_var = <#subcmd_type as clap::Subcommand>::augment_subcommands( #app_var );\n                    let #app_var = #app_var\n                        #implicit_methods\n                        #override_methods;\n                })\n            }\n            Kind::Flatten(ty) => {\n                let inner_type = match (**ty, sub_type(&field.ty)) {\n                    (Ty::Option, Some(sub_type)) => sub_type,\n                    _ => &field.ty,\n                };\n\n                let next_help_heading = item.next_help_heading();\n                let next_display_order = item.next_display_order();\n                let flatten_group_assert = if matches!(**ty, Ty::Option) {\n                    quote_spanned! { kind.span()=>\n                        <#inner_type as clap::Args>::group_id().expect(\"cannot `#[flatten]` an `Option<Args>` with `#[group(skip)]`\");\n                    }\n                } else {\n                    quote! {}\n                };\n                if override_required {\n                    Some(quote_spanned! { kind.span()=>\n                        #flatten_group_assert\n                        let #app_var = #app_var\n                            #next_help_heading\n                            #next_display_order;\n                        let #app_var = <#inner_type as clap::Args>::augment_args_for_update(#app_var);\n                    })\n                } else {\n                    Some(quote_spanned! { kind.span()=>\n                        #flatten_group_assert\n                        let #app_var = #app_var\n                            #next_help_heading\n                            #next_display_order;","sourceCodeStart":215,"sourceCodeEnd":251,"githubUrl":"https://github.com/clap-rs/clap/blob/ccc92c3b1171c00a6ef610bfa0018fa59c8510f9/clap_derive/src/derives/args.rs#L215-L251","documentation":"This is a runtime `.expect()` panic emitted into the generated derive code at `clap_derive/src/derives/args.rs:233`. For a field annotated `#[clap(flatten)]` whose type is `Option<T>` (where `T: clap::Args`), the derive emits `<T as clap::Args>::group_id().expect(...)`. `group_id()` returns `None` when the inner `Args` struct carries `#[group(skip)]`, so the `.expect()` panics. Clap throws it because flattening an *optional* group requires a real group id to attach the \"present-or-absent\" semantics; with the group skipped there is no id to bind optionality to, so the combination is unsupported.","triggerScenarios":"Define `#[derive(clap::Parser)] struct Cli { #[clap(flatten)] args: Option<InnerArgs> }` where `InnerArgs` is `#[derive(clap::Args)]` and is annotated with `#[group(skip)]` (see the regression test `flatten_skipped_group` in `tests/derive/flatten.rs:288-305`). The panic occurs at parse time when the derived `augment_args`/`augment_args_for_update` runs (e.g. `Cli::parse()`, `Cli::try_parse_from(...)`). Wrapping in `Option<Box<InnerArgs>>` triggers the same path via `sub_type`.","commonSituations":"Splitting a large CLI into sub-structs that are reused, marking one `#[group(skip)]` to avoid an extra arg group in help, then trying to make it optional via `Option<...>` and flattening it into the parent. Refactors that promote a flattened `Args` to `Option<Args>` for conditional configuration, or copying a `#[group(skip)]` struct that worked elsewhere into an `Option`-wrapped flatten, both hit this. The message was improved in the 4.x series (see CHANGELOG) but the panic remains.","solutions":["Remove `#[group(skip)]` from the inner `Args` struct so it exposes a group id, keeping `#[clap(flatten)] args: Option<InnerArgs>` working.","Drop the `Option<...>` wrapper and flatten the inner `Args` directly (e.g. `#[clap(flatten)] args: InnerArgs`); make individual fields optional instead of the whole group.","If the whole group must be conditionally absent and skipped, stop using `#[flatten]` and model it as a `#[command(subcommand)]` or a manual `Option<InnerArgs>` field parsed by value, so clap does not need the inner group id."],"exampleFix":"// before\n#[derive(clap::Parser)]\nstruct Cli {\n    #[clap(flatten)]\n    args: Option<Args>,\n}\n\n#[derive(clap::Args)]\n#[group(skip)]\nstruct Args {\n    #[clap(short)]\n    param: bool,\n}\n\n// after — give the inner Args a group id by removing the skip\n#[derive(clap::Parser)]\nstruct Cli {\n    #[clap(flatten)]\n    args: Option<Args>,\n}\n\n#[derive(clap::Args)]\nstruct Args {\n    #[clap(short)]\n    param: bool,\n}","handlingStrategy":"validation","validationCode":"// Pre-flight check (run in a #[test] or at startup). For a #[command(flatten)] field of\n// type Option<T>, clap_derive emits the runtime call\n//   <T as clap::Args>::group_id().expect(\"cannot `#[flatten]` an `Option<Args>` with `#[group(skip)]`\")\n// which panics while building the Command (during parse/try_parse) when the inner Args has\n// #[group(skip)] (group_id() == None). Verify the precondition explicitly:\nfn assert_flattenable_optional_args<T: clap::Args>(name: &str) {\n    assert!(\n        <T as clap::Args>::group_id().is_some(),\n        \"{} has #[group(skip)] and cannot be used as Option<{}> with #[command(flatten)]\",\n        name,\n        name\n    );\n}\n\n#[test]\nfn flatten_targets_expose_a_group() {\n    // list every inner Args type you flatten behind Option<T>:\n    assert_flattenable_optional_args::<MyInnerArgs>(\"MyInnerArgs\");\n}","typeGuard":"// True => T: Args is safe to flatten as Option<T>.\n// False => T carries #[group(skip)] and parse() will panic.\nfn is_flattenable_optional_args<T: clap::Args>() -> bool {\n    <T as clap::Args>::group_id().is_some()\n}","tryCatchPattern":"// The panic occurs at runtime while augmenting the Command (inside parse/parse_from),\n// so it can be intercepted with catch_unwind as a last-resort guard.\nuse std::panic::{self, AssertUnwindSafe};\n\nlet cli = match panic::catch_unwind(AssertUnwindSafe(|| Cli::parse_from(std::env::args()))) {\n    Ok(cli) => cli,\n    Err(_payload) => {\n        // Most common cause: #[command(flatten)] Option<Args> whose inner type has #[group(skip)].\n        eprintln!(\"CLI misconfigured: cannot `#[flatten]` an `Option<Args>` with `#[group(skip)]`\");\n        std::process::exit(2);\n    }\n};","preventionTips":["Never combine `#[command(flatten)]` on an `Option<T>` field with `#[group(skip)]` on the inner `T: Args`; the generated code calls `group_id().expect(...)` at runtime.","Prefer making the flattened field required (`inner: T` instead of `Option<T>`) — clap only emits the `group_id().expect()` guard for the `Option<Args>` case.","If the inner struct genuinely needs `#[group(skip)]`, do not flatten it behind `Option`: flatten it as a required `Args` or model it as a separate argument/subcommand.","Add a unit test that builds `Cli::command()` (or asserts `<T as Args>::group_id().is_some()` for each flatten target) so the misconfiguration fails in CI rather than at first run.","When upgrading clap, grep your CLI structs for the combination of `flatten` and `group(skip)`, since attribute-compatibility rules can change between versions."],"tags":["clap","derive","flatten","args","group","parser","panic"],"analyzedSha":"ccc92c3b1171c00a6ef610bfa0018fa59c8510f9","analyzedAt":"2026-08-01T15:49:18.624Z","schemaVersion":2}