mirror of
https://github.com/clap-rs/clap
synced 2024-09-20 14:31:58 +00:00
Merge #2817
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:
commit
b835ce9061
13 changed files with 309 additions and 17 deletions
4
.github/workflows/ci-pr.yml
vendored
4
.github/workflows/ci-pr.yml
vendored
|
@ -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
|
||||
|
|
14
.github/workflows/ci.yml
vendored
14
.github/workflows/ci.yml
vendored
|
@ -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"
|
||||
|
|
2
.github/workflows/coverage.yml
vendored
2
.github/workflows/coverage.yml
vendored
|
@ -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:
|
||||
|
|
2
.github/workflows/lint.yml
vendored
2
.github/workflows/lint.yml
vendored
|
@ -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:
|
||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
41
examples/24a_multicall_busybox.rs
Normal file
41
examples/24a_multicall_busybox.rs
Normal 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,
|
||||
})
|
||||
}
|
25
examples/24b_multicall_hostname.rs
Normal file
25
examples/24b_multicall_hostname.rs
Normal 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),
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue