From d1811747700a28498e2bf0be81a8574944cf11de Mon Sep 17 00:00:00 2001 From: Richard Maw Date: Tue, 14 Sep 2021 21:38:41 +0100 Subject: [PATCH] Add Multicall setting If the AppSettings::Multicall setting is given then argv0 is parsed as the first subcommand argument or parsed as normal with any other name. --- clap_derive/examples/multicall.rs | 60 +++++++++++++++++++++ examples/24a_multicall_busybox.rs | 60 +++++++++++++++++++++ examples/24b_multicall_hostname.rs | 18 +++++++ src/build/app/mod.rs | 56 +++++++++++++++++++ src/build/app/settings.rs | 87 ++++++++++++++++++++++++++++++ tests/subcommands.rs | 64 ++++++++++++++++++++++ 6 files changed, 345 insertions(+) create mode 100644 clap_derive/examples/multicall.rs create mode 100644 examples/24a_multicall_busybox.rs create mode 100644 examples/24b_multicall_hostname.rs diff --git a/clap_derive/examples/multicall.rs b/clap_derive/examples/multicall.rs new file mode 100644 index 000000000000..c464062c8acd --- /dev/null +++ b/clap_derive/examples/multicall.rs @@ -0,0 +1,60 @@ +use std::{ + env::args_os, + fs::{hard_link, read_link}, + path::{Path, PathBuf}, + process::exit, +}; + +use clap::{Clap, IntoApp}; + +#[derive(Clap, Debug)] +enum Applets { + True, + False, + Install { + #[clap(default_value = "/usr/local/bin")] + path: PathBuf, + }, +} + +#[derive(Clap, Debug)] +#[clap(setting = clap::AppSettings::Multicall)] +struct Args { + #[clap(subcommand)] + applet: Applets, +} + +fn main() { + match Args::parse().applet { + Applets::True => exit(0), + Applets::False => exit(1), + Applets::Install { path } => { + let app = Args::into_app(); + let applets: Vec<_> = app.get_subcommands().map(|c| c.get_name()).collect(); + let exec_path = read_link("/proc/self/exe") + .ok() + .or_else(|| { + args_os().next().and_then(|s| { + let p: &Path = s.as_ref(); + if p.is_absolute() { + Some(PathBuf::from(s)) + } else { + None + } + }) + }) + .expect( + "Should be able to read /proc/self/exe or argv0 should be present and absolute", + ); + let mut dest = PathBuf::from(path); + for applet in applets { + if applet == "install" { + continue; + } + dest.push(applet); + hard_link(&exec_path, &dest).expect("Should be able to hardlink"); + dest.pop(); + } + } + } +} diff --git a/examples/24a_multicall_busybox.rs b/examples/24a_multicall_busybox.rs new file mode 100644 index 000000000000..076ba679d25c --- /dev/null +++ b/examples/24a_multicall_busybox.rs @@ -0,0 +1,60 @@ +use std::{ + env::args_os, + fs::{hard_link, read_link}, + path::{Path, PathBuf}, + process::exit, +}; + +use clap::{App, AppSettings, Arg}; + +fn main() { + let app = App::new("busybox") + .setting(AppSettings::ArgRequiredElseHelp) + .setting(AppSettings::Multicall) + .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")); + let applets: Vec = app + .get_subcommands() + .map(|c| c.get_name().to_owned()) + .collect(); + let matches = app.get_matches(); + if matches.occurrences_of("install") > 0 { + let exec_path = read_link("/proc/self/exe") + .ok() + .or_else(|| { + args_os().next().and_then(|s| { + let p: &Path = s.as_ref(); + if p.is_absolute() { + Some(PathBuf::from(s)) + } else { + None + } + }) + }) + .expect( + "Should be able to read /proc/self/exe or argv0 should be present and absolute", + ); + let mut dest = PathBuf::from(matches.value_of("install").unwrap()); + for applet in applets { + dest.push(applet); + hard_link(&exec_path, &dest).expect("Should be able to hardlink"); + dest.pop(); + } + exit(0); + } + + exit(match matches.subcommand_name() { + Some("true") => 0, + Some("false") => 1, + _ => 127, + }) +} diff --git a/examples/24b_multicall_hostname.rs b/examples/24b_multicall_hostname.rs new file mode 100644 index 000000000000..6b4ee3244672 --- /dev/null +++ b/examples/24b_multicall_hostname.rs @@ -0,0 +1,18 @@ +use std::process::exit; + +use clap::{App, AppSettings}; + +fn main() { + let matches = App::new("hostname") + .setting(AppSettings::Multicall) + .subcommand(App::new("hostname").about("show hostname")) + .subcommand(App::new("dnsdomainname").about("show domain")) + .get_matches(); + + // If hardlinked as a different name + match matches.subcommand_name() { + Some("hostname") => println!("www"), + Some("dnsdomainname") => println!("example.com"), + _ => exit(127), + } +} diff --git a/src/build/app/mod.rs b/src/build/app/mod.rs index 7698be3dc968..b09d74f311e6 100644 --- a/src/build/app/mod.rs +++ b/src/build/app/mod.rs @@ -1936,6 +1936,10 @@ impl<'help> App<'help> { /// provided arguments from [`env::args_os`] in order to allow for invalid UTF-8 code points, /// which are legal on many platforms. /// + /// # Panics + /// + /// See [`App::try_get_matches_from_mut`]. + /// /// # Examples /// /// ```no_run @@ -1945,6 +1949,7 @@ impl<'help> App<'help> { /// .get_matches(); /// ``` /// [`env::args_os`]: std::env::args_os() + /// [`App::try_get_matches_from_mut`]: App::try_get_matches_from_mut() #[inline] pub fn get_matches(self) -> ArgMatches { self.get_matches_from(&mut env::args_os()) @@ -1952,6 +1957,10 @@ impl<'help> App<'help> { /// Starts the parsing process, just like [`App::get_matches`] but doesn't consume the `App`. /// + /// # Panics + /// + /// See [`App::try_get_matches_from_mut`]. + /// /// # Examples /// /// ```no_run @@ -1993,6 +2002,10 @@ impl<'help> App<'help> { /// [`ErrorKind::DisplayHelp`] or [`ErrorKind::DisplayVersion`] respectively. You must call /// [`Error::exit`] or perform a [`std::process::exit`]. /// + /// # Panics + /// + /// See [`App::try_get_matches_from_mut`]. + /// /// # Examples /// /// ```no_run @@ -2023,6 +2036,10 @@ impl<'help> App<'help> { /// **NOTE:** The first argument will be parsed as the binary name unless /// [`AppSettings::NoBinaryName`] is used. /// + /// # Panics + /// + /// See [`App::try_get_matches_from_mut`]. + /// /// # Examples /// /// ```no_run @@ -2071,6 +2088,10 @@ impl<'help> App<'help> { /// or [`ErrorKind::DisplayVersion`] respectively. You must call [`Error::exit`] or /// perform a [`std::process::exit`] yourself. /// + /// # Panics + /// + /// See [`App::try_get_matches_from_mut`]. + /// /// **NOTE:** The first argument will be parsed as the binary name unless /// [`AppSettings::NoBinaryName`] is used. /// @@ -2121,12 +2142,47 @@ impl<'help> App<'help> { /// .unwrap_or_else(|e| e.exit()); /// ``` /// [`App::try_get_matches_from`]: App::try_get_matches_from() + /// + /// ### Panics + /// + /// If both [`AppSettings::Multicall`] + /// and [`AppSettings::NoBinaryName`] are used. pub fn try_get_matches_from_mut(&mut self, itr: I) -> ClapResult where I: IntoIterator, T: Into + Clone, { + if self.settings.is_set(AppSettings::NoBinaryName) + && self.settings.is_set(AppSettings::Multicall) + { + panic!("NoBinaryName can't be used with Multicall"); + } + let mut it = Input::from(itr.into_iter()); + + 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()) { + match self + .subcommands + .iter_mut() + .find(|subcommand| subcommand.aliases_to(command)) + { + Some(subcommand) => { + self.name.clear(); + self.bin_name = None; + it.insert(&[subcommand.get_name()]); + return self._do_parse(&mut it); + } + _ => { + self.bin_name.get_or_insert_with(|| command.to_owned()); + return self._do_parse(&mut it); + } + }; + } + } + }; // Get the name of the program (argument 1 of env::args()) and determine the // actual file // that was used to execute the program. This is because a program called diff --git a/src/build/app/settings.rs b/src/build/app/settings.rs index 48569b95483f..b23cfe16c03e 100644 --- a/src/build/app/settings.rs +++ b/src/build/app/settings.rs @@ -50,6 +50,7 @@ bitflags! { const USE_LONG_FORMAT_FOR_HELP_SC = 1 << 42; const INFER_LONG_ARGS = 1 << 43; const IGNORE_ERRORS = 1 << 44; + const MULTICALL = 1 << 45; } } @@ -108,6 +109,8 @@ impl_settings! { AppSettings, AppFlags, => Flags::HELP_REQUIRED, Hidden("hidden") => Flags::HIDDEN, + Multicall("multicall") + => Flags::MULTICALL, NoAutoHelp("noautohelp") => Flags::NO_AUTO_HELP, NoAutoVersion("noautoversion") @@ -689,6 +692,83 @@ pub enum AppSettings { /// [`subcommands`]: crate::App::subcommand() DeriveDisplayOrder, + /// Specifies that the parser should match first argument as [`subcommands`], + /// treat any matches as if the subcommand had been passed, + /// and print arguments like the [`subcommands`] are the top-level commands, + /// but if none match parse as usual. + /// + /// This is desired behaviour for "multicall" executables such as busybox + /// which dispatch to which applet to run + /// based on the name the executable has been linked as. + /// + /// # Panics + /// + /// If this option is combined with [`NoBinaryName`] then + /// [`try_get_matches_from_mut`] will [`panic!`]. + /// + /// # Examples + /// + /// It does not make sense to combine NoBinaryName with Multicall + /// so it will panic. + /// + /// ```should_panic + /// # use clap::{App, AppSettings}; + /// let _ = App::new("busybox") + /// .setting(AppSettings::NoBinaryName) + /// .setting(AppSettings::Multicall) + /// .subcommand(App::new("true")) + /// .subcommand(App::new("false")) + /// .get_matches_from(&["true"]); + /// ``` + /// + /// When the executable is run from a link named after an applet + /// that applet is matched. + /// + /// ```rust + /// # use clap::{App, AppSettings}; + /// let app = App::new("busybox") + /// .setting(AppSettings::Multicall) + /// .subcommand(App::new("true")) + /// .subcommand(App::new("false")); + /// let m = app.get_matches_from(&["true"]); + /// assert_eq!(m.subcommand_name(), Some("true")); + /// ``` + /// + /// When run from a link that is not named after an applet + /// the applet may be provided as a subcommand argument + /// + /// ```rust + /// # use clap::{App, AppSettings}; + /// # let app = App::new("busybox") + /// # .setting(AppSettings::Multicall) + /// # .subcommand(App::new("true")); + /// let m = app.get_matches_from(&["busybox", "true"]); + /// assert_eq!(m.subcommand_name(), Some("true")); + /// ``` + /// + /// If the name of an applet is the name of the command, + /// the applet's name is matched + /// and subcommands may not be provided to select another applet. + /// + /// ```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() + Multicall, + /// Specifies to use the version of the current command for all [`subcommands`]. /// /// Defaults to `false`; subcommands have independent version strings from their parents. @@ -842,6 +922,11 @@ pub enum AppSettings { /// This is normally the case when using a "daemon" style mode, or an interactive CLI where /// one would not normally type the binary or program name for each command. /// + /// # Panics + /// + /// If this option is combined with [`Multicall`] then + /// [`try_get_matches_from_mut`] will [`panic!`]. + /// /// # Examples /// /// ```rust @@ -854,6 +939,8 @@ pub enum AppSettings { /// let cmds: Vec<&str> = m.values_of("cmd").unwrap().collect(); /// assert_eq!(cmds, ["command", "set"]); /// ``` + /// [`Multicall`]: crate::AppSettings::Multicall + /// [`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. diff --git a/tests/subcommands.rs b/tests/subcommands.rs index e447b503576f..0adb649705e7 100644 --- a/tests/subcommands.rs +++ b/tests/subcommands.rs @@ -506,3 +506,67 @@ For more information try --help true )); } + +#[test] +fn busybox_like_multicall() { + let app = App::new("busybox") + .setting(AppSettings::Multicall) + .arg( + Arg::new("install") + .long("install") + .exclusive(true) + .takes_value(true) + .default_missing_value("/usr/local/bin"), + ) + .subcommand(App::new("true")) + .subcommand(App::new("false")); + + let m = app.clone().get_matches_from(&["busybox", "true"]); + assert_eq!(m.subcommand_name(), Some("true")); + + let m = app.clone().get_matches_from(&["true"]); + assert_eq!(m.subcommand_name(), Some("true")); + + let m = app.clone().get_matches_from(&["busybox", "--install"]); + assert_eq!(m.subcommand_name(), None); + assert_eq!(m.occurrences_of("install"), 1); + + let m = app.clone().get_matches_from(&["a.out", "--install"]); + assert_eq!(m.subcommand_name(), None); + assert_eq!(m.occurrences_of("install"), 1); + + let m = app.clone().get_matches_from(&["a.out", "true"]); + assert_eq!(m.subcommand_name(), Some("true")); + + let m = app.try_get_matches_from(&["true", "--install"]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind, ErrorKind::UnknownArgument); +} + +#[test] +fn hostname_like_multicall() { + let mut app = App::new("hostname") + .setting(AppSettings::Multicall) + .subcommand(App::new("hostname")) + .subcommand(App::new("dnsdomainname")); + + let m = app.clone().get_matches_from(&["hostname"]); + assert_eq!(m.subcommand_name(), Some("hostname")); + + let m = app.clone().get_matches_from(&["dnsdomainname"]); + assert_eq!(m.subcommand_name(), Some("dnsdomainname")); + + let m = app.clone().get_matches_from(&["a.out", "hostname"]); + assert_eq!(m.subcommand_name(), Some("hostname")); + + let m = app.clone().get_matches_from(&["a.out", "dnsdomainname"]); + assert_eq!(m.subcommand_name(), Some("dnsdomainname")); + + let m = app.try_get_matches_from_mut(&["hostname", "hostname"]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind, ErrorKind::UnknownArgument); + + let m = app.try_get_matches_from(&["hostname", "dnsdomainname"]); + assert!(m.is_err()); + assert_eq!(m.unwrap_err().kind, ErrorKind::UnknownArgument); +}