#[cfg(test)]
use std::assert_matches;
use std::{
collections::{HashMap, HashSet},
fmt::{self, Display},
};
use indexmap::IndexMap;
use crate::{Interp, cons::ConsList};
use crate::{JimError, JimObject, JimResult};
const RECURSION_LIMIT: usize = 1000;
#[derive(Default)]
pub struct ProcArgParser {
flags: IndexMap<String, Option<String>>,
pos_args: Vec<(String, ArgRep)>,
}
#[derive(Debug, Clone, Copy)]
enum ArgRep {
Optional,
Required,
Repeated(bool),
}
#[derive(Debug, Clone)]
pub struct ProcArgs<'jim, 'parser>
where
'jim: 'parser,
{
proc_name: String,
flags: HashSet<&'parser str>,
options: HashMap<&'parser str, JimObject<'jim>>,
args: HashMap<&'parser str, JimObject<'jim>>,
}
#[derive(Debug, Clone)]
pub enum ParsedArgument<'jim, 'parser>
where
'jim: 'parser,
{
Flag(&'parser str),
Option(&'parser str, JimObject<'jim>),
Positional(JimObject<'jim>),
}
struct PosArgResult<'jim, 'parser>
where
'jim: 'parser,
{
name: &'parser str,
repetition: ArgRep,
value: JimObject<'jim>,
}
struct ProcUsage<'parser, 'name> {
name: &'name str,
parser: &'parser ProcArgParser,
}
struct ProcArgIter<'jim, 'parser, 'args>
where
'jim: 'parser,
'jim: 'args,
{
parser: &'parser ProcArgParser,
args: &'args [JimObject<'jim>],
pos: usize,
flags_done: bool,
}
impl ProcArgParser {
pub fn new() -> Self {
Default::default()
}
pub fn add_flag(&mut self, flag: &str) -> &mut Self {
self.flags.insert(flag.to_owned(), None);
self
}
pub fn add_option(&mut self, flag: &str, meta: &str) -> &mut Self {
self.flags.insert(flag.to_owned(), Some(meta.to_owned()));
self
}
pub fn add_required_arg(&mut self, name: &str) -> &mut Self {
self.pos_args.push((name.to_owned(), ArgRep::Required));
self
}
pub fn add_optional_arg(&mut self, name: &str) -> &mut Self {
self.pos_args.push((name.to_owned(), ArgRep::Optional));
self
}
pub fn add_repeated_arg(&mut self, name: &str, required: bool) -> &mut Self {
self.pos_args
.push((name.to_owned(), ArgRep::Repeated(required)));
self
}
pub fn usage(&self, proc_name: &str) -> impl Display {
ProcUsage {
name: proc_name,
parser: self,
}
}
pub fn parse<'jim, 'parser, 'args>(
&'parser self,
interp: &'jim Interp,
args: &'args [JimObject<'jim>],
) -> JimResult<ProcArgs<'jim, 'parser>>
where
'jim: 'parser,
'jim: 'args,
{
if args.is_empty() {
return Err("empty argument array".into());
}
let mut result = ProcArgs {
proc_name: args[0].as_str()?.to_owned(),
flags: Default::default(),
options: Default::default(),
args: Default::default(),
};
let mut pos_args = Vec::new();
for arg in self.parse_iter(args) {
match arg? {
ParsedArgument::Flag(name) => {
if !result.flags.insert(name) {
return Err(format!("unexpected repeated flag: {}", name).into());
}
}
ParsedArgument::Option(name, value) => {
if result.options.insert(name, value).is_some() {
return Err(format!("unexpected repeated option: {}", name).into());
}
}
ParsedArgument::Positional(value) => {
pos_args.push(value);
}
}
}
self.align_pos_args(interp, &result.proc_name, &pos_args, &mut result.args)?;
Ok(result)
}
pub fn parse_iter<'jim, 'parser, 'args>(
&'parser self,
args: &'args [JimObject<'jim>],
) -> impl Iterator<Item = JimResult<ParsedArgument<'jim, 'parser>>>
where
'jim: 'args,
'jim: 'parser,
{
ProcArgIter {
parser: self,
args,
pos: 1,
flags_done: false,
}
}
fn align_pos_args<'jim, 'parser>(
&'parser self,
interp: &'jim Interp,
proc_name: &str,
values: &[JimObject<'jim>],
out: &mut HashMap<&'parser str, JimObject<'jim>>,
) -> JimResult<()>
where
'jim: 'parser,
{
let specs: ConsList<_> = self
.pos_args
.iter()
.map(|(n, r)| (n.as_str(), *r))
.collect();
let aligned = self.align_pos_args_inner(proc_name, &specs, values, 0)?;
for ar in aligned.iter() {
if ar.repetition.is_repeated() {
let e = out.entry(ar.name).or_insert_with(|| interp.new_object());
e.list_append_obj(ar.value.clone());
} else {
out.insert(ar.name, ar.value.clone());
}
}
Ok(())
}
fn align_pos_args_inner<'jim, 'parser, 'specs>(
&'parser self,
proc_name: &str,
specs: &'specs ConsList<(&'parser str, ArgRep)>,
values: &[JimObject<'jim>],
count: usize,
) -> JimResult<ConsList<PosArgResult<'jim, 'parser>>>
where
'jim: 'parser,
'parser: 'specs,
{
if count >= RECURSION_LIMIT {
return Err("recursion limit exceeded, too many arguments".into());
}
if values.is_empty() {
for (name, rep) in specs.iter() {
if rep.is_required() {
return Err(self.parse_error(
proc_name,
format_args!("missing required argument {}", name),
));
}
}
Ok(Default::default())
} else if let Some(spec) = specs.first() {
let (name, rep) = spec;
if rep.is_repeated() {
if specs.rest().is_empty() {
return Ok(values
.iter()
.map(|v| PosArgResult {
name,
repetition: *rep,
value: v.clone(),
})
.collect());
}
let rest = if rep.is_required() {
let rspecs = specs.rest().prepend((name, rep.optional()));
self.align_pos_args_inner(proc_name, &rspecs, &values[1..], count + 1)?
} else if let Ok(rest) =
self.align_pos_args_inner(proc_name, specs, &values[1..], count + 1)
{
rest
} else {
return self.align_pos_args_inner(proc_name, &specs.rest(), values, count + 1);
};
Ok(rest.prepend(PosArgResult {
name,
repetition: *rep,
value: values[0].clone(),
}))
} else {
let rest =
self.align_pos_args_inner(proc_name, &specs.rest(), &values[1..], count + 1);
if let Ok(rest) = rest {
Ok(rest.prepend(PosArgResult {
name,
repetition: *rep,
value: values[0].clone(),
}))
} else if rep.is_required() {
Err(self.parse_error(proc_name, format_args!("missing argument {}", name)))
} else {
self.align_pos_args_inner(proc_name, &specs.rest(), values, count + 1)
}
}
} else {
Err(self.parse_error(proc_name, format_args!("too many arguments")))
}
}
fn parse_error(&self, name: &str, err: fmt::Arguments<'_>) -> JimError {
format!("{}\nusage: {}", err, self.usage(name)).into()
}
}
impl<'jim, 'parser> ProcArgs<'jim, 'parser>
where
'jim: 'parser,
{
pub fn proc_name(&self) -> &str {
&self.proc_name
}
pub fn get_flag(&self, flag: &str) -> bool {
self.flags.contains(flag)
}
pub fn get_option(&self, flag: &str) -> Option<&JimObject<'jim>> {
self.options.get(flag)
}
pub fn get_arg(&self, name: &str) -> Option<&JimObject<'jim>> {
self.args.get(name)
}
pub fn require_arg(&self, name: &str) -> JimResult<&JimObject<'jim>> {
self.args
.get(name)
.ok_or_else(|| format!("missing value for argument {}", name).into())
}
}
impl<'jim, 'parser, 'args> Iterator for ProcArgIter<'jim, 'parser, 'args>
where
'jim: 'parser,
'jim: 'args,
{
type Item = JimResult<ParsedArgument<'jim, 'parser>>;
fn next(&mut self) -> Option<Self::Item> {
if self.pos >= self.args.len() {
return None;
}
let arg = &self.args[self.pos];
let astr = arg.as_bytes();
self.pos += 1;
if !arg.as_bytes().starts_with(b"-") {
self.flags_done = true
}
if self.flags_done {
return Some(Ok(ParsedArgument::Positional(arg.clone())));
}
assert_eq!(astr[0], b'-');
if astr == b"--" {
self.flags_done = true;
return self.next();
}
let astr = match arg.as_str() {
Ok(s) => s,
Err(e) => return Some(Err(e.into())),
};
if let Some((k, mv)) = self.parser.flags.get_key_value(astr) {
if mv.is_some() {
if self.pos < self.args.len() {
let val = &self.args[self.pos];
self.pos += 1;
Some(Ok(ParsedArgument::Option(k, val.clone())))
} else {
self.pos = self.args.len();
Some(Err(self.parse_error(format_args!(
"missing argument for option {}",
astr
))))
}
} else {
Some(Ok(ParsedArgument::Flag(k)))
}
} else {
self.pos = self.args.len();
Some(Err(self.parse_error(format_args!("unknown flag {}", astr))))
}
}
}
impl<'jim, 'parser, 'args> ProcArgIter<'jim, 'parser, 'args>
where
'jim: 'parser,
'jim: 'args,
{
fn parse_error(&self, err: fmt::Arguments<'_>) -> JimError {
let name = self.args[0].to_string();
self.parser.parse_error(&name, err)
}
}
impl<'parser, 'name> Display for ProcUsage<'parser, 'name> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut started = false;
if !self.name.is_empty() {
write!(f, "{}", self.name)?;
started = true;
}
for (flag, arg) in self.parser.flags.iter() {
if started {
f.write_str(" ")?;
} else {
started = true;
}
write!(f, "?{}", flag)?;
if let Some(an) = arg {
write!(f, " {}", an)?;
}
write!(f, "?")?;
}
for (name, mult) in self.parser.pos_args.iter() {
if started {
f.write_str(" ")?;
} else {
started = true;
}
if !mult.is_required() {
f.write_str("?")?;
}
write!(f, "{}", name)?;
if mult.is_repeated() {
f.write_str("...")?;
}
if !mult.is_required() {
f.write_str("?")?;
}
}
Ok(())
}
}
impl ArgRep {
fn is_required(&self) -> bool {
matches!(self, ArgRep::Required | ArgRep::Repeated(true))
}
fn is_repeated(&self) -> bool {
matches!(self, ArgRep::Repeated(_))
}
fn optional(&self) -> Self {
match self {
ArgRep::Optional => ArgRep::Optional,
ArgRep::Required => ArgRep::Optional,
ArgRep::Repeated(_) => ArgRep::Repeated(false),
}
}
}
#[test]
fn test_empty() {
let parser = ProcArgParser::default();
let mut args = parser.parse_iter(&[]);
assert!(args.next().is_none());
}
#[test]
fn test_positional() -> JimResult<()> {
let interp = Interp::new()?;
let parser = ProcArgParser::default();
let input = interp.new_string("testproc {NR 9}");
let input: Vec<_> = input.list_iter().collect();
let mut args = parser.parse_iter(&input);
let arg = args.next().expect("unexpected end of iterator")?;
if let ParsedArgument::Positional(arg) = arg {
assert_eq!(arg.as_str()?, "NR 9");
} else {
panic!("argument {:?} is not positional", arg);
}
Ok(())
}
#[test]
fn test_flag_only() -> JimResult<()> {
let interp = Interp::new()?;
let mut parser = ProcArgParser::default();
parser.add_flag("-no");
let input = interp.new_string("testproc -no");
let input: Vec<_> = input.list_iter().collect();
let mut args = parser.parse_iter(&input);
let arg = args.next().expect("unexpected end of iterator")?;
assert_matches!(arg, ParsedArgument::Flag("-no"));
assert!(args.next().is_none());
Ok(())
}
#[test]
fn test_option_only() -> JimResult<()> {
let interp = Interp::new()?;
let mut parser = ProcArgParser::default();
parser.add_option("-name", "name");
let input = interp.new_string("testproc -name {NR 9}");
let input: Vec<_> = input.list_iter().collect();
let mut args = parser.parse_iter(&input);
let arg = args.next().expect("unexpected end of iterator")?;
if let ParsedArgument::Option("-name", arg) = arg {
assert_eq!(arg.as_str()?, "NR 9");
} else {
panic!("argument {:?} is not an option", arg);
}
assert!(args.next().is_none());
Ok(())
}
#[test]
fn test_flag_value() -> JimResult<()> {
let interp = Interp::new()?;
let mut parser = ProcArgParser::default();
parser.add_flag("-nope");
let input = interp.new_string("testproc -nope {NR 9}");
let input: Vec<_> = input.list_iter().collect();
let mut args = parser.parse_iter(&input);
let arg = args.next().expect("unexpected end of iterator")?;
assert_matches!(arg, ParsedArgument::Flag("-nope"));
let arg = args.next().expect("unexpected end of iterator")?;
if let ParsedArgument::Positional(arg) = arg {
assert_eq!(arg.as_str()?, "NR 9");
} else {
panic!("argument {:?} is not positional", arg);
}
assert!(args.next().is_none());
Ok(())
}
#[test]
fn test_unknown_flag_error() -> JimResult<()> {
let interp = Interp::new()?;
let mut parser = ProcArgParser::default();
parser.add_flag("-nope");
let input = interp.new_string("testproc -yep {NR 9}");
let input: Vec<_> = input.list_iter().collect();
let mut args = parser.parse_iter(&input);
let arg = args.next().expect("unexpected end of iterator");
assert!(arg.is_err());
assert!(args.next().is_none());
Ok(())
}
#[test]
fn test_double_hyphen_skips() -> JimResult<()> {
let interp = Interp::new()?;
let mut parser = ProcArgParser::default();
parser.add_flag("-nope");
parser.add_option("-ident", "name");
let input = interp.new_string("testproc -nope -- -ident {NR 9}");
let input: Vec<_> = input.list_iter().collect();
let mut args = parser.parse_iter(&input);
let arg = args.next().expect("unexpected end of iterator")?;
assert_matches!(arg, ParsedArgument::Flag("-nope"));
let arg = args.next().expect("unexpected end of iterator")?;
assert_matches!(arg, ParsedArgument::Positional(_));
let arg = args.next().expect("unexpected end of iterator")?;
assert_matches!(arg, ParsedArgument::Positional(_));
assert!(args.next().is_none());
Ok(())
}