Skip to content

Commit d10938d

Browse files
committed
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
1 parent 475e254 commit d10938d

File tree

4 files changed

+68
-26
lines changed

4 files changed

+68
-26
lines changed

clap_builder/src/builder/command.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ pub struct Command {
105105
subcommand_heading: Option<Str>,
106106
external_value_parser: Option<super::ValueParser>,
107107
long_help_exists: bool,
108+
deferred: Option<fn(Command) -> Command>,
108109
app_ext: Extensions,
109110
}
110111

@@ -428,6 +429,30 @@ impl Command {
428429
self
429430
}
430431

432+
/// Delay initialization for parts of the `Command`
433+
///
434+
/// This is useful for large applications to delay definitions of subcommands until they are
435+
/// being invoked.
436+
///
437+
/// # Examples
438+
///
439+
/// ```rust
440+
/// # use clap_builder as clap;
441+
/// # use clap::{Command, arg};
442+
/// Command::new("myprog")
443+
/// .subcommand(Command::new("config")
444+
/// .about("Controls configuration features")
445+
/// .defer(|cmd| {
446+
/// cmd.arg(arg!(<config> "Required configuration file to use"))
447+
/// })
448+
/// )
449+
/// # ;
450+
/// ```
451+
pub fn defer(mut self, deferred: fn(Command) -> Command) -> Self {
452+
self.deferred = Some(deferred);
453+
self
454+
}
455+
431456
/// Catch problems earlier in the development cycle.
432457
///
433458
/// Most error states are handled as asserts under the assumption they are programming mistake
@@ -3824,6 +3849,10 @@ impl Command {
38243849
pub(crate) fn _build_self(&mut self, expand_help_tree: bool) {
38253850
debug!("Command::_build: name={:?}", self.get_name());
38263851
if !self.settings.is_set(AppSettings::Built) {
3852+
if let Some(deferred) = self.deferred.take() {
3853+
*self = (deferred)(std::mem::take(self));
3854+
}
3855+
38273856
// Make sure all the globally set flags apply to us as well
38283857
self.settings = self.settings | self.g_settings;
38293858

@@ -4652,6 +4681,7 @@ impl Default for Command {
46524681
subcommand_heading: Default::default(),
46534682
external_value_parser: Default::default(),
46544683
long_help_exists: false,
4684+
deferred: None,
46554685
app_ext: Default::default(),
46564686
}
46574687
}

tests/builder/propagate_globals.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ fn get_app() -> Command {
1717
.global(true)
1818
.action(ArgAction::Count),
1919
)
20-
.subcommand(Command::new("outer").subcommand(Command::new("inner")))
20+
.subcommand(Command::new("outer").defer(|cmd| cmd.subcommand(Command::new("inner"))))
2121
}
2222

2323
fn get_matches(cmd: Command, argv: &'static str) -> ArgMatches {

tests/builder/subcommands.rs

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@ use super::utils;
55
#[test]
66
fn subcommand() {
77
let m = Command::new("test")
8-
.subcommand(
9-
Command::new("some").arg(
8+
.subcommand(Command::new("some").defer(|cmd| {
9+
cmd.arg(
1010
Arg::new("test")
1111
.short('t')
1212
.long("test")
1313
.action(ArgAction::Set)
1414
.help("testing testing"),
15-
),
16-
)
15+
)
16+
}))
1717
.arg(Arg::new("other").long("other"))
1818
.try_get_matches_from(vec!["myprog", "some", "--test", "testing"])
1919
.unwrap();
@@ -30,15 +30,15 @@ fn subcommand() {
3030
#[test]
3131
fn subcommand_none_given() {
3232
let m = Command::new("test")
33-
.subcommand(
34-
Command::new("some").arg(
33+
.subcommand(Command::new("some").defer(|cmd| {
34+
cmd.arg(
3535
Arg::new("test")
3636
.short('t')
3737
.long("test")
3838
.action(ArgAction::Set)
3939
.help("testing testing"),
40-
),
41-
)
40+
)
41+
}))
4242
.arg(Arg::new("other").long("other"))
4343
.try_get_matches_from(vec![""])
4444
.unwrap();
@@ -50,14 +50,16 @@ fn subcommand_none_given() {
5050
fn subcommand_multiple() {
5151
let m = Command::new("test")
5252
.subcommands(vec![
53-
Command::new("some").arg(
54-
Arg::new("test")
55-
.short('t')
56-
.long("test")
57-
.action(ArgAction::Set)
58-
.help("testing testing"),
59-
),
60-
Command::new("add").arg(Arg::new("roster").short('r')),
53+
Command::new("some").defer(|cmd| {
54+
cmd.arg(
55+
Arg::new("test")
56+
.short('t')
57+
.long("test")
58+
.action(ArgAction::Set)
59+
.help("testing testing"),
60+
)
61+
}),
62+
Command::new("add").defer(|cmd| cmd.arg(Arg::new("roster").short('r'))),
6163
])
6264
.arg(Arg::new("other").long("other"))
6365
.try_get_matches_from(vec!["myprog", "some", "--test", "testing"])
@@ -148,8 +150,9 @@ Usage: dym [COMMAND]
148150
For more information, try '--help'.
149151
";
150152

151-
let cmd = Command::new("dym")
152-
.subcommand(Command::new("subcmd").arg(arg!(-s --subcmdarg <subcmdarg> "tests")));
153+
let cmd = Command::new("dym").subcommand(
154+
Command::new("subcmd").defer(|cmd| cmd.arg(arg!(-s --subcmdarg <subcmdarg> "tests"))),
155+
);
153156

154157
utils::assert_output(cmd, "dym --subcmarg subcmd", EXPECTED, true);
155158
}
@@ -166,8 +169,9 @@ Usage: dym [COMMAND]
166169
For more information, try '--help'.
167170
";
168171

169-
let cmd = Command::new("dym")
170-
.subcommand(Command::new("subcmd").arg(arg!(-s --subcmdarg <subcmdarg> "tests")));
172+
let cmd = Command::new("dym").subcommand(
173+
Command::new("subcmd").defer(|cmd| cmd.arg(arg!(-s --subcmdarg <subcmdarg> "tests"))),
174+
);
171175

172176
utils::assert_output(cmd, "dym --subcmarg foo", EXPECTED, true);
173177
}
@@ -427,7 +431,7 @@ fn busybox_like_multicall() {
427431
}
428432
let cmd = Command::new("busybox")
429433
.multicall(true)
430-
.subcommand(Command::new("busybox").subcommands(applet_commands()))
434+
.subcommand(Command::new("busybox").defer(|cmd| cmd.subcommands(applet_commands())))
431435
.subcommands(applet_commands());
432436

433437
let m = cmd
@@ -553,7 +557,9 @@ Options:
553557
.version("1.0.0")
554558
.propagate_version(true)
555559
.multicall(true)
556-
.subcommand(Command::new("foo").subcommand(Command::new("bar").arg(Arg::new("value"))));
560+
.subcommand(Command::new("foo").defer(|cmd| {
561+
cmd.subcommand(Command::new("bar").defer(|cmd| cmd.arg(Arg::new("value"))))
562+
}));
557563
utils::assert_output(cmd, "foo bar --help", EXPECTED, false);
558564
}
559565

@@ -573,7 +579,10 @@ Options:
573579
.version("1.0.0")
574580
.propagate_version(true)
575581
.multicall(true)
576-
.subcommand(Command::new("foo").subcommand(Command::new("bar").arg(Arg::new("value"))));
582+
.subcommand(
583+
Command::new("foo")
584+
.defer(|cmd| cmd.subcommand(Command::new("bar").arg(Arg::new("value")))),
585+
);
577586
utils::assert_output(cmd, "help foo bar", EXPECTED, false);
578587
}
579588

@@ -593,7 +602,10 @@ Options:
593602
.version("1.0.0")
594603
.propagate_version(true)
595604
.multicall(true)
596-
.subcommand(Command::new("foo").subcommand(Command::new("bar").arg(Arg::new("value"))));
605+
.subcommand(
606+
Command::new("foo")
607+
.defer(|cmd| cmd.subcommand(Command::new("bar").arg(Arg::new("value")))),
608+
);
597609
cmd.build();
598610
let subcmd = cmd.find_subcommand_mut("foo").unwrap();
599611
let subcmd = subcmd.find_subcommand_mut("bar").unwrap();

tests/builder/version.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ fn with_both() -> Command {
1919
}
2020

2121
fn with_subcommand() -> Command {
22-
with_version().subcommand(Command::new("bar").subcommand(Command::new("baz")))
22+
with_version().subcommand(Command::new("bar").defer(|cmd| cmd.subcommand(Command::new("baz"))))
2323
}
2424

2525
#[test]

0 commit comments

Comments
 (0)