diff --git a/clap_derive/examples/multicall.rs b/clap_derive/examples/multicall.rs new file mode 100644 index 000000000000..87a527c36b9c --- /dev/null +++ b/clap_derive/examples/multicall.rs @@ -0,0 +1,67 @@ +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, +} + +#[derive(Clap, Debug)] +#[clap(setting = clap::AppSettings::Multicall)] +struct Args { + #[clap( + long, + default_missing_value = "/usr/local/bin", + exclusive = true, + use_delimiter = false + )] + install: Option>, + #[clap(subcommand)] + applet: Option, +} + +fn main() { + let args = Args::parse(); + if let Some(path) = args.install { + let path = path.expect("clap Option>'s inner Option should always be present if given default_missing_value"); + 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(); + } + exit(0); + } + match args.applet { + Some(Applets::True) => exit(0), + Some(Applets::False) => exit(1), + _ => Args::into_app().print_help().unwrap(), + } +} diff --git a/examples/24a_multicall_busybox.rs b/examples/24a_multicall_busybox.rs new file mode 100644 index 000000000000..0dbfa1f2fd0d --- /dev/null +++ b/examples/24a_multicall_busybox.rs @@ -0,0 +1,56 @@ +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 matches = app.get_matches_mut(); + 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 app.get_subcommands().map(|c| c.get_name()) { + 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/debug_asserts.rs b/src/build/app/debug_asserts.rs index 38ac11ea531b..8910919ef338 100644 --- a/src/build/app/debug_asserts.rs +++ b/src/build/app/debug_asserts.rs @@ -321,8 +321,24 @@ fn assert_app_flags(app: &App) { panic!("{}", s) } } - } + }; + ($a:ident conflicts $($b:ident)|+) => { + if app.is_set($a) { + let mut s = String::new(); + + $( + if app.is_set($b) { + s.push_str(&format!("\nAppSettings::{} conflicts with AppSettings::{}.\n", std::stringify!($b), std::stringify!($a))); + } + )+ + + if !s.is_empty() { + panic!("{}", s) + } + } + }; } checker!(AllowInvalidUtf8ForExternalSubcommands requires AllowExternalSubcommands); + checker!(Multicall conflicts NoBinaryName); } diff --git a/src/build/app/mod.rs b/src/build/app/mod.rs index 7698be3dc968..c969987e9d91 100644 --- a/src/build/app/mod.rs +++ b/src/build/app/mod.rs @@ -1945,6 +1945,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()) @@ -2127,6 +2128,30 @@ impl<'help> App<'help> { T: Into + Clone, { 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..3575136ab067 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,58 @@ pub enum AppSettings { /// [`subcommands`]: crate::App::subcommand() DeriveDisplayOrder, + /// Parse the bin name (argv[0]) as a subcommand + /// + /// Busybox is a common example of a "multicall" executable + /// where the command `cat` is a link to the `busybox` bin + /// and, when `cat` is run, `busybox` acts is if you ran `busybox cat`. + /// + /// Multicall can't be used with [`NoBinaryName`] since they interpret + /// the command name in incompatible ways. + /// + /// # Examples + /// + /// Multicall applets are defined as subcommands + /// to an app which has the Multicall setting enabled. + /// + /// ```rust + /// # use clap::{App, AppSettings}; + /// let mut app = App::new("busybox") + /// .setting(AppSettings::Multicall) + /// .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")); + /// // When called from a link named after an applet that applet is matched. + /// let m = app.get_matches_from(&["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. @@ -854,6 +909,7 @@ 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. 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); +}