use std::cell::RefCell;
use std::collections::HashMap;
use std::env::{self, current_exe};
use std::fmt::Write;
use std::process::exit;
use std::rc::Rc;
pub use errors::HpError;
pub mod errors;
type Action = Rc<RefCell<dyn FnMut(Vec<String>)>>;
pub type TemplateId = usize;
#[derive(Clone, Debug)]
pub struct ParsedArgument {
id: TemplateId,
values: Vec<String>,
}
impl ParsedArgument {
fn new(id: usize, values: Vec<String>) -> Self {
Self { id, values }
}
pub fn values(&self) -> &Vec<String> {
&self.values
}
pub fn id(&self) -> TemplateId {
self.id
}
pub fn number_of_values(&self) -> usize {
self.values.len()
}
}
#[derive(Clone, Debug)]
pub struct ParsedArguments {
hm: HashMap<String, ParsedArgument>,
ids: HashMap<usize, ParsedArgument>,
}
impl ParsedArguments {
pub fn get(&self, key: impl AsRef<str>) -> Option<&ParsedArgument> {
let key = format!("0#{}", key.as_ref());
self.hm.get(&key)
}
pub fn get_with_id(&self, id: TemplateId) -> Option<&ParsedArgument> {
self.ids.get(&id)
}
pub fn has(&self, key: impl AsRef<str>) -> bool {
self.get(key).is_some()
}
pub fn has_with_id(&self, id: TemplateId) -> bool {
self.get_with_id(id).is_some()
}
pub fn get_with_context(
&self,
context: usize,
key: impl AsRef<str>,
) -> Option<&ParsedArgument> {
let key = format!("{context}#{}", key.as_ref());
self.hm.get(&key)
}
pub fn has_with_context(&self, context: usize, key: impl AsRef<str>) -> bool {
self.get_with_context(context, key).is_some()
}
}
#[derive(Default, Clone)]
pub struct Template {
matches: Vec<String>,
num_values: usize,
optional_vals: bool,
help: String,
subargument_of: Option<usize>,
id: TemplateId,
action: Option<Action>,
}
impl Template {
pub fn new() -> Self {
Self {
matches: Vec::new(),
num_values: 0,
optional_vals: false,
help: "".into(),
subargument_of: None,
id: 0,
action: None,
}
}
pub fn matches<S: AsRef<str>>(mut self, name: S) -> Self {
let name = name.as_ref().to_string();
if !self.matches.contains(&name) {
self.matches.push(name)
}
self
}
pub fn number_of_values(mut self, nv: usize) -> Self {
self.num_values = nv;
self
}
pub fn optional_values(mut self, ov: bool) -> Self {
self.optional_vals = ov;
self
}
pub fn with_help<S: AsRef<str>>(mut self, help_string: S) -> Self {
self.help = help_string.as_ref().into();
self
}
pub fn on_parse<F: FnMut(Vec<String>) + 'static>(mut self, action: F) -> Self {
self.action = Some(Rc::new(RefCell::new(action)));
self
}
pub(crate) fn set_id(&mut self, id: usize) {
self.id = id
}
pub(crate) fn subarg(&mut self, id: usize) {
let _ = self.subargument_of.insert(id);
}
}
#[derive(Default, Clone)]
pub struct Parser {
stored: HashMap<String, Template>,
order: Vec<String>,
last_id: usize,
exit_on_help: bool,
author: String,
description: String,
usage: String,
program_name: String,
help: Option<String>,
}
impl Parser {
pub fn new() -> Self {
let exe_name = match current_exe() {
Ok(pb) => {
if let Some(name) = pb.file_name() {
name.to_str().unwrap_or("").to_string()
} else {
"".to_string()
}
}
Err(_) => "".to_string(),
};
Self {
stored: HashMap::new(),
order: Vec::new(),
last_id: 0,
exit_on_help: true,
author: "".to_string(),
description: "".to_string(),
usage: "".to_string(),
program_name: exe_name,
help: None,
}
}
pub fn exit_on_help(mut self, v: bool) -> Self {
self.exit_on_help = v;
self
}
pub fn with_author<S: AsRef<str>>(mut self, v: S) -> Self {
self.author = v.as_ref().to_string();
self
}
pub fn with_description<S: AsRef<str>>(mut self, v: S) -> Self {
self.description = v.as_ref().to_string();
self
}
pub fn with_usage<S: AsRef<str>>(mut self, v: S) -> Self {
self.usage = v.as_ref().to_string();
self
}
pub fn with_program_name<S: AsRef<str>>(mut self, v: S) -> Self {
self.program_name = v.as_ref().to_string();
self
}
pub fn set_help<S: AsRef<str>>(mut self, v: S) -> Self {
self.help = Some(v.as_ref().to_string());
self
}
fn generate_id(&mut self) -> usize {
self.last_id += 1;
self.last_id
}
fn add_to_map(&mut self, mut template: Template) -> TemplateId {
let template_id = self.generate_id();
template.set_id(template_id);
let matches = template.matches.clone();
for name in matches.iter() {
let subarg = template.subargument_of.unwrap_or(0);
let new_name = format!("{}#{}", subarg, name.clone());
let _ = self.stored.insert(new_name.clone(), template.clone());
self.order.push(name.clone())
}
template_id
}
pub fn add<S: AsRef<str>>(
&mut self,
matches: S,
num_values: usize,
help_message: S,
) -> TemplateId {
let template = Template::new()
.matches(matches)
.number_of_values(num_values)
.with_help(help_message);
self.add_to_map(template)
}
pub fn add_template(&mut self, template: Template) -> TemplateId {
self.add_to_map(template)
}
pub fn add_subcommand<S: AsRef<str>>(
&mut self,
subargument_of: usize,
matches: S,
num_values: usize,
help_message: S,
) -> TemplateId {
let id = self.generate_id();
let mut template = Template::new()
.matches(matches.as_ref())
.number_of_values(num_values)
.with_help(help_message.as_ref());
template.set_id(id);
template.subarg(subargument_of);
self.add_to_map(template)
}
pub fn add_subcommand_template(
&mut self,
subargument_of: usize,
mut template: Template,
) -> TemplateId {
let id = self.generate_id();
template.set_id(id);
template.subarg(subargument_of);
self.add_to_map(template)
}
fn create_help(&self) -> String {
let mut result_string = String::new();
let longest_value_len = self
.stored
.values()
.into_iter()
.map(|t| {
let mut temp = t.matches.join(" | ");
if t.num_values > 0 {
let optional = match t.optional_vals {
true => " optional ",
false => " ",
};
write!(temp, " [{}{optional}value/s]", t.num_values).unwrap();
}
temp.len()
})
.max();
if !self.program_name.is_empty() {
write!(result_string, "{}", self.program_name).unwrap_or(());
}
if !self.description.is_empty() {
writeln!(result_string, ": {}", self.description).unwrap_or(());
}
if !self.author.is_empty() {
writeln!(result_string, "Author: {}", self.author).unwrap_or(());
}
if !self.usage.is_empty() {
writeln!(result_string, "Usage:\n {}", self.usage).unwrap_or(());
} else {
writeln!(
result_string,
"Usage:\n $ {} -[-command] [value/s...]",
self.program_name
)
.unwrap_or(());
}
let longest_value_len = match longest_value_len {
Some(l) => l + 4,
None => 4,
};
let mut max_level = 0;
writeln!(result_string, "Arguments:").unwrap_or(());
let mut template_vec: Vec<(&Template, usize)> = Vec::new();
for name in self.order.iter() {
let each = self
.stored
.values()
.find(|temp| temp.matches.contains(name))
.unwrap();
if !template_vec
.iter()
.any(|(template, _)| template.id == each.id)
{
if let Some(sub_arg_of) = each.subargument_of {
if let Some((index, (_, level))) = template_vec
.iter()
.enumerate()
.find(|(_, (t, _))| t.id == sub_arg_of)
{
if level + 1 > max_level {
max_level = level + 1;
}
template_vec.insert(index + 1, (each, level + 1));
}
} else {
template_vec.push((each, 0))
}
}
}
for (template, level) in template_vec.iter() {
let mut lvl = String::new();
(0..(level * 4)).for_each(|_| lvl.push(' '));
let mut matches = template.matches.join(" | ");
if template.num_values > 0 {
let optional = match template.optional_vals {
true => " optional ",
false => " ",
};
write!(matches, " [{}{optional}value/s]", template.num_values).unwrap();
}
while matches.len() != longest_value_len + (max_level * 4) - lvl.len() {
matches.push(' ');
}
writeln!(result_string, " {lvl}{matches} {}", template.help).unwrap_or(());
}
let mut help = String::from("-h, --help");
while help.len() != longest_value_len + max_level * 4 {
help.push(' ');
}
write!(result_string, " {help} Print this help message!").unwrap_or(());
result_string
}
fn help_and_exit(&self) {
if let Some(help) = &self.help {
println!("{help}");
} else {
let help_string = self.create_help();
println!("{help_string}");
}
if self.exit_on_help {
exit(0);
}
}
pub fn parse(&mut self, from: Option<Vec<&str>>) -> Result<ParsedArguments, HpError> {
let args: Vec<String>;
if let Some(from_vec) = from {
args = from_vec.iter().map(|each| each.to_string()).collect();
} else {
args = env::args().collect();
}
let mut hm = HashMap::new();
let mut idhm = HashMap::new();
let mut context = 0;
for (index, arg) in args.iter().enumerate() {
if arg == "--help" || arg == "-h" {
self.help_and_exit()
}
let query = format!("{context}#{arg}");
let query2 = format!("0#{arg}");
if self.stored.get(&query).is_some() {
if let Some(template) = self.stored.get(&query) {
context = template.id;
let mut i = index;
let mut count = 0;
let mut values: Vec<String> = Vec::new();
while i < index + template.num_values {
i += 1;
if i == args.len() {
break;
}
let value = &args[i];
let q1 = format!("{context}#{value}");
let q2 = format!("0#{value}");
if self.stored.get(&q1).is_some() || self.stored.get(&q2).is_some() {
break;
} else {
values.push(value.to_string());
count += 1;
}
}
if !template.optional_vals && count < template.num_values {
return Err(HpError::NumberOfValues(
arg.into(),
count,
template.num_values,
));
}
if let Some(action) = &template.action {
action.borrow_mut()(values.clone());
}
let pa = ParsedArgument::new(template.id, values);
hm.insert(query, pa.clone());
idhm.insert(template.id, pa);
}
} else if let Some(template) = self.stored.get(&query2) {
context = template.id;
let mut i = index;
let mut count = 0;
let mut values: Vec<String> = Vec::new();
while i < index + template.num_values {
i += 1;
if i == args.len() {
break;
}
let value = &args[i];
let q1 = format!("{context}#{value}");
let q2 = format!("0#{value}");
if self.stored.get(&q1).is_some() || self.stored.get(&q2).is_some() {
break;
} else {
values.push(value.to_string());
count += 1;
}
}
if !template.optional_vals && count < template.num_values {
return Err(HpError::NumberOfValues(
arg.into(),
count,
template.num_values,
));
}
if let Some(action) = &template.action {
action.borrow_mut()(values.clone());
}
let pa = ParsedArgument::new(template.id, values);
hm.insert(query2, pa.clone());
idhm.insert(template.id, pa);
} else if let Some(template) = self.stored.values().find(|t| t.matches.contains(arg)) {
if let Some(parent) = template.subargument_of {
let parent = self.stored.values().find(|t| t.id == parent).unwrap();
let parent_match = &parent.matches[0];
return Err(HpError::OutOfContext(
arg.to_string(),
parent_match.to_string(),
));
}
}
}
Ok(ParsedArguments { hm, ids: idhm })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn help() {
let mut parser = Parser::new()
.with_usage("")
.with_author("me")
.with_description("Example program")
.exit_on_help(false);
parser.add("--say", 0, "Repeat something");
let expand = parser.add_template(
Template::new()
.matches("-x")
.matches("--expand")
.optional_values(false)
.number_of_values(0)
.with_help("Expand something."),
);
let sub_sub = parser.add_subcommand_template(
expand,
Template::new()
.matches("--string")
.number_of_values(0)
.with_help("Expands a string"),
);
let sub_sub_sub = parser.add_subcommand(sub_sub, "--super-test", 0, "Amazing super test.");
let _inf = parser.add_subcommand(sub_sub_sub, "-i", 0, "Infinite nesting!");
parser.parse(Some(vec!["--help"])).unwrap();
}
#[test]
fn parsed_args() {
let mut parser = Parser::new();
parser.add("--hello", 0, "hello");
let arg = parser.add("arg", 3, "hello");
parser.add_template(
Template::new()
.matches("-not-found")
.number_of_values(0)
.with_help("bad.")
.on_parse(|_| ()),
);
let result = parser.parse(Some(vec!["--hello", "arg", "h", "w", "x"]));
assert!(result.is_ok());
let r = result.expect("bad");
assert!(r.has("--hello"));
assert!(r.get_with_id(arg).unwrap().values.len() == 3);
assert!(!r.has("-not-found"))
}
#[test]
fn context_parsing() {
let mut parser = Parser::new()
.with_usage("")
.with_author("me")
.with_description("Example program")
.exit_on_help(true);
parser.add("--say", 0, "Repeat something");
let expand = parser.add_template(
Template::new()
.matches("-x")
.matches("--expand")
.optional_values(false)
.number_of_values(0)
.with_help("Expand something."),
);
let sub_sub = parser.add_subcommand_template(
expand,
Template::new()
.matches("--string")
.number_of_values(0)
.with_help("Expands a string"),
);
let sub_sub_sub = parser.add_subcommand(sub_sub, "--super-test", 0, "Amazing super test.");
parser.add_subcommand(sub_sub_sub, "-i", 0, "Infinite nesting!");
let result = parser
.parse(Some(vec!["-x", "--string", "--super-test", "-i"]))
.unwrap();
assert!(result.has_with_context(expand, "--string"));
assert!(result.has_with_context(sub_sub, "--super-test"));
assert!(result.has_with_context(sub_sub_sub, "-i"));
}
#[test]
fn out_of_context() {
let mut parser: Parser = Parser::new()
.with_usage("")
.with_author("me")
.with_description("Example program")
.exit_on_help(true);
parser.add("--say", 0, "Repeat something");
let expand = parser.add_template(
Template::new()
.matches("-x")
.matches("--expand")
.optional_values(false)
.number_of_values(0)
.with_help("Expand something."),
);
parser.add_subcommand_template(
expand,
Template::new()
.matches("--string")
.number_of_values(0)
.with_help("Expands a string"),
);
let result = parser.parse(Some(vec!["--string"]));
assert!(result.is_err());
println!("{}", result.err().unwrap())
}
#[test]
fn action() {
let mut parser = Parser::new();
let mut last_val = String::new();
parser.add_template(
Template::new()
.matches("say")
.on_parse(move |values| {
println!("Saying: ");
for each in values.iter() {
last_val = each.to_string().clone();
}
println!("Last val {last_val}");
})
.number_of_values(8)
.optional_values(true),
);
assert!(parser.parse(Some(vec!["say", "hello", "world"])).is_ok())
}
}