pub use vec1;
use clap::{
parser::{RawValues, ValueSource, ValuesRef},
ArgMatches, Command,
};
use snafu::{ResultExt, Snafu};
use std::{any::Any, collections::HashMap, error::Error, iter::Flatten, vec::IntoIter};
use vec1::Vec1;
pub trait ActionCommand<T = (), E = Box<dyn Error>> {
fn name(&self) -> &'static str;
fn command(&self, command: Command) -> Command;
fn action(&self, matches: Vec1<&ArgMatches>) -> Result<T, E>;
}
pub struct CommandMap<'a, T = (), E = Box<dyn Error>> {
command_map: HashMap<&'static str, Box<dyn ActionCommand<T, E> + Send + Sync + 'a>>,
}
impl<'a> CommandMap<'a> {
pub fn builder() -> CommandMapBuilder<'a> {
CommandMapBuilder {
command_map: HashMap::new(),
}
}
pub fn commands(&self) -> Vec<Command> {
self.command_map
.values()
.map(|v| v.command(Command::new(v.name())))
.collect()
}
pub fn dispatch(&self, matches: Vec1<&ArgMatches>) -> Result<(), DispatchError> {
let local_matches = matches.last();
if let Some((command_name, subcommand)) = local_matches.subcommand() {
if let Some(action_command) = self.command_map.get(command_name) {
action_command
.action(Vec1::from_vec_push(matches.to_vec(), subcommand))
.with_context(|_| ActionCommandSnafu {
command_name: command_name.to_owned(),
})?;
return Ok(());
}
return Err(DispatchError::SubcommandNotInMap {
command_name: command_name.to_owned(),
all_commands: self
.command_map
.values()
.map(|action_command| action_command.name())
.collect(),
});
}
Err(DispatchError::NoSubcommand)
}
}
#[derive(Debug, Snafu)]
pub enum DispatchError {
ActionCommand {
command_name: String,
source: Box<dyn std::error::Error>,
},
SubcommandNotInMap {
command_name: String,
all_commands: Vec<&'static str>,
},
NoSubcommand,
}
pub struct CommandMapBuilder<'a> {
command_map: HashMap<&'static str, Box<dyn ActionCommand + Send + Sync + 'a>>,
}
impl<'a> CommandMapBuilder<'a> {
pub fn push(
mut self,
action_command: impl ActionCommand + Send + Sync + 'a,
) -> CommandMapBuilder<'a> {
self.command_map
.insert(action_command.name(), Box::new(action_command));
self
}
pub fn push_all(
mut self,
action_commands: impl IntoIterator<Item = impl ActionCommand + Send + Sync + 'a>,
) -> CommandMapBuilder<'a> {
for action_command in action_commands {
self.command_map
.insert(action_command.name(), Box::new(action_command));
}
self
}
pub fn build(self) -> CommandMap<'a> {
CommandMap {
command_map: self.command_map,
}
}
}
pub struct CommandMapActionCommand<'a> {
name: &'static str,
command_map: CommandMap<'a>,
}
impl<'a> CommandMapActionCommand<'a> {
pub fn new(name: &'static str, command_map: CommandMap<'a>) -> CommandMapActionCommand<'a> {
CommandMapActionCommand { name, command_map }
}
}
impl<'a> ActionCommand for CommandMapActionCommand<'a> {
fn name(&self) -> &'static str {
self.name
}
fn command(&self, command: clap::Command) -> clap::Command {
command.subcommands(self.command_map.commands())
}
fn action(&self, matches: Vec1<&ArgMatches>) -> Result<(), Box<dyn std::error::Error>> {
match self.command_map.dispatch(matches) {
Ok(()) => Ok(()),
Err(e) => Err(Box::new(e)),
}
}
}
pub fn get_many<'a, T: Any + Clone + Send + Sync + 'static>(
matches: &[&'a ArgMatches],
id: &str,
) -> Flatten<IntoIter<ValuesRef<'a, T>>> {
let mut collected_values = vec![];
for matches in matches.iter() {
if let Ok(Some(values)) = matches.try_get_many(id) {
collected_values.push(values);
}
}
collected_values.into_iter().flatten()
}
pub fn get_one<'a, T: Any + Clone + Send + Sync + 'static>(
matches: &[&'a ArgMatches],
id: &str,
) -> Option<&'a T> {
let mut best_match = None;
let mut best_match_specificity = ValueSource::DefaultValue;
for matches in matches.iter() {
let current_match = match matches.try_get_one::<T>(id) {
Ok(arg_match) => arg_match,
Err(_) => continue,
};
let current_specificity = matches.value_source(id);
if let Some(current_specificity) = current_specificity {
if best_match_specificity <= current_specificity {
best_match = current_match;
best_match_specificity = current_specificity;
}
}
}
best_match
}
pub fn get_raw<'a>(matches: &[&'a ArgMatches], id: &str) -> Flatten<IntoIter<RawValues<'a>>> {
let mut collected_values = vec![];
for matches in matches.iter() {
if let Ok(Some(values)) = matches.try_get_raw(id) {
collected_values.push(values);
}
}
collected_values.into_iter().flatten()
}
#[doc(hidden)]
pub struct HelloWorldCommand {}
impl ActionCommand for HelloWorldCommand {
fn name(&self) -> &'static str {
"hello-world"
}
fn command(&self, command: Command) -> Command {
command.about("Say hello to the world").alias("h")
}
fn action(&self, _matches: Vec1<&ArgMatches>) -> Result<(), Box<dyn std::error::Error>> {
println!("Hello, world!");
Ok(())
}
}
#[doc = include_str!("../README.md")]
#[cfg(doctest)]
struct ReadmeDoctests {}
#[cfg(test)]
mod tests {
use super::{get_one, ActionCommand, CommandMap, CommandMapActionCommand, DispatchError};
use clap::{builder::NonEmptyStringValueParser, Arg, ArgMatches, Command};
use std::ffi::OsString;
use vec1::{vec1, Vec1};
struct HelloWorldCommand {}
impl ActionCommand for HelloWorldCommand {
fn name(&self) -> &'static str {
"hello-world"
}
fn command(&self, command: clap::Command) -> clap::Command {
command.alias("h").arg(
Arg::new("bar")
.short('b')
.value_parser(NonEmptyStringValueParser::new())
.required(true),
)
}
fn action(&self, matches: Vec1<&ArgMatches>) -> Result<(), Box<dyn std::error::Error>> {
println!(
"Hello, World! My args are {{ foo: {}, bar: {} }}",
matches.first().get_one::<String>("foo").unwrap(),
matches.last().get_one::<String>("bar").unwrap(),
);
Ok(())
}
}
fn example_dispatch(
itr: impl IntoIterator<Item = impl Into<OsString> + Clone>,
) -> Result<(), DispatchError> {
let base_command = Command::new("command_matching").arg(
Arg::new("foo")
.short('f')
.value_parser(NonEmptyStringValueParser::new())
.required(true),
);
let command_map_action_command = CommandMapActionCommand::new(
"foo",
CommandMap::builder().push(HelloWorldCommand {}).build(),
);
let command_map = CommandMap::builder()
.push(command_map_action_command)
.build();
let base_command = base_command
.subcommands(command_map.commands())
.subcommand(Command::new("bar"));
let matches = base_command.get_matches_from(itr);
command_map.dispatch(vec1![&matches])
}
#[test]
fn alias_matching() {
let r = example_dispatch([
"command_matching",
"-f",
"my_foo",
"foo",
"h", "-b",
"my_bar",
]);
assert!(r.is_ok());
}
#[test]
fn subcommand_not_in_map() {
let r = example_dispatch(["command_matching", "-f", "my_foo", "bar"]);
assert!(matches!(
r,
Err(DispatchError::SubcommandNotInMap {
command_name: _,
all_commands: _,
})
));
}
#[test]
fn no_subcommand() {
let r = example_dispatch(["command_matching", "-f", "my_foo"]);
assert!(matches!(r, Err(DispatchError::NoSubcommand)));
}
#[test]
fn get_one_picks_most_specific() {
let command = Command::new("my-program")
.arg(Arg::new("my-arg").long("my-arg").default_value("gamma"))
.subcommand(Command::new("my-subcommand").arg(Arg::new("my-arg").long("my-arg")));
let matches = command.get_matches_from([
"my-program",
"--my-arg",
"alpha",
"my-subcommand",
"--my-arg",
"beta",
]);
let command_matches = vec![&matches, matches.subcommand().unwrap().1];
let arg = get_one::<String>(&command_matches, "my-arg");
assert_eq!("beta", arg.unwrap());
}
#[test]
fn get_one_ignores_defaults() {
let command = Command::new("my-program")
.arg(Arg::new("my-arg").long("my-arg").default_value("gamma"))
.subcommand(Command::new("my-subcommand").arg(Arg::new("my-arg").long("my-arg")));
let matches = command.get_matches_from([
"my-program",
"my-subcommand",
"--my-arg",
"beta",
]);
let command_matches = vec![&matches, matches.subcommand().unwrap().1];
let arg = get_one::<String>(&command_matches, "my-arg");
assert_eq!("beta", arg.unwrap());
}
}