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
This commit is contained in:
Ed Page 2023-03-24 23:22:23 -05:00
parent 475e254d25
commit d10938dd33
4 changed files with 68 additions and 26 deletions

View file

@ -105,6 +105,7 @@ pub struct Command {
subcommand_heading: Option<Str>,
external_value_parser: Option<super::ValueParser>,
long_help_exists: bool,
deferred: Option<fn(Command) -> 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!(<config> "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(),
}
}

View file

@ -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 {

View file

@ -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 <subcmdarg> "tests")));
let cmd = Command::new("dym").subcommand(
Command::new("subcmd").defer(|cmd| cmd.arg(arg!(-s --subcmdarg <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 <subcmdarg> "tests")));
let cmd = Command::new("dym").subcommand(
Command::new("subcmd").defer(|cmd| cmd.arg(arg!(-s --subcmdarg <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();

View file

@ -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]