2817: Add support for Multicall executables as subcommands with a Multicall setting r=pksunkara a=fishface60



Co-authored-by: Richard Maw <richard.maw@gmail.com>
This commit is contained in:
bors[bot] 2021-10-16 00:32:52 +00:00 committed by GitHub
commit b835ce9061
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 309 additions and 17 deletions

View file

@ -41,7 +41,7 @@ jobs:
test-full:
name: Tests (Full)
env:
FLAGS: --features 'wrap_help yaml regex unstable-replace'
FLAGS: --features 'wrap_help yaml regex unstable-replace unstable-multicall'
strategy:
fail-fast: false
matrix:
@ -80,7 +80,7 @@ jobs:
- name: Default features
run: cargo check --all-targets
- name: All features + Debug
run: cargo check --all-targets --features "wrap_help yaml regex unstable-replace debug"
run: cargo check --all-targets --features "wrap_help yaml regex unstable-replace unstable-multicall debug"
- name: No features
run: cargo check --all-targets --no-default-features --features "std cargo"
- name: UI Tests

View file

@ -97,19 +97,19 @@ jobs:
if: matrix.features == 'all'
with:
command: test
args: --target ${{ matrix.target }} --features "wrap_help yaml regex unstable-replace"
args: --target ${{ matrix.target }} --features "wrap_help yaml regex unstable-replace unstable-multicall"
- name: Check debug
uses: actions-rs/cargo@v1
if: matrix.features == 'all'
with:
command: check
args: --target ${{ matrix.target }} --features "wrap_help yaml regex unstable-replace debug"
args: --target ${{ matrix.target }} --features "wrap_help yaml regex unstable-replace unstable-multicall debug"
- name: Test release
uses: actions-rs/cargo@v1
if: matrix.features == 'release'
with:
command: test
args: --target ${{ matrix.target }} --features "wrap_help yaml regex unstable-replace" --release
args: --target ${{ matrix.target }} --features "wrap_help yaml regex unstable-replace unstable-multicall" --release
nightly:
name: Nightly Tests
strategy:
@ -139,19 +139,19 @@ jobs:
if: matrix.features == 'all'
with:
command: test
args: --features "wrap_help yaml regex unstable-replace"
args: --features "wrap_help yaml regex unstable-replace unstable-multicall"
- name: Check debug
uses: actions-rs/cargo@v1
if: matrix.features == 'all'
with:
command: check
args: --features "wrap_help yaml regex unstable-replace debug"
args: --features "wrap_help yaml regex unstable-replace unstable-multicall debug"
- name: Test release
uses: actions-rs/cargo@v1
if: matrix.features == 'release'
with:
command: test
args: --features "wrap_help yaml regex unstable-replace" --release
args: --features "wrap_help yaml regex unstable-replace unstable-multicall" --release
wasm:
name: Wasm Check
runs-on: ubuntu-latest
@ -172,4 +172,4 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: check
args: --target ${{ matrix.target }} --features "yaml regex unstable-replace"
args: --target ${{ matrix.target }} --features "yaml regex unstable-replace unstable-multicall"

View file

@ -31,7 +31,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: llvm-cov
args: --features "wrap_help yaml regex unstable-replace" --lcov --output-path lcov.info
args: --features "wrap_help yaml regex unstable-replace unstable-multicall" --lcov --output-path lcov.info
- name: Coveralls
uses: coverallsapp/github-action@master
with:

View file

@ -32,7 +32,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
args: --features "wrap_help yaml regex unstable-replace" -- -D warnings
args: --features "wrap_help yaml regex unstable-replace unstable-multicall" -- -D warnings
- name: Format check
uses: actions-rs/cargo@v1
with:

View file

@ -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",
@ -108,6 +116,7 @@ yaml = ["yaml-rust"]
# In-work features
unstable-replace = []
unstable-multicall = []
[profile.test]
opt-level = 1
@ -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"]
targets = ["x86_64-unknown-linux-gnu"]
[workspace]

View file

@ -478,6 +478,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

View file

@ -0,0 +1,41 @@
//! 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 app = App::new(env!("CARGO_CRATE_NAME"))
.setting(AppSettings::ArgRequiredElseHelp)
.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"));
#[cfg(feature = "unstable-multicall")]
let app = app.setting(AppSettings::Multicall);
let matches = app.get_matches();
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,
})
}

View file

@ -0,0 +1,25 @@
//! 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)
.subcommand(App::new("hostname").about("shot hostname part of FQDN"))
.subcommand(App::new("dnsdomainname").about("show domain name part of FQDN"));
#[cfg(feature = "unstable-multicall")]
let app = app.setting(AppSettings::Multicall);
match app.get_matches().subcommand_name() {
Some("hostname") => println!("www"),
Some("dnsdomainname") => println!("example.com"),
_ => exit(127),
}
}

View file

@ -358,8 +358,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);
}

View file

@ -1954,6 +1954,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())
@ -2136,6 +2137,55 @@ 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

View file

@ -45,6 +45,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;
}
}
@ -95,6 +97,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")
@ -577,6 +582,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();
/// 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.
@ -742,6 +830,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.

View file

@ -32,10 +32,24 @@ 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") => {
#[cfg(not(feature = "unstable-multicall"))]
continue;
#[allow(unreachable_code)]
"busybox".into()
}
Some("24b_multicall_hostname.rs") => {
#[cfg(not(feature = "unstable-multicall"))]
continue;
#[allow(unreachable_code)]
"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!(

View file

@ -504,3 +504,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);
}