Skip to content

Commit

Permalink
Add Multicall setting
Browse files Browse the repository at this point in the history
If the AppSettings::Multicall setting is given
then argv0 is parsed as the first subcommand argument
or parsed as normal with any other name.
  • Loading branch information
fishface60 committed Oct 5, 2021
1 parent 236cf58 commit fa1d10c
Show file tree
Hide file tree
Showing 7 changed files with 296 additions and 1 deletion.
60 changes: 60 additions & 0 deletions 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();
}
}
}
}
56 changes: 56 additions & 0 deletions 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,
})
}
18 changes: 18 additions & 0 deletions 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),
}
}
18 changes: 17 additions & 1 deletion src/build/app/debug_asserts.rs
Expand Up @@ -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);
}
25 changes: 25 additions & 0 deletions src/build/app/mod.rs
Expand Up @@ -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())
Expand Down Expand Up @@ -2127,6 +2128,30 @@ impl<'help> App<'help> {
T: Into<OsString> + 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
Expand Down
56 changes: 56 additions & 0 deletions src/build/app/settings.rs
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
64 changes: 64 additions & 0 deletions tests/subcommands.rs
Expand Up @@ -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);
}

0 comments on commit fa1d10c

Please sign in to comment.