Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Multicall executables as subcommands with a Multicall setting #2817

Merged
merged 13 commits into from Oct 16, 2021
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 10 additions & 1 deletion Cargo.toml
Expand Up @@ -81,6 +81,14 @@ lazy_static = "1"
version-sync = "0.9"
criterion = "0.3.2"

[[example]]
name = "busybox"
path = "examples/24a_multicall_busybox.rs"

[[example]]
name = "hostname"
path = "examples/24b_multicall_hostname.rs"

[features]
default = [
"std",
Expand Down Expand Up @@ -108,6 +116,7 @@ yaml = ["yaml-rust"]

# In-work features
unstable-replace = []
unstable-multicall = []

[profile.test]
opt-level = 1
Expand All @@ -117,7 +126,7 @@ lto = true
codegen-units = 1

[package.metadata.docs.rs]
features = ["yaml", "regex", "unstable-replace"]
features = ["yaml", "regex", "unstable-replace", "unstable-multicall"]
fishface60 marked this conversation as resolved.
Show resolved Hide resolved
targets = ["x86_64-unknown-linux-gnu"]

[workspace]
Expand Down
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -508,6 +508,7 @@ features = ["std", "suggestions", "color"]
These features are opt-in. But be wary that they can contain breaking changes between minor releases.

* **unstable-replace**: Enable [`App::replace`](https://github.com/clap-rs/clap/issues/2836)
* **unstable-multicall**: Enable [`AppSettings::Multicall`](https://github.com/clap-rs/clap/issues/2861)

### More Information

Expand Down
39 changes: 39 additions & 0 deletions examples/24a_multicall_busybox.rs
@@ -0,0 +1,39 @@
//! Example of a `busybox-style` multicall program
//!
//! 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,
//! 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.

use std::process::exit;

use clap::{App, AppSettings, Arg};

fn main() {
let mut app = App::new(env!("CARGO_CRATE_NAME"))
.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),
epage marked this conversation as resolved.
Show resolved Hide resolved
)
.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 {
unimplemented!("Make hardlinks to the executable here");
}

exit(match matches.subcommand_name() {
Some("true") => 0,
Some("false") => 1,
_ => 127,
})
}
23 changes: 23 additions & 0 deletions examples/24b_multicall_hostname.rs
@@ -0,0 +1,23 @@
//! Example of a `hostname-style` multicall program
//!
//! See the documentation for clap::AppSettings::Multicall for rationale.
//!
//! This example omits the implementation of displaying address config

use std::process::exit;

use clap::{App, AppSettings};

fn main() {
let app = App::new(env!("CARGO_CRATE_NAME"))
.setting(AppSettings::ArgRequiredElseHelp)
.setting(AppSettings::Multicall)
.subcommand(App::new("hostname").about("shot hostname part of FQDN"))
.subcommand(App::new("dnsdomainname").about("show domain name part of FQDN"));

match app.get_matches().subcommand_name() {
Some("hostname") => println!("www"),
Some("dnsdomainname") => println!("example.com"),
_ => exit(127),
}
}
19 changes: 18 additions & 1 deletion src/build/app/debug_asserts.rs
Expand Up @@ -345,8 +345,25 @@ 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);
#[cfg(feature = "unstable-multicall")]
checker!(Multicall conflicts NoBinaryName);
kbknapp marked this conversation as resolved.
Show resolved Hide resolved
}
48 changes: 48 additions & 0 deletions src/build/app/mod.rs
Expand Up @@ -1950,6 +1950,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 @@ -2132,6 +2133,53 @@ impl<'help> App<'help> {
T: Into<OsString> + Clone,
{
let mut it = Input::from(itr.into_iter());

#[cfg(feature = "unstable-multicall")]
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()) {
// Stop borrowing command so we can get another mut ref to it.
let command = command.to_owned();
debug!(
"App::try_get_matches_from_mut: Parsed command {} from argv",
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);
}
};
}
}
};
// 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
89 changes: 89 additions & 0 deletions src/build/app/settings.rs
Expand Up @@ -50,6 +50,8 @@ bitflags! {
const USE_LONG_FORMAT_FOR_HELP_SC = 1 << 42;
const INFER_LONG_ARGS = 1 << 43;
const IGNORE_ERRORS = 1 << 44;
#[cfg(feature = "unstable-multicall")]
const MULTICALL = 1 << 45;
}
}

Expand Down Expand Up @@ -106,6 +108,9 @@ impl_settings! { AppSettings, AppFlags,
=> Flags::HELP_REQUIRED,
Hidden("hidden")
=> Flags::HIDDEN,
#[cfg(feature = "unstable-multicall")]
Multicall("multicall")
=> Flags::MULTICALL,
NoAutoHelp("noautohelp")
=> Flags::NO_AUTO_HELP,
NoAutoVersion("noautoversion")
Expand Down Expand Up @@ -646,6 +651,89 @@ pub enum AppSettings {
/// [`subcommands`]: crate::App::subcommand()
DeriveDisplayOrder,

/// 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.
///
/// A "multicall" executable is a single executable
/// that contains a variety of applets,
/// and decides which applet to run based on the name of the file.
/// The executable can be called from different names by creating hard links
/// or symbolic links to it.
///
/// This is desirable when it is convenient to store code
/// for many programs in the same file,
/// such as deduplicating code across multiple programs
/// without loading a shared library at runtime.
///
/// 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.
///
/// Busybox is a 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.
///
/// This is desirable when the launcher program has additional options
/// or it is useful to run the applet without installing a symlink
/// e.g. to test the applet without installing it
/// or there may already be a command of that name installed.
///
/// ```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();
pksunkara marked this conversation as resolved.
Show resolved Hide resolved
/// 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"));
/// ```
///
/// `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.
///
/// ```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()
#[cfg(feature = "unstable-multicall")]
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 @@ -811,6 +899,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
14 changes: 9 additions & 5 deletions tests/examples.rs
Expand Up @@ -10,7 +10,7 @@ fn run_example<S: AsRef<str>>(name: S, args: &[&str]) -> Output {
"--example",
name.as_ref(),
"--features",
"yaml",
"yaml unstable-multicall",
"--",
];
all_args.extend_from_slice(args);
Expand All @@ -32,10 +32,14 @@ fn examples_are_functional() {
for path in example_paths {
example_count += 1;

let example_name = path
.file_stem()
.and_then(OsStr::to_str)
.expect("unable to determine example name");
let example_name = match path.file_name().and_then(OsStr::to_str) {
Some("24a_multicall_busybox.rs") => "busybox".into(),
Some("24b_multicall_hostname.rs") => "hostname".into(),
_ => path
.file_stem()
.and_then(OsStr::to_str)
.expect("unable to determine example name"),
};

let help_output = run_example(example_name, &["--help"]);
assert!(
Expand Down
46 changes: 46 additions & 0 deletions tests/subcommands.rs
Expand Up @@ -507,3 +507,49 @@ For more information try --help
true
));
}

#[cfg(feature = "unstable-multicall")]
#[test]
fn busybox_like_multicall() {
let app = App::new("busybox")
.setting(AppSettings::Multicall)
.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().try_get_matches_from(&["a.out"]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::UnknownArgument);
}

#[cfg(feature = "unstable-multicall")]
#[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().try_get_matches_from(&["a.out"]);
assert!(m.is_err());
assert_eq!(m.unwrap_err().kind, ErrorKind::UnknownArgument);

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);
}