From d10938dd33de167674977c366a0b3f77044fc4ae Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 24 Mar 2023 23:22:23 -0500 Subject: [PATCH] feat: Allow deferred initialization of subcommands This is mostly targeted at reducing startup time for no-op commands within *very* large applications, like deno (see #4774). This comes at the cost of 1.1 KiB of binary size --- clap_builder/src/builder/command.rs | 30 +++++++++++++++ tests/builder/propagate_globals.rs | 2 +- tests/builder/subcommands.rs | 60 +++++++++++++++++------------ tests/builder/version.rs | 2 +- 4 files changed, 68 insertions(+), 26 deletions(-) diff --git a/clap_builder/src/builder/command.rs b/clap_builder/src/builder/command.rs index 62930766..0363f4b8 100644 --- a/clap_builder/src/builder/command.rs +++ b/clap_builder/src/builder/command.rs @@ -105,6 +105,7 @@ pub struct Command { subcommand_heading: Option, external_value_parser: Option, long_help_exists: bool, + deferred: Option Command>, app_ext: Extensions, } @@ -428,6 +429,30 @@ impl Command { self } + /// Delay initialization for parts of the `Command` + /// + /// This is useful for large applications to delay definitions of subcommands until they are + /// being invoked. + /// + /// # Examples + /// + /// ```rust + /// # use clap_builder as clap; + /// # use clap::{Command, arg}; + /// Command::new("myprog") + /// .subcommand(Command::new("config") + /// .about("Controls configuration features") + /// .defer(|cmd| { + /// cmd.arg(arg!( "Required configuration file to use")) + /// }) + /// ) + /// # ; + /// ``` + pub fn defer(mut self, deferred: fn(Command) -> Command) -> Self { + self.deferred = Some(deferred); + self + } + /// Catch problems earlier in the development cycle. /// /// Most error states are handled as asserts under the assumption they are programming mistake @@ -3824,6 +3849,10 @@ impl Command { pub(crate) fn _build_self(&mut self, expand_help_tree: bool) { debug!("Command::_build: name={:?}", self.get_name()); if !self.settings.is_set(AppSettings::Built) { + if let Some(deferred) = self.deferred.take() { + *self = (deferred)(std::mem::take(self)); + } + // Make sure all the globally set flags apply to us as well self.settings = self.settings | self.g_settings; @@ -4652,6 +4681,7 @@ impl Default for Command { subcommand_heading: Default::default(), external_value_parser: Default::default(), long_help_exists: false, + deferred: None, app_ext: Default::default(), } } diff --git a/tests/builder/propagate_globals.rs b/tests/builder/propagate_globals.rs index c3231bfd..6155e0b9 100644 --- a/tests/builder/propagate_globals.rs +++ b/tests/builder/propagate_globals.rs @@ -17,7 +17,7 @@ fn get_app() -> Command { .global(true) .action(ArgAction::Count), ) - .subcommand(Command::new("outer").subcommand(Command::new("inner"))) + .subcommand(Command::new("outer").defer(|cmd| cmd.subcommand(Command::new("inner")))) } fn get_matches(cmd: Command, argv: &'static str) -> ArgMatches { diff --git a/tests/builder/subcommands.rs b/tests/builder/subcommands.rs index 4131fda9..db57b2ec 100644 --- a/tests/builder/subcommands.rs +++ b/tests/builder/subcommands.rs @@ -5,15 +5,15 @@ use super::utils; #[test] fn subcommand() { let m = Command::new("test") - .subcommand( - Command::new("some").arg( + .subcommand(Command::new("some").defer(|cmd| { + cmd.arg( Arg::new("test") .short('t') .long("test") .action(ArgAction::Set) .help("testing testing"), - ), - ) + ) + })) .arg(Arg::new("other").long("other")) .try_get_matches_from(vec!["myprog", "some", "--test", "testing"]) .unwrap(); @@ -30,15 +30,15 @@ fn subcommand() { #[test] fn subcommand_none_given() { let m = Command::new("test") - .subcommand( - Command::new("some").arg( + .subcommand(Command::new("some").defer(|cmd| { + cmd.arg( Arg::new("test") .short('t') .long("test") .action(ArgAction::Set) .help("testing testing"), - ), - ) + ) + })) .arg(Arg::new("other").long("other")) .try_get_matches_from(vec![""]) .unwrap(); @@ -50,14 +50,16 @@ fn subcommand_none_given() { fn subcommand_multiple() { let m = Command::new("test") .subcommands(vec![ - Command::new("some").arg( - Arg::new("test") - .short('t') - .long("test") - .action(ArgAction::Set) - .help("testing testing"), - ), - Command::new("add").arg(Arg::new("roster").short('r')), + Command::new("some").defer(|cmd| { + cmd.arg( + Arg::new("test") + .short('t') + .long("test") + .action(ArgAction::Set) + .help("testing testing"), + ) + }), + Command::new("add").defer(|cmd| cmd.arg(Arg::new("roster").short('r'))), ]) .arg(Arg::new("other").long("other")) .try_get_matches_from(vec!["myprog", "some", "--test", "testing"]) @@ -148,8 +150,9 @@ Usage: dym [COMMAND] For more information, try '--help'. "; - let cmd = Command::new("dym") - .subcommand(Command::new("subcmd").arg(arg!(-s --subcmdarg "tests"))); + let cmd = Command::new("dym").subcommand( + Command::new("subcmd").defer(|cmd| cmd.arg(arg!(-s --subcmdarg "tests"))), + ); utils::assert_output(cmd, "dym --subcmarg subcmd", EXPECTED, true); } @@ -166,8 +169,9 @@ Usage: dym [COMMAND] For more information, try '--help'. "; - let cmd = Command::new("dym") - .subcommand(Command::new("subcmd").arg(arg!(-s --subcmdarg "tests"))); + let cmd = Command::new("dym").subcommand( + Command::new("subcmd").defer(|cmd| cmd.arg(arg!(-s --subcmdarg "tests"))), + ); utils::assert_output(cmd, "dym --subcmarg foo", EXPECTED, true); } @@ -427,7 +431,7 @@ fn busybox_like_multicall() { } let cmd = Command::new("busybox") .multicall(true) - .subcommand(Command::new("busybox").subcommands(applet_commands())) + .subcommand(Command::new("busybox").defer(|cmd| cmd.subcommands(applet_commands()))) .subcommands(applet_commands()); let m = cmd @@ -553,7 +557,9 @@ Options: .version("1.0.0") .propagate_version(true) .multicall(true) - .subcommand(Command::new("foo").subcommand(Command::new("bar").arg(Arg::new("value")))); + .subcommand(Command::new("foo").defer(|cmd| { + cmd.subcommand(Command::new("bar").defer(|cmd| cmd.arg(Arg::new("value")))) + })); utils::assert_output(cmd, "foo bar --help", EXPECTED, false); } @@ -573,7 +579,10 @@ Options: .version("1.0.0") .propagate_version(true) .multicall(true) - .subcommand(Command::new("foo").subcommand(Command::new("bar").arg(Arg::new("value")))); + .subcommand( + Command::new("foo") + .defer(|cmd| cmd.subcommand(Command::new("bar").arg(Arg::new("value")))), + ); utils::assert_output(cmd, "help foo bar", EXPECTED, false); } @@ -593,7 +602,10 @@ Options: .version("1.0.0") .propagate_version(true) .multicall(true) - .subcommand(Command::new("foo").subcommand(Command::new("bar").arg(Arg::new("value")))); + .subcommand( + Command::new("foo") + .defer(|cmd| cmd.subcommand(Command::new("bar").arg(Arg::new("value")))), + ); cmd.build(); let subcmd = cmd.find_subcommand_mut("foo").unwrap(); let subcmd = subcmd.find_subcommand_mut("bar").unwrap(); diff --git a/tests/builder/version.rs b/tests/builder/version.rs index c83bec9e..0fe22ead 100644 --- a/tests/builder/version.rs +++ b/tests/builder/version.rs @@ -19,7 +19,7 @@ fn with_both() -> Command { } fn with_subcommand() -> Command { - with_version().subcommand(Command::new("bar").subcommand(Command::new("baz"))) + with_version().subcommand(Command::new("bar").defer(|cmd| cmd.subcommand(Command::new("baz")))) } #[test]