From e3d355fa857defcf099387d566cc36cdc8094cc7 Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Wed, 17 Nov 2021 20:20:52 +0000 Subject: [PATCH 1/3] feat: Make Multicall just strip dir from argv0 --- examples/multicall_busybox.md | 4 +- examples/multicall_busybox.rs | 53 +++++++++++-------- src/build/app/mod.rs | 39 +++----------- src/build/app/settings.rs | 95 ++++++++++++++++++++--------------- tests/builder/subcommands.rs | 10 ++-- 5 files changed, 102 insertions(+), 99 deletions(-) diff --git a/examples/multicall_busybox.md b/examples/multicall_busybox.md index c9dd47fefcc..cb5de990f95 100644 --- a/examples/multicall_busybox.md +++ b/examples/multicall_busybox.md @@ -29,13 +29,13 @@ $ busybox busybox USAGE: - busybox[EXE] [OPTIONS] [SUBCOMMAND] + busybox[EXE] [OPTIONS] [APPLET] OPTIONS: -h, --help Print help information --install Install hardlinks for all subcommands in path -SUBCOMMANDS: +APPLETS: false does nothing unsuccessfully help Print this message or the help of the given subcommand(s) true does nothing successfully diff --git a/examples/multicall_busybox.rs b/examples/multicall_busybox.rs index c64291064ea..9f07aef195a 100644 --- a/examples/multicall_busybox.rs +++ b/examples/multicall_busybox.rs @@ -2,32 +2,45 @@ use std::process::exit; use clap::{App, AppSettings, Arg}; +fn applet_commands() -> [App<'static>; 2] { + [ + App::new("true").about("does nothing successfully"), + App::new("false").about("does nothing unsuccessfully"), + ] +} + fn main() { let app = App::new(env!("CARGO_CRATE_NAME")) - .setting(AppSettings::ArgRequiredElseHelp) - .subcommand_value_name("APPLET") - .subcommand_help_heading("APPLETS") - .arg( - Arg::new("install") - .long("install") - .help("Install hardlinks for all subcommands in path") - .exclusive(true) - .takes_value(true) - .default_missing_value("/usr/local/bin") - .use_delimiter(false), + .setting(AppSettings::Multicall) + .subcommand( + App::new("busybox") + .setting(AppSettings::ArgRequiredElseHelp) + .subcommand_value_name("APPLET") + .subcommand_help_heading("APPLETS") + .arg( + Arg::new("install") + .long("install") + .help("Install hardlinks for all subcommands in path") + .exclusive(true) + .takes_value(true) + .default_missing_value("/usr/local/bin") + .use_delimiter(false), + ) + .subcommands(applet_commands()), ) - .subcommand(App::new("true").about("does nothing successfully")) - .subcommand(App::new("false").about("does nothing unsuccessfully")); + .subcommands(applet_commands()); - let app = app.setting(AppSettings::Multicall); let matches = app.get_matches(); - if matches.occurrences_of("install") > 0 { - unimplemented!("Make hardlinks to the executable here"); + let mut subcommand = matches.subcommand(); + if let Some(("busybox", cmd)) = subcommand { + if cmd.occurrences_of("install") > 0 { + unimplemented!("Make hardlinks to the executable here"); + } + subcommand = cmd.subcommand(); } - - match matches.subcommand_name() { - Some("true") => exit(0), - Some("false") => exit(1), + match subcommand { + Some(("false", _)) => exit(1), + Some(("true", _)) => exit(0), _ => unreachable!("parser should ensure only valid subcommand names are used"), } } diff --git a/src/build/app/mod.rs b/src/build/app/mod.rs index b6337cb9d00..7b53502c185 100644 --- a/src/build/app/mod.rs +++ b/src/build/app/mod.rs @@ -615,39 +615,12 @@ impl<'help> App<'help> { 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); } } }; diff --git a/src/build/app/settings.rs b/src/build/app/settings.rs index e2fa1f5ba8d..d9ae9fb1e36 100644 --- a/src/build/app/settings.rs +++ b/src/build/app/settings.rs @@ -390,10 +390,7 @@ pub enum AppSettings { /// [`ErrorKind::UnknownArgument`]: crate::ErrorKind::UnknownArgument AllowExternalSubcommands, - /// 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, @@ -411,10 +408,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. + /// + /// ```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 a common example of a "multicall" executable + /// 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. @@ -424,52 +448,41 @@ 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}; + /// fn applet_commands() -> [App<'static>; 2] { + /// [App::new("true"), App::new("false")] + /// } /// let mut app = App::new("busybox") /// .setting(AppSettings::Multicall) - /// .subcommand(App::new("true")) - /// .subcommand(App::new("false")); + /// .subcommand( + /// App::new("busybox") + /// .subcommand_value_name("APPLET") + /// .subcommand_help_heading("APPLETS") + /// .subcommands(applet_commands()), + /// ) + /// .subcommands(applet_commands()); /// // 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. + /// **NOTE:** Applets are slightly semantically different from subcommands, + /// so it's recommended to use [`App::subcommand_help_heading`] and + /// [`App::subcommand_value_name`] to change the descriptive text as above. /// - /// ```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() + /// [`App::subcommand_value_name`]: crate::App::subcommand_value_name + /// [`App::subcommand_help_heading`]: crate::App::subcommand_help_heading #[cfg(feature = "unstable-multicall")] Multicall, diff --git a/tests/builder/subcommands.rs b/tests/builder/subcommands.rs index 015fc7e30af..467c24145e1 100644 --- a/tests/builder/subcommands.rs +++ b/tests/builder/subcommands.rs @@ -511,13 +511,17 @@ For more information try --help #[cfg(feature = "unstable-multicall")] #[test] fn busybox_like_multicall() { + fn applet_commands() -> [App<'static>; 2] { + [App::new("true"), App::new("false")] + } let app = App::new("busybox") .setting(AppSettings::Multicall) - .subcommand(App::new("true")) - .subcommand(App::new("false")); + .subcommand(App::new("busybox").subcommands(applet_commands())) + .subcommands(applet_commands()); 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")); From 17c64ef42b9655ba591f297f8d7ba59833d3958c Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Sat, 11 Dec 2021 17:18:03 +0000 Subject: [PATCH 2/3] fix: Example typo --- examples/multicall_busybox.md | 2 +- examples/multicall_hostname.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/multicall_busybox.md b/examples/multicall_busybox.md index cb5de990f95..2bcd67e1e77 100644 --- a/examples/multicall_busybox.md +++ b/examples/multicall_busybox.md @@ -12,7 +12,7 @@ $ busybox true $ busybox false ? 1 ``` -*Note: without the links setup, we can't demonostrate the multicall behavior* +*Note: without the links setup, we can't demonstrate the multicall behavior* But includes the `--install` option as an example of why it can be useful for the main program to take arguments that aren't applet subcommands. diff --git a/examples/multicall_hostname.md b/examples/multicall_hostname.md index 3f078b3ddc1..e3707987505 100644 --- a/examples/multicall_hostname.md +++ b/examples/multicall_hostname.md @@ -10,4 +10,4 @@ This example omits the implementation of displaying address config $ hostname www ``` -*Note: without the links setup, we can't demonostrate the multicall behavior* +*Note: without the links setup, we can't demonstrate the multicall behavior* From 38b9645bed66a6db6dfbcd9a80192132f6f725fb Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Thu, 9 Dec 2021 23:05:31 +0000 Subject: [PATCH 3/3] fix: Windows Multicall support The executable suffix is unconditionally stripped off the file path so that the file name matches subcommands names without having to add the EXE suffix on different platforms. --- examples/multicall_busybox.md | 8 ++++---- examples/multicall_hostname.md | 2 +- src/build/app/mod.rs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/multicall_busybox.md b/examples/multicall_busybox.md index 2bcd67e1e77..fc49a85ef5e 100644 --- a/examples/multicall_busybox.md +++ b/examples/multicall_busybox.md @@ -6,7 +6,7 @@ See the documentation for clap::AppSettings::Multicall for rationale. This example omits every command except true and false, which are the most trivial to implement, -```bash,ignore +```bash $ busybox true ? 0 $ busybox false @@ -16,20 +16,20 @@ $ busybox false But includes the `--install` option as an example of why it can be useful for the main program to take arguments that aren't applet subcommands. -```bash,ignore +```bash $ busybox --install ? failed ... ``` Though users must pass something: -```bash,ignore +```bash $ busybox ? failed busybox USAGE: - busybox[EXE] [OPTIONS] [APPLET] + busybox [OPTIONS] [APPLET] OPTIONS: -h, --help Print help information diff --git a/examples/multicall_hostname.md b/examples/multicall_hostname.md index e3707987505..cdef7ee70a7 100644 --- a/examples/multicall_hostname.md +++ b/examples/multicall_hostname.md @@ -6,7 +6,7 @@ See the documentation for clap::AppSettings::Multicall for rationale. This example omits the implementation of displaying address config -```bash,ignore +```bash $ hostname www ``` diff --git a/src/build/app/mod.rs b/src/build/app/mod.rs index 7b53502c185..c58e60d8c50 100644 --- a/src/build/app/mod.rs +++ b/src/build/app/mod.rs @@ -607,7 +607,7 @@ 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!(