use core::marker::PhantomData;
use std::{collections::BTreeMap, convert::From, fmt};
const HELP: &str = "help";
const HELP_INPUT: &str = "--help";
const VERSION: &str = "version";
const VERSION_INPUT: &str = "--version";
const INPUT_PREFIX: &str = "--";
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ParserError {
UnexpectedInput(String),
DuplicateInput(String),
MutuallyExclusiveInput(String, String),
MissingValue(String),
MissingInput(String),
}
impl fmt::Display for ParserError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnexpectedInput(u) => write!(f, "found {u}, which was not an expected argument"),
Self::DuplicateInput(i) => {
write!(f, "found argument {i} more then once, but only one instance was expected")
}
Self::MutuallyExclusiveInput(y, z) => write!(
f,
"arguments {y} and {z} are mutually exclusive and cannot be used at the same time"
),
Self::MissingValue(i) => {
write!(f, "found argument {i}, which requires a value, but no value was given")
}
Self::MissingInput(i) => write!(f, "argument {i} is required but was not found"),
}
}
}
pub trait GetParserForOptions {
fn parser() -> Parser;
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct OptionsParser<T: GetParserForOptions> {
_phantom: PhantomData<T>,
}
impl<T: GetParserForOptions> OptionsParser<T> {
pub fn parse(inputs: &mut Vec<String>) -> Result<Parser, ParserError> {
if !inputs.is_empty() && !inputs[0].starts_with(INPUT_PREFIX) {
inputs.remove(0);
}
let mut parser = T::parser();
parser.parse(inputs)?;
Ok(parser)
}
}
pub trait GetParserForCommand {
fn parser(&self) -> Parser;
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct CommandParser<C: From<String> + GetParserForCommand> {
_phantom: PhantomData<C>,
}
impl<C: From<String> + GetParserForCommand> CommandParser<C> {
pub fn parse(inputs: &mut Vec<String>) -> Result<(C, Parser), ParserError> {
let command = Self::extract_command(inputs)?;
let mut parser = command.parser();
parser.parse(inputs)?;
Ok((command, parser))
}
fn extract_command(inputs: &mut Vec<String>) -> Result<C, ParserError> {
inputs.remove(0);
let command: C = inputs
.first()
.ok_or(ParserError::MissingInput("No command provided".to_owned()))?
.clone()
.into();
inputs.remove(0);
Ok(command)
}
}
#[derive(Default, Clone, Debug, PartialEq)]
pub struct Parser {
token_map: TokenMap,
}
impl Parser {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn token(mut self, token: Token) -> Self {
self.token_map.insert(token);
self
}
#[must_use]
pub fn help(&self) -> bool {
self.token_map.get_flag(HELP).unwrap_or(false)
}
#[must_use]
pub fn version(&self) -> bool {
self.token_map.get_flag(VERSION).unwrap_or(false)
}
#[must_use]
pub fn flag(&self, name: &str) -> Option<bool> {
self.token_map.get_flag(name)
}
#[must_use]
pub fn single(&self, name: &str) -> Option<&String> {
self.token_map.get_single(name)
}
#[must_use]
pub fn multiple(&self, name: &str) -> Option<&[String]> {
self.token_map.get_multiple(name)
}
pub fn parse(&mut self, inputs: &[String]) -> Result<(), ParserError> {
self.token_map.parse(inputs)
}
#[must_use]
pub fn info(&self) -> String {
let mut info = vec![];
let required = self.tokens_info(true);
if !required.is_empty() {
info.push("Required CLI inputs:".to_string());
info.push(required);
}
info.push(String::new());
let optional = self.tokens_info(false);
if !optional.is_empty() {
info.push("Optional CLI inputs:".to_string());
info.push(optional);
}
info.join("\n")
}
fn tokens_info(&self, is_required: bool) -> String {
let tokens: Vec<_> = self
.token_map
.tokens
.values()
.filter(|arg| is_required == arg.required)
.collect();
let width =
tokens.iter().map(|arg| arg.name().len()).max().unwrap_or(0);
tokens
.into_iter()
.map(|arg| arg.info(width))
.collect::<Vec<_>>()
.join("\n")
}
}
#[derive(Default, Clone, Debug, PartialEq)]
pub struct Token {
name: String,
help: String,
required: bool,
requires: Option<String>,
forbids: Vec<String>,
takes_value: bool,
default_value: Option<TokenType>,
user_value: Option<TokenType>,
allow_multiple: bool,
}
impl Token {
#[must_use]
pub fn new(name: &str, help: &str) -> Token {
Token {
name: name.to_string(),
help: help.to_string(),
..Default::default()
}
}
#[must_use]
pub fn allow_multiple(mut self, multiple: bool) -> Self {
self.allow_multiple = multiple;
self
}
#[must_use]
pub fn required(mut self, required: bool) -> Self {
self.required = required;
self
}
#[must_use]
pub fn requires(mut self, required: &str) -> Self {
self.requires = Some(required.to_string());
self
}
#[must_use]
pub fn forbids(mut self, forbidden: Vec<&str>) -> Self {
self.forbids = forbidden.into_iter().map(String::from).collect();
self
}
#[must_use]
pub fn takes_value(mut self, takes_value: bool) -> Self {
self.takes_value = takes_value;
self
}
#[must_use]
pub fn default_value(mut self, default: &str) -> Self {
self.default_value = Some(TokenType::Single(default.to_string()));
self
}
fn name(&self) -> String {
if self.takes_value {
format!(" --{} <{}>", self.name, self.name)
} else {
format!(" --{}", self.name)
}
}
fn info(&self, width: usize) -> String {
let mut info =
vec![format!("{:<width$}", self.name()), self.help.clone()];
if let Some(v) = &self.default_value {
info.push(format!("[default: {v}]"));
}
info.join(" ")
}
}
#[derive(Clone, Debug, PartialEq)]
enum TokenType {
Flag,
Single(String),
Multiple(Vec<String>),
}
impl TokenType {
fn as_single(&self) -> Option<&String> {
match self {
TokenType::Single(s) => Some(s),
_ => None,
}
}
fn as_multiple(&self) -> Option<&[String]> {
match self {
TokenType::Multiple(v) => Some(v),
_ => None,
}
}
fn as_flag(&self) -> bool {
matches!(self, TokenType::Flag)
}
fn push_val(&mut self, val: &str) -> Result<(), ParserError> {
match self {
TokenType::Multiple(ref mut v) => {
v.push(val.to_string());
Ok(())
}
_ => Err(ParserError::DuplicateInput(val.to_string())),
}
}
}
impl fmt::Display for TokenType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Flag => write!(f, "true"),
Self::Single(s) => write!(f, "{s}"),
Self::Multiple(v) => write!(f, "{v:?}"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
struct TokenMap {
tokens: BTreeMap<String, Token>,
}
impl TokenMap {
fn parse(&mut self, inputs: &[String]) -> Result<(), ParserError> {
if inputs.contains(&HELP_INPUT.to_string()) {
let h = self
.tokens
.get_mut(HELP)
.expect("CLI Parser internal error: help token does not exist");
h.user_value = Some(TokenType::Flag);
return Ok(());
}
if inputs.contains(&VERSION_INPUT.to_string()) {
let v = self.tokens.get_mut(VERSION).expect(
"CLI Parser internal error: version token does not exist",
);
v.user_value = Some(TokenType::Flag);
return Ok(());
}
self.do_parse(inputs)
}
fn get_multiple(&self, name: &str) -> Option<&[String]> {
self.type_of(name).and_then(TokenType::as_multiple)
}
fn get_single(&self, name: &str) -> Option<&String> {
self.type_of(name).and_then(TokenType::as_single)
}
fn get_flag(&self, name: &str) -> Option<bool> {
self.type_of(name).map(TokenType::as_flag)
}
fn do_parse(&mut self, inputs: &[String]) -> Result<(), ParserError> {
let mut iter = inputs.iter().peekable();
while let Some(input) = iter.next() {
self.check_input(input)?;
let name = &input[INPUT_PREFIX.len()..];
let token = self.tokens.get_mut(name).ok_or_else(|| {
ParserError::UnexpectedInput(name.to_string())
})?;
let user_value = if token.takes_value {
let value = if iter
.peek()
.filter(|i| !i.starts_with(INPUT_PREFIX))
.is_some()
{
iter.next().unwrap().to_string()
} else if let Some(ref d) = token.default_value {
d.to_string()
} else {
Err(ParserError::MissingValue(name.to_string()))?
};
if token.allow_multiple {
match token.user_value.take() {
Some(mut multiple) => {
multiple.push_val(&value)?;
multiple
}
None => TokenType::Multiple(vec![value]),
}
} else {
TokenType::Single(value)
}
} else {
TokenType::Flag
};
token.user_value = Some(user_value);
}
self.check_constraints(inputs)?;
Ok(())
}
fn check_constraints(&self, inputs: &[String]) -> Result<(), ParserError> {
for token in self.tokens.values() {
if token.required && token.user_value.is_none() {
Err(ParserError::MissingInput(token.name.to_string()))?;
}
if token.user_value.is_some() {
if let Some(ref other_name) = token.requires {
if !inputs.contains(&(format!("--{other_name}"))) {
return Err(ParserError::MissingInput(
other_name.to_string(),
));
}
}
for other_name in &token.forbids {
if inputs.contains(&(format!("--{other_name}"))) {
Err(ParserError::MutuallyExclusiveInput(
token.name.to_string(),
other_name.to_string(),
))?;
}
}
}
}
Ok(())
}
fn check_input(&self, input: &str) -> Result<(), ParserError> {
if !input.starts_with(INPUT_PREFIX) {
return Err(ParserError::UnexpectedInput(input.to_string()));
}
let name = &input[INPUT_PREFIX.len()..];
let token = self
.tokens
.get(name)
.ok_or_else(|| ParserError::UnexpectedInput(input.to_string()))?;
if !token.allow_multiple && token.user_value.is_some() {
return Err(ParserError::DuplicateInput(name.to_string()));
}
Ok(())
}
fn type_of(&self, name: &str) -> Option<&TokenType> {
self.tokens.get(name).and_then(|token| {
token.user_value.as_ref().or(token.default_value.as_ref())
})
}
fn insert(&mut self, token: Token) {
self.tokens.insert(token.name.clone(), token);
}
}
impl Default for TokenMap {
fn default() -> Self {
let mut token_map =
Self { tokens: BTreeMap::<String, Token>::default() };
token_map.insert(Token::new(HELP, "Display the help message."));
token_map.insert(Token::new(VERSION, "Display the version."));
token_map
}
}
#[cfg(test)]
mod test {
use super::*;
fn setup() -> Parser {
Parser::new()
.token(
Token::new("required-with-value", "info 1")
.required(true)
.takes_value(true),
)
.token(
Token::new("requires-no-value", "info 2")
.requires("optional-value")
.takes_value(false),
)
.token(
Token::new("multiple", "info 69")
.allow_multiple(true)
.takes_value(true),
)
.token(
Token::new("optional-with-default", "info 3")
.takes_value(true)
.default_value("default1"),
)
.token(
Token::new("forbid1-with-value", "info 5")
.takes_value(true)
.forbids(vec!["forbid2-no-value"]),
)
.token(
Token::new("forbid2-no-value", "info 6")
.forbids(vec!["forbid1-with-value"]),
)
.token(Token::new("optional-value", "info 7").takes_value(true))
}
#[test]
fn parse_works() {
let input: Vec<_> = vec![
"--required-with-value",
"val1",
"--requires-no-value",
"--multiple",
"val2",
"--multiple",
"val3",
"--optional-with-default",
"--forbid1-with-value",
"val4",
"--optional-value",
"val5",
]
.into_iter()
.map(String::from)
.collect::<Vec<String>>();
let mut parser = setup();
parser.parse(&input).unwrap();
assert_eq!(
parser.single("required-with-value"),
Some(&"val1".to_string())
);
assert_eq!(parser.flag("requires-no-value"), Some(true));
assert_eq!(parser.flag("requires-no-va"), None);
assert_eq!(
parser.multiple("multiple"),
Some(&["val2".to_string(), "val3".to_string()][..])
);
assert_eq!(
parser.single("optional-with-default"),
Some(&"default1".to_string())
);
assert_eq!(
parser.single("forbid1-with-value"),
Some(&"val4".to_string())
);
assert_eq!(parser.single("optional-value"), Some(&"val5".to_string()));
}
#[test]
fn parser_errors_correctly() {
let input = [
"--requires-no-value",
"--required-with-value",
"--forbid2-with-default",
"a",
]
.into_iter()
.map(String::from)
.collect::<Vec<String>>();
assert_eq!(
setup().parse(&input),
Err(ParserError::MissingValue("required-with-value".to_string()))
);
let input: Vec<_> = [
"--forbid2-no-value",
"--multiple",
"beans",
"--required-with-value",
"a",
"--forbid2-no-value",
"--multiple",
"based",
]
.into_iter()
.map(String::from)
.collect();
assert_eq!(
setup().parse(&input),
Err(ParserError::DuplicateInput("forbid2-no-value".to_string()))
);
let input: Vec<_> =
vec!["--requires-no-value", "--optional-with-default", "420"]
.into_iter()
.map(String::from)
.collect();
assert_eq!(
setup().parse(&input),
Err(ParserError::MissingInput("required-with-value".to_string()))
);
let input: Vec<_> = vec![
"--requires-no-value",
"--required-with-value",
"brawndo",
"--vape-nation",
]
.into_iter()
.map(String::from)
.collect();
assert_eq!(
setup().parse(&input),
Err(ParserError::UnexpectedInput("--vape-nation".to_string()))
);
let input: Vec<_> = vec![
"--requires-no-value",
"--required-with-value",
"brawndo",
"one-vape-world",
]
.into_iter()
.map(String::from)
.collect();
assert_eq!(
setup().parse(&input),
Err(ParserError::UnexpectedInput("one-vape-world".to_string()))
);
let input =
vec!["--forbid1-with-value", "eataly", "--forbid2-no-value"]
.into_iter()
.map(String::from)
.collect::<Vec<String>>();
assert_eq!(
setup().parse(&input),
Err(ParserError::MutuallyExclusiveInput(
"forbid1-with-value".to_string(),
"forbid2-no-value".to_string(),
))
);
}
#[test]
fn version_works() {
let input: Vec<_> = ["--version", "right to bear vapes"]
.into_iter()
.map(String::from)
.collect();
let mut parser = setup();
parser.parse(&input).unwrap();
assert!(parser.version());
}
#[test]
fn help_works() {
let input: Vec<_> = [
"--help",
"your body needs to vape",
"--because",
"vapes got what your body needs",
]
.into_iter()
.map(String::from)
.collect();
let mut parser = setup();
parser.parse(&input).unwrap();
assert!(parser.help());
}
#[test]
fn info_works() {
let parser = Parser::new()
.token(
Token::new("token1", "info 1").required(true).takes_value(true),
)
.token(
Token::new("token2-is-super-long", "info 2").takes_value(false),
)
.token(
Token::new("token3", "info 3")
.takes_value(false)
.default_value("token3-default"),
);
let expected = "Required CLI inputs:\n\t--token1 <token1> info 1\n\nOptional CLI inputs:\n\t--help Display the help message.\n\t--token2-is-super-long info 2\n\t--token3 info 3 [default: token3-default]\n\t--version Display the version.";
assert_eq!(parser.info(), expected);
}
}