Skip to content

Commit

Permalink
Rewrite Multicall handling to just strip dir from argv0
Browse files Browse the repository at this point in the history
  • Loading branch information
fishface60 committed Nov 19, 2021
1 parent 568fd46 commit 2fe7a89
Show file tree
Hide file tree
Showing 4 changed files with 84 additions and 90 deletions.
40 changes: 26 additions & 14 deletions examples/24a_multicall_busybox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,40 @@ use clap::{App, AppSettings, Arg};
fn main() {
let app = App::new(env!("CARGO_CRATE_NAME"))
.setting(AppSettings::ArgRequiredElseHelp)
.arg(
Arg::new("install")
.long("install")
.about("Install hardlinks for all subcommands in path")
.exclusive(true)
.takes_value(true)
.default_missing_value("/usr/local/bin")
.use_delimiter(false),
.subcommand(
App::new("busybox")
.arg(
Arg::new("install")
.long("install")
.about("Install hardlinks for all subcommands in path")
.exclusive(true)
.takes_value(true)
.default_missing_value("/usr/local/bin")
.use_delimiter(false),
)
.subcommand(App::new("true").about("does nothing successfully"))
.subcommand(App::new("false").about("does nothing unsuccessfully")),
)
.subcommand(App::new("true").about("does nothing successfully"))
.subcommand(App::new("false").about("does nothing unsuccessfully"));

#[cfg(feature = "unstable-multicall")]
let app = app.setting(AppSettings::Multicall);
let matches = app.get_matches();
if matches.occurrences_of("install") > 0 {
unimplemented!("Make hardlinks to the executable here");
}

exit(match matches.subcommand_name() {
Some("true") => 0,
Some("false") => 1,
exit(match matches.subcommand() {
Some(("busybox", cmd)) => {
if cmd.occurrences_of("install") > 0 {
unimplemented!("Make hardlinks to the executable here");
}
match cmd.subcommand() {
Some(("false", _)) => 1,
Some(("true", _)) => 0,
_ => 127,
}
}
Some(("false", _)) => 1,
Some(("true", _)) => 0,
_ => 127,
})
}
41 changes: 7 additions & 34 deletions src/build/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2182,47 +2182,20 @@ impl<'help> App<'help> {
if self.settings.is_set(AppSettings::Multicall) {
if let Some((argv0, _)) = it.next() {
let argv0 = Path::new(&argv0);
if let Some(command) = argv0.file_name().and_then(|f| f.to_str()) {
if let Some(command) = argv0.file_stem().and_then(|f| f.to_str()) {
// Stop borrowing command so we can get another mut ref to it.
let command = command.to_owned();
debug!(
"App::try_get_matches_from_mut: Parsed command {} from argv",
command
);

let subcommand = self
.subcommands
.iter_mut()
.find(|subcommand| subcommand.aliases_to(&command));
debug!(
"App::try_get_matches_from_mut: Matched subcommand {:?}",
subcommand
);

match subcommand {
None if command == self.name => {
debug!("App::try_get_matches_from_mut: no existing applet but matches name");
debug!(
"App::try_get_matches_from_mut: Setting bin_name to command name"
);
self.bin_name.get_or_insert(command);
debug!(
"App::try_get_matches_from_mut: Continuing with top-level parser."
);
return self._do_parse(&mut it);
}
_ => {
debug!(
"App::try_get_matches_from_mut: existing applet or no program name"
);
debug!("App::try_get_matches_from_mut: Reinserting command into arguments so subcommand parser matches it");
it.insert(&[&command]);
debug!("App::try_get_matches_from_mut: Clearing name and bin_name so that displayed command name starts with applet name");
self.name.clear();
self.bin_name = None;
return self._do_parse(&mut it);
}
};
debug!("App::try_get_matches_from_mut: Reinserting command into arguments so subcommand parser matches it");
it.insert(&[&command]);
debug!("App::try_get_matches_from_mut: Clearing name and bin_name so that displayed command name starts with applet name");
self.name.clear();
self.bin_name = None;
return self._do_parse(&mut it);
}
}
};
Expand Down
85 changes: 44 additions & 41 deletions src/build/app/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -637,10 +637,7 @@ pub enum AppSettings {
/// [`subcommands`]: crate::App::subcommand()
DeriveDisplayOrder,

/// Parse the bin name (argv[0]) as a subcommand
///
/// This adds a small performance penalty to startup
/// as it requires comparing the bin name against every subcommand name.
/// Strip directory path from argv[0] and use as an argument.
///
/// A "multicall" executable is a single executable
/// that contains a variety of applets,
Expand All @@ -658,10 +655,37 @@ pub enum AppSettings {
///
/// # Examples
///
/// Multicall applets are defined as subcommands
/// to an app which has the Multicall setting enabled.
/// `hostname` is an example of a multicall executable.
/// Both `hostname` and `dnsdomainname` are provided by the same executable
/// and which behaviour to use is based on the executable file name.
///
/// This is desirable when the executable has a primary purpose
/// but there is other related functionality that would be convenient to provide
/// and it is convenient for the code to implement it to be in the same executable.
///
/// The name of the app is essentially unused
/// and may be the same as the name of a subcommand.
///
/// The names of the immediate subcommands of the App
/// are matched against the basename of the first argument,
/// which is conventionally the path of the executable.
///
/// This does not allow the subcommand to be passed as the first non-path argument.
///
/// Busybox is a common example of a "multicall" executable
/// ```rust
/// # use clap::{App, AppSettings, ErrorKind};
/// let mut app = App::new("hostname")
/// .setting(AppSettings::Multicall)
/// .subcommand(App::new("hostname"))
/// .subcommand(App::new("dnsdomainname"));
/// let m = app.try_get_matches_from_mut(&["/usr/bin/hostname", "dnsdomainname"]);
/// assert!(m.is_err());
/// assert_eq!(m.unwrap_err().kind, ErrorKind::UnknownArgument);
/// let m = app.get_matches_from(&["/usr/bin/dnsdomainname"]);
/// assert_eq!(m.subcommand_name(), Some("dnsdomainname"));
/// ```
///
/// Busybox is another common example of a multicall executable
/// with a subcommmand for each applet that can be run directly,
/// e.g. with the `cat` applet being run by running `busybox cat`,
/// or with `cat` as a link to the `busybox` binary.
Expand All @@ -671,52 +695,32 @@ pub enum AppSettings {
/// e.g. to test the applet without installing it
/// or there may already be a command of that name installed.
///
/// To make an applet usable as both a multicall link and a subcommand
/// the subcommands must be defined both in the top-level App
/// and as subcommands of the "main" applet.
///
/// ```rust
/// # use clap::{App, AppSettings};
/// let mut app = App::new("busybox")
/// .setting(AppSettings::Multicall)
/// .subcommand(
/// App::new("busybox")
/// .subcommand(App::new("true"))
/// .subcommand(App::new("false")),
/// )
/// .subcommand(App::new("true"))
/// .subcommand(App::new("false"));
/// // When called from the executable's canonical name
/// // its applets can be matched as subcommands.
/// let m = app.try_get_matches_from_mut(&["busybox", "true"]).unwrap();
/// assert_eq!(m.subcommand_name(), Some("true"));
/// let m = app.try_get_matches_from_mut(&["/usr/bin/busybox", "true"]).unwrap();
/// assert_eq!(m.subcommand_name(), Some("busybox"));
/// assert_eq!(m.subcommand().unwrap().1.subcommand_name(), Some("true"));
/// // When called from a link named after an applet that applet is matched.
/// let m = app.get_matches_from(&["true"]);
/// let m = app.get_matches_from(&["/usr/bin/true"]);
/// assert_eq!(m.subcommand_name(), Some("true"));
/// ```
///
/// `hostname` is another example of a multicall executable.
/// It differs from busybox by not supporting running applets via subcommand
/// and is instead only runnable via links.
///
/// This is desirable when the executable has a primary purpose
/// rather than being a collection of varied applets,
/// so it is appropriate to name the executable after its purpose,
/// but there is other related functionality that would be convenient to provide
/// and it is convenient for the code to implement it to be in the same executable.
///
/// This behaviour can be opted-into
/// by naming a subcommand with the same as the program
/// as applet names take priority.
///
/// ```rust
/// # use clap::{App, AppSettings, ErrorKind};
/// let mut app = App::new("hostname")
/// .setting(AppSettings::Multicall)
/// .subcommand(App::new("hostname"))
/// .subcommand(App::new("dnsdomainname"));
/// let m = app.try_get_matches_from_mut(&["hostname", "dnsdomainname"]);
/// assert!(m.is_err());
/// assert_eq!(m.unwrap_err().kind, ErrorKind::UnknownArgument);
/// let m = app.get_matches_from(&["hostname"]);
/// assert_eq!(m.subcommand_name(), Some("hostname"));
/// ```
///
/// [`subcommands`]: crate::App::subcommand()
/// [`panic!`]: https://doc.rust-lang.org/std/macro.panic!.html
/// [`NoBinaryName`]: crate::AppSettings::NoBinaryName
/// [`try_get_matches_from_mut`]: crate::App::try_get_matches_from_mut()
#[cfg(feature = "unstable-multicall")]
Multicall,

Expand Down Expand Up @@ -892,7 +896,6 @@ pub enum AppSettings {
/// let cmds: Vec<&str> = m.values_of("cmd").unwrap().collect();
/// assert_eq!(cmds, ["command", "set"]);
/// ```
/// [`try_get_matches_from_mut`]: crate::App::try_get_matches_from_mut()
NoBinaryName,

/// Places the help string for all arguments on the line after the argument.
Expand Down
8 changes: 7 additions & 1 deletion tests/subcommands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -510,11 +510,17 @@ For more information try --help
fn busybox_like_multicall() {
let app = App::new("busybox")
.setting(AppSettings::Multicall)
.subcommand(
App::new("busybox")
.subcommand(App::new("true"))
.subcommand(App::new("false")),
)
.subcommand(App::new("true"))
.subcommand(App::new("false"));

let m = app.clone().get_matches_from(&["busybox", "true"]);
assert_eq!(m.subcommand_name(), Some("true"));
assert_eq!(m.subcommand_name(), Some("busybox"));
assert_eq!(m.subcommand().unwrap().1.subcommand_name(), Some("true"));

let m = app.clone().get_matches_from(&["true"]);
assert_eq!(m.subcommand_name(), Some("true"));
Expand Down

0 comments on commit 2fe7a89

Please sign in to comment.