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 d181174
Show file tree
Hide file tree
Showing 6 changed files with 345 additions and 0 deletions.
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();
}
}
}
}
60 changes: 60 additions & 0 deletions 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<String> = 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,
})
}
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),
}
}
56 changes: 56 additions & 0 deletions src/build/app/mod.rs
Expand Up @@ -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
Expand All @@ -1945,13 +1949,18 @@ 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())
}

/// 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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
///
Expand Down Expand Up @@ -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<I, T>(&mut self, itr: I) -> ClapResult<ArgMatches>
where
I: IntoIterator<Item = T>,
T: Into<OsString> + 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
Expand Down
87 changes: 87 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,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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down

0 comments on commit d181174

Please sign in to comment.