From 057aef71dcb041a4a0c7b2833d2ddb4fd64ed009 Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:01:15 -0800 Subject: [PATCH 1/5] refactor(derive): Extract top-level methods Move the application of initial_top_level_methods and final_top_level_methods out of gen_augment and into its callers in both args.rs and subcommand.rs. This gives callers control over when and how metadata (about, version, etc.) is applied, separately from args and groups. Part of #4959 --- clap_derive/src/derives/args.rs | 15 ++++++++------ clap_derive/src/derives/subcommand.rs | 30 ++++++++++++++++++--------- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/clap_derive/src/derives/args.rs b/clap_derive/src/derives/args.rs index 53bd5111091..ff62ebc2b8f 100644 --- a/clap_derive/src/derives/args.rs +++ b/clap_derive/src/derives/args.rs @@ -77,6 +77,8 @@ pub(crate) fn gen_for_struct( let app_var = Ident::new("__clap_app", Span::call_site()); let augmentation = gen_augment(fields, &app_var, item, false)?; let augmentation_update = gen_augment(fields, &app_var, item, true)?; + let initial_app_methods = item.initial_top_level_methods(); + let final_app_methods = item.final_top_level_methods(); let group_id = if item.skip_group() { quote!(None) @@ -155,10 +157,14 @@ pub(crate) fn gen_for_struct( #group_id } fn augment_args<'b>(#app_var: clap::Command) -> clap::Command { - #augmentation + let #app_var = #app_var #initial_app_methods; + let #app_var = #augmentation; + #app_var #final_app_methods } fn augment_args_for_update<'b>(#app_var: clap::Command) -> clap::Command { - #augmentation_update + let #app_var = #app_var #initial_app_methods; + let #app_var = #augmentation_update; + #app_var #final_app_methods } } }) @@ -380,8 +386,6 @@ pub(crate) fn gen_augment( } else { quote!() }; - let initial_app_methods = parent_item.initial_top_level_methods(); - let final_app_methods = parent_item.final_top_level_methods(); let group_app_methods = if parent_item.skip_group() { quote!() } else { @@ -432,11 +436,10 @@ pub(crate) fn gen_augment( Ok(quote! {{ #deprecations let #app_var = #app_var - #initial_app_methods #group_app_methods ; #( #args )* - #app_var #final_app_methods + #app_var }}) } diff --git a/clap_derive/src/derives/subcommand.rs b/clap_derive/src/derives/subcommand.rs index 6a29134c00e..484cc289835 100644 --- a/clap_derive/src/derives/subcommand.rs +++ b/clap_derive/src/derives/subcommand.rs @@ -64,6 +64,8 @@ pub(crate) fn gen_for_enum( let augmentation = gen_augment(variants, item, false)?; let augmentation_update = gen_augment(variants, item, true)?; let has_subcommand = gen_has_subcommand(variants)?; + let initial_app_methods = item.initial_top_level_methods(); + let final_app_methods = item.final_top_level_methods(); Ok(quote! { #[allow( @@ -123,10 +125,14 @@ pub(crate) fn gen_for_enum( #[automatically_derived] impl #impl_generics clap::Subcommand for #item_name #ty_generics #where_clause { fn augment_subcommands <'b>(__clap_app: clap::Command) -> clap::Command { - #augmentation + let __clap_app = __clap_app #initial_app_methods; + let __clap_app = #augmentation; + __clap_app #final_app_methods } fn augment_subcommands_for_update <'b>(__clap_app: clap::Command) -> clap::Command { - #augmentation_update + let __clap_app = __clap_app #initial_app_methods; + let __clap_app = #augmentation_update; + __clap_app #final_app_methods } fn has_subcommand(__clap_name: &str) -> bool { #has_subcommand @@ -277,9 +283,16 @@ fn gen_augment( let subcommand_var = Ident::new("__clap_subcommand", Span::call_site()); let sub_augment = match variant.fields { Named(ref fields) => { - // Defer to `gen_augment` for adding cmd methods let fields = collect_args_fields(item, fields)?; - args::gen_augment(&fields, &subcommand_var, item, override_required)? + let arg_block = + args::gen_augment(&fields, &subcommand_var, item, override_required)?; + let initial_app_methods = item.initial_top_level_methods(); + let final_from_attrs = item.final_top_level_methods(); + quote! { + let #subcommand_var = #subcommand_var #initial_app_methods; + let #subcommand_var = #arg_block; + #subcommand_var #final_from_attrs + } } Unit => { let arg_block = quote!( #subcommand_var ); @@ -343,14 +356,11 @@ fn gen_augment( } else { quote!() }; - let initial_app_methods = parent_item.initial_top_level_methods(); - let final_app_methods = parent_item.final_top_level_methods(); - Ok(quote! { + Ok(quote! {{ #deprecations; - let #app_var = #app_var #initial_app_methods; #( #subcommands )*; - #app_var #final_app_methods - }) + #app_var + }}) } fn gen_has_subcommand(variants: &[(&Variant, Item)]) -> Result { From e6e635d765c71ae1af8f71c799033096c9c09a2d Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:01:58 -0800 Subject: [PATCH 2/5] refactor(derive): Unify Kind::Command codegen Restructure Kind::Command branches in subcommand.rs so that all three field variants (Named, Unit, Unnamed) return a (sub_augment, initial_app_methods, final_from_attrs) tuple, with the surrounding code applying them uniformly. Part of #4959 --- clap_derive/src/derives/subcommand.rs | 49 ++++++++++++--------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/clap_derive/src/derives/subcommand.rs b/clap_derive/src/derives/subcommand.rs index 484cc289835..c4450b9945c 100644 --- a/clap_derive/src/derives/subcommand.rs +++ b/clap_derive/src/derives/subcommand.rs @@ -281,29 +281,22 @@ fn gen_augment( Kind::Command(_) => { let subcommand_var = Ident::new("__clap_subcommand", Span::call_site()); - let sub_augment = match variant.fields { + let (sub_augment, initial_app_methods, final_from_attrs) = match variant.fields { Named(ref fields) => { let fields = collect_args_fields(item, fields)?; - let arg_block = + let sub_augment = args::gen_augment(&fields, &subcommand_var, item, override_required)?; - let initial_app_methods = item.initial_top_level_methods(); - let final_from_attrs = item.final_top_level_methods(); - quote! { - let #subcommand_var = #subcommand_var #initial_app_methods; - let #subcommand_var = #arg_block; - #subcommand_var #final_from_attrs - } - } - Unit => { - let arg_block = quote!( #subcommand_var ); - let initial_app_methods = item.initial_top_level_methods(); - let final_from_attrs = item.final_top_level_methods(); - quote! { - let #subcommand_var = #subcommand_var #initial_app_methods; - let #subcommand_var = #arg_block; - #subcommand_var #final_from_attrs - } + ( + sub_augment, + item.initial_top_level_methods(), + item.final_top_level_methods(), + ) } + Unit => ( + quote! { #subcommand_var }, + item.initial_top_level_methods(), + item.final_top_level_methods(), + ), Unnamed(FieldsUnnamed { ref unnamed, .. }) if unnamed.len() == 1 => { let ty = &unnamed[0].ty; let arg_block = if override_required { @@ -319,13 +312,11 @@ fn gen_augment( } } }; - let initial_app_methods = item.initial_top_level_methods(); - let final_from_attrs = item.final_top_level_methods(); - quote! { - let #subcommand_var = #subcommand_var #initial_app_methods; - let #subcommand_var = #arg_block; - #subcommand_var #final_from_attrs - } + ( + arg_block, + item.initial_top_level_methods(), + item.final_top_level_methods(), + ) } Unnamed(..) => { abort!(variant, "invalid variant for `#[command(subcommand)]`, expected a newtype variant") @@ -341,8 +332,10 @@ fn gen_augment( let subcommand = quote! { let #app_var = #app_var.subcommand({ #deprecations - let #subcommand_var = clap::Command::new(#name); - #sub_augment + let #subcommand_var = clap::Command::new(#name) + #initial_app_methods; + let #subcommand_var = #sub_augment; + #subcommand_var #final_from_attrs }); }; Some(subcommand) From 33ab7453893f3c648f1929cd243d7906cf0e4bad Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:28:02 -0800 Subject: [PATCH 3/5] test(derive): Add baseline defer tests Add tests showing the current eager initialization behavior. The subsequent feature commit will modify these to use #[command(defer)] and update assertions to reflect deferred behavior. Tests cover: - Subcommand with named fields - Nested subcommands with Args structs - Flattened Args in subcommands - Enum variants with inline args - Default behavior is eager Part of #4959 --- tests/derive/defer.rs | 257 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 tests/derive/defer.rs diff --git a/tests/derive/defer.rs b/tests/derive/defer.rs new file mode 100644 index 00000000000..47561e22643 --- /dev/null +++ b/tests/derive/defer.rs @@ -0,0 +1,257 @@ +// Tests for subcommand initialization behavior. +// +// These tests establish the current (eager) behavior as a baseline. +// A subsequent commit will add `#[command(defer = true)]` to enable lazy +// initialization, and the test diffs will show how behavior changes. +// +// See: https://github.com/clap-rs/clap/issues/4959 + +use clap::{Args, CommandFactory, Parser, Subcommand}; + +#[test] +fn subcommand_with_named_fields() { + #[derive(Parser, Debug)] + struct Cli { + #[command(subcommand)] + cmd: Commands, + } + + #[derive(Subcommand, Debug)] + enum Commands { + /// Add a file + Add { + #[arg(long)] + file: String, + }, + /// Remove a file + Remove { + #[arg(long)] + force: bool, + }, + } + + let cmd = Cli::command(); + let add_cmd = cmd + .get_subcommands() + .find(|s| s.get_name() == "add") + .expect("add subcommand should exist"); + assert_eq!( + add_cmd.get_about().map(|s| s.to_string()), + Some("Add a file".to_string()), + ); + + let remove_cmd = cmd + .get_subcommands() + .find(|s| s.get_name() == "remove") + .expect("remove should exist"); + assert_eq!( + remove_cmd.get_about().map(|s| s.to_string()), + Some("Remove a file".to_string()), + ); + + let user_args: Vec<_> = add_cmd + .get_arguments() + .filter(|a| a.get_id() != "help" && a.get_id() != "version") + .map(|a| a.get_id().as_str()) + .collect(); + assert!( + user_args.contains(&"file"), + "args should be immediately visible, got: {user_args:?}" + ); +} + +#[test] +fn nested_subcommands() { + #[derive(Parser, Debug, PartialEq)] + struct Cli { + #[command(subcommand)] + cmd: Option, + } + + #[derive(Subcommand, Debug, PartialEq)] + enum TopLevel { + /// Account operations + Account(AccountArgs), + } + + #[derive(Args, Debug, PartialEq)] + struct AccountArgs { + /// Account ID + account_id: Option, + #[command(subcommand)] + action: Option, + } + + #[derive(Subcommand, Debug, PartialEq)] + enum AccountAction { + /// View account + View { + #[arg(long)] + verbose: bool, + }, + } + + let cmd = Cli::command(); + + let account = cmd + .get_subcommands() + .find(|s| s.get_name() == "account") + .expect("account should exist"); + let args: Vec<_> = account + .get_arguments() + .filter(|a| a.get_id() != "help" && a.get_id() != "version") + .map(|a| a.get_id().as_str()) + .collect(); + assert!( + args.contains(&"account_id"), + "account_id should be visible, got: {args:?}" + ); + + let parsed = Cli::try_parse_from(["test", "account", "alice", "view"]).unwrap(); + assert_eq!( + parsed, + Cli { + cmd: Some(TopLevel::Account(AccountArgs { + account_id: Some("alice".to_string()), + action: Some(AccountAction::View { verbose: false }) + })) + } + ); + + let parsed = Cli::try_parse_from(["test", "account", "alice", "view", "--verbose"]).unwrap(); + assert_eq!( + parsed, + Cli { + cmd: Some(TopLevel::Account(AccountArgs { + account_id: Some("alice".to_string()), + action: Some(AccountAction::View { verbose: true }) + })) + } + ); +} + +#[test] +fn flatten_args_in_subcommand_are_eager() { + #[derive(Parser, Debug, PartialEq)] + struct Cli { + #[command(subcommand)] + cmd: Commands, + } + + #[derive(Subcommand, Debug, PartialEq)] + enum Commands { + Add(FlattenedArgs), + } + + #[derive(Args, Debug, PartialEq)] + struct FlattenedArgs { + #[arg(long)] + file: String, + #[command(flatten)] + common: CommonArgs, + } + + #[derive(Args, Debug, PartialEq)] + struct CommonArgs { + #[arg(long)] + verbose: bool, + } + + let cmd = Cli::command(); + let add = cmd + .get_subcommands() + .find(|s| s.get_name() == "add") + .expect("add should exist"); + + let user_args: Vec<_> = add + .get_arguments() + .filter(|a| a.get_id() != "help" && a.get_id() != "version") + .map(|a| a.get_id().as_str()) + .collect(); + assert!( + user_args.contains(&"file"), + "file should be visible, got: {user_args:?}" + ); + assert!( + user_args.contains(&"verbose"), + "flattened args should be visible, got: {user_args:?}" + ); + + let parsed = + Cli::try_parse_from(["test", "add", "--file", "config.toml", "--verbose"]).unwrap(); + assert_eq!( + parsed, + Cli { + cmd: Commands::Add(FlattenedArgs { + file: "config.toml".to_string(), + common: CommonArgs { verbose: true }, + }), + } + ); +} + +#[test] +fn enum_variant_with_inline_args() { + #[derive(Parser, Debug, PartialEq)] + struct Cli { + #[command(subcommand)] + cmd: Cmd, + } + + #[derive(Subcommand, Debug, PartialEq)] + enum Cmd { + Sub { + #[arg(long)] + flag: bool, + }, + } + + let parsed = Cli::try_parse_from(["test", "sub", "--flag"]).unwrap(); + assert_eq!( + parsed, + Cli { + cmd: Cmd::Sub { flag: true } + } + ); + + let parsed = Cli::try_parse_from(["test", "sub"]).unwrap(); + assert_eq!( + parsed, + Cli { + cmd: Cmd::Sub { flag: false } + } + ); +} + +#[test] +fn default_behavior_is_eager() { + #[derive(Parser, Debug)] + struct Cli { + #[command(subcommand)] + cmd: Commands, + } + + #[derive(Subcommand, Debug)] + enum Commands { + Add { + #[arg(long)] + file: String, + }, + } + + let cmd = Cli::command(); + let add_cmd = cmd + .get_subcommands() + .find(|s| s.get_name() == "add") + .expect("add should exist"); + + let user_args: Vec<_> = add_cmd + .get_arguments() + .filter(|a| a.get_id() != "help" && a.get_id() != "version") + .map(|a| a.get_id().as_str()) + .collect(); + assert!( + user_args.contains(&"file"), + "default: args should be immediately visible" + ); +} From 2f7917c2c8540680be1dfe8f2c27e55f5bf35fbd Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Tue, 10 Feb 2026 14:03:11 -0800 Subject: [PATCH 4/5] test(derive): Add build() call in help test find_subcommand inspects nested subcommands that may be deferred once unstable-v5 defaults defer to true. Calling build() first ensures the subcommands are materialized, making this test forward-compatible. Part of #4959 --- tests/derive/help.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/derive/help.rs b/tests/derive/help.rs index ebb3c6bfd43..197a9c59611 100644 --- a/tests/derive/help.rs +++ b/tests/derive/help.rs @@ -139,6 +139,9 @@ fn app_help_heading_flattened() { .unwrap(); assert_eq!(should_be_in_section_b.get_help_heading(), Some("HEADING B")); + let mut cmd = cmd; + cmd.build(); + let sub_a_two = cmd.find_subcommand("sub-a-two").unwrap(); let should_be_in_sub_a = sub_a_two @@ -376,7 +379,9 @@ fn derive_order_no_next_order() { let mut cmd = Args::command(); let help = cmd.render_help().to_string(); - assert_data_eq!(help, str![[r#" + assert_data_eq!( + help, + str![[r#" Usage: test [OPTIONS] Options: @@ -387,7 +392,8 @@ Options: --option-b second option -V, --version Print version -"#]]); +"#]] + ); } #[test] From 531fdd5c8e054fe9c58fc37ac4b230b22afa2157 Mon Sep 17 00:00:00 2001 From: r-near <163825889+r-near@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:28:50 -0800 Subject: [PATCH 5/5] feat(derive): Add #[command(defer)] attribute Add support for deferred subcommand initialization in the derive API via #[command(defer = true)]. When enabled, the derive macro generates code that uses Command::defer() to lazily initialize subcommand args, while keeping metadata (about, version, etc.) eagerly applied so that --help displays subcommand descriptions without needing to build. The attribute defaults to false in clap v4 and true when the unstable-v5 feature is enabled. This can dramatically improve startup time for CLIs with many deeply nested subcommands (e.g. 4-6 seconds down to ~12ms for a CLI with 330 subcommands). Part of #4959 --- clap_derive/src/attr.rs | 19 ++++++ clap_derive/src/derives/subcommand.rs | 8 +++ clap_derive/src/item.rs | 11 ++++ tests/derive/defer.rs | 85 +++++++++++++++++++++++---- 4 files changed, 110 insertions(+), 13 deletions(-) diff --git a/clap_derive/src/attr.rs b/clap_derive/src/attr.rs index 51736c4d1f6..e18ce18aab5 100644 --- a/clap_derive/src/attr.rs +++ b/clap_derive/src/attr.rs @@ -69,6 +69,23 @@ impl ClapAttr { } } } + + pub(crate) fn lit_bool_or_abort(&self) -> Result { + let value = self.value_or_abort()?; + match value { + AttrValue::Expr(Expr::Lit(syn::ExprLit { + lit: syn::Lit::Bool(lit), + .. + })) => Ok(lit.value()), + _ => { + abort!( + self.name, + "attribute `{}` can only accept bool literals", + self.name + ) + } + } + } } impl Parse for ClapAttr { @@ -102,6 +119,7 @@ impl Parse for ClapAttr { "long_help" => Some(MagicAttrName::LongHelp), "author" => Some(MagicAttrName::Author), "version" => Some(MagicAttrName::Version), + "defer" => Some(MagicAttrName::Defer), _ => None, }; @@ -168,6 +186,7 @@ pub(crate) enum MagicAttrName { DefaultValuesOsT, NextDisplayOrder, NextHelpHeading, + Defer, } #[derive(Clone)] diff --git a/clap_derive/src/derives/subcommand.rs b/clap_derive/src/derives/subcommand.rs index c4450b9945c..bcddaef30a9 100644 --- a/clap_derive/src/derives/subcommand.rs +++ b/clap_derive/src/derives/subcommand.rs @@ -323,6 +323,14 @@ fn gen_augment( } }; + let sub_augment = if parent_item.defer() { + quote! { + #subcommand_var.defer(|#subcommand_var| { #sub_augment }) + } + } else { + sub_augment + }; + let deprecations = if !override_required { item.deprecations() } else { diff --git a/clap_derive/src/item.rs b/clap_derive/src/item.rs index 2f3c7f9c2cb..8d023e345de 100644 --- a/clap_derive/src/item.rs +++ b/clap_derive/src/item.rs @@ -50,6 +50,7 @@ pub(crate) struct Item { group_id: Name, group_methods: Vec, kind: Sp, + defer: bool, } impl Item { @@ -279,6 +280,7 @@ impl Item { group_id, group_methods: vec![], kind, + defer: cfg!(feature = "unstable-v5"), } } @@ -835,6 +837,11 @@ impl Item { self.skip_group = true; } + Some(MagicAttrName::Defer) => { + assert_attr_kind(attr, &[AttrKind::Command])?; + self.defer = attr.lit_bool_or_abort()?; + } + None // Magic only for the default, otherwise just forward to the builder | Some(MagicAttrName::Short) @@ -1090,6 +1097,10 @@ impl Item { pub(crate) fn skip_group(&self) -> bool { self.skip_group } + + pub(crate) fn defer(&self) -> bool { + self.defer + } } #[derive(Clone)] diff --git a/tests/derive/defer.rs b/tests/derive/defer.rs index 47561e22643..f43a54d7346 100644 --- a/tests/derive/defer.rs +++ b/tests/derive/defer.rs @@ -1,9 +1,7 @@ -// Tests for subcommand initialization behavior. -// -// These tests establish the current (eager) behavior as a baseline. -// A subsequent commit will add `#[command(defer = true)]` to enable lazy -// initialization, and the test diffs will show how behavior changes. +// Tests for #[command(defer = true/false)] attribute. // +// This feature enables lazy initialization of subcommand arguments, +// dramatically improving CLI startup time for applications with many subcommands. // See: https://github.com/clap-rs/clap/issues/4959 use clap::{Args, CommandFactory, Parser, Subcommand}; @@ -17,6 +15,7 @@ fn subcommand_with_named_fields() { } #[derive(Subcommand, Debug)] + #[command(defer = true)] enum Commands { /// Add a file Add { @@ -49,6 +48,24 @@ fn subcommand_with_named_fields() { Some("Remove a file".to_string()), ); + // Args should be deferred before build + let user_args: Vec<_> = add_cmd + .get_arguments() + .filter(|a| a.get_id() != "help" && a.get_id() != "version") + .collect(); + assert!( + user_args.is_empty(), + "args should be deferred before build, got: {:?}", + user_args.iter().map(|a| a.get_id()).collect::>() + ); + + // After build, args should be visible + let mut cmd = cmd; + cmd.build(); + let add_cmd = cmd + .get_subcommands() + .find(|s| s.get_name() == "add") + .expect("add subcommand should exist after build"); let user_args: Vec<_> = add_cmd .get_arguments() .filter(|a| a.get_id() != "help" && a.get_id() != "version") @@ -56,7 +73,7 @@ fn subcommand_with_named_fields() { .collect(); assert!( user_args.contains(&"file"), - "args should be immediately visible, got: {user_args:?}" + "args should be visible after build, got: {user_args:?}" ); } @@ -69,6 +86,7 @@ fn nested_subcommands() { } #[derive(Subcommand, Debug, PartialEq)] + #[command(defer = true)] enum TopLevel { /// Account operations Account(AccountArgs), @@ -83,6 +101,7 @@ fn nested_subcommands() { } #[derive(Subcommand, Debug, PartialEq)] + #[command(defer = true)] enum AccountAction { /// View account View { @@ -91,12 +110,28 @@ fn nested_subcommands() { }, } + // Args should be deferred before build let cmd = Cli::command(); - let account = cmd .get_subcommands() .find(|s| s.get_name() == "account") .expect("account should exist"); + let user_args: Vec<_> = account + .get_arguments() + .filter(|a| a.get_id() != "help" && a.get_id() != "version") + .collect(); + assert!( + user_args.is_empty(), + "account arguments should be deferred before build" + ); + + // After build, args and nested subcommands should be visible + let mut cmd = cmd; + cmd.build(); + let account = cmd + .get_subcommands() + .find(|s| s.get_name() == "account") + .expect("account should exist after build"); let args: Vec<_> = account .get_arguments() .filter(|a| a.get_id() != "help" && a.get_id() != "version") @@ -104,9 +139,10 @@ fn nested_subcommands() { .collect(); assert!( args.contains(&"account_id"), - "account_id should be visible, got: {args:?}" + "account_id should be visible after build, got: {args:?}" ); + // Parsing should still work (triggers build internally) let parsed = Cli::try_parse_from(["test", "account", "alice", "view"]).unwrap(); assert_eq!( parsed, @@ -131,7 +167,7 @@ fn nested_subcommands() { } #[test] -fn flatten_args_in_subcommand_are_eager() { +fn flatten_args_in_subcommand_are_deferred() { #[derive(Parser, Debug, PartialEq)] struct Cli { #[command(subcommand)] @@ -139,6 +175,7 @@ fn flatten_args_in_subcommand_are_eager() { } #[derive(Subcommand, Debug, PartialEq)] + #[command(defer = true)] enum Commands { Add(FlattenedArgs), } @@ -163,6 +200,22 @@ fn flatten_args_in_subcommand_are_eager() { .find(|s| s.get_name() == "add") .expect("add should exist"); + let user_args: Vec<_> = add + .get_arguments() + .filter(|a| a.get_id() != "help" && a.get_id() != "version") + .collect(); + assert!( + user_args.is_empty(), + "flattened args should be deferred before build" + ); + + let mut cmd = cmd; + cmd.build(); + let add = cmd + .get_subcommands() + .find(|s| s.get_name() == "add") + .expect("add should exist after build"); + let user_args: Vec<_> = add .get_arguments() .filter(|a| a.get_id() != "help" && a.get_id() != "version") @@ -199,6 +252,7 @@ fn enum_variant_with_inline_args() { } #[derive(Subcommand, Debug, PartialEq)] + #[command(defer = true)] enum Cmd { Sub { #[arg(long)] @@ -250,8 +304,13 @@ fn default_behavior_is_eager() { .filter(|a| a.get_id() != "help" && a.get_id() != "version") .map(|a| a.get_id().as_str()) .collect(); - assert!( - user_args.contains(&"file"), - "default: args should be immediately visible" - ); + + if cfg!(feature = "unstable-v5") { + assert!(user_args.is_empty(), "v5 default: args should be deferred"); + } else { + assert!( + user_args.contains(&"file"), + "v4 default: args should be immediately visible" + ); + } }