use std::cell::RefCell;
use std::error::Error as StdError;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::result::Result as StdResult;
use std::str::FromStr;
pub struct Options<'a, S>
where
S: AsRef<str>,
{
args: &'a [S],
inner: RefCell<OptionsInner<'a>>,
}
struct OptionsInner<'a> {
position: usize,
state: State<'a>,
}
#[derive(Copy, Clone)]
enum State<'a> {
Start,
EndOfOption(Opt<'a>),
ShortOptionCluster(Opt<'a>, usize),
LongOptionWithValue(Opt<'a>, usize),
}
impl<'a> State<'a> {
fn last_opt(&self) -> Opt<'a> {
match *self {
State::Start => panic!("called last_opt() on State::Start"),
State::EndOfOption(opt) => opt,
State::ShortOptionCluster(opt, _) => opt,
State::LongOptionWithValue(opt, _) => opt,
}
}
}
impl<'a, S> Options<'a, S>
where
S: AsRef<str>,
{
pub fn new(args: &[S]) -> Options<S> {
Options {
args,
inner: RefCell::new(OptionsInner {
position: 0,
state: State::Start,
}),
}
}
pub fn next(&self) -> Option<Result<Opt<'a>>> {
let mut inner = self.inner.borrow_mut();
match inner.state {
State::Start | State::EndOfOption(_) => {
if inner.position >= self.args.len() {
inner.state = State::Start;
return None;
}
let arg = self.args[inner.position].as_ref();
if arg == "--" {
inner.position += 1;
inner.state = State::Start;
None
} else if arg == "-" {
inner.state = State::Start;
None
} else if arg.starts_with("--") {
if let Some(equals) = arg.find('=') {
let opt = Opt::Long(&arg[2..equals]);
inner.state = State::LongOptionWithValue(opt, equals + 1);
Some(Ok(opt))
} else {
let opt = Opt::Long(&arg[2..]);
inner.position += 1;
inner.state = State::EndOfOption(opt);
Some(Ok(opt))
}
} else if arg.starts_with("-") {
let ch = arg[1..].chars().next().unwrap();
let opt = Opt::Short(ch);
let index = 1 + ch.len_utf8();
if index >= arg.len() {
inner.position += 1;
inner.state = State::EndOfOption(opt);
} else {
inner.state = State::ShortOptionCluster(opt, index);
}
Some(Ok(opt))
} else {
inner.state = State::Start;
None
}
}
State::ShortOptionCluster(_, index) => {
let arg = self.args[inner.position].as_ref();
let ch = arg[index..].chars().next().unwrap();
let opt = Opt::Short(ch);
let index = index + ch.len_utf8();
if index >= arg.len() {
inner.position += 1;
inner.state = State::EndOfOption(opt);
} else {
inner.state = State::ShortOptionCluster(opt, index);
}
Some(Ok(opt))
}
State::LongOptionWithValue(opt, _) => Some(Err(Error::DoesNotRequireValue(opt))),
}
}
pub fn value_str(&self) -> Result<&'a str> {
let mut inner = self.inner.borrow_mut();
match inner.state {
State::Start => panic!("called value() with no previous option"),
State::EndOfOption(opt) => {
if inner.position >= self.args.len() {
Err(Error::RequiresValue(opt))
} else {
let val = self.args[inner.position].as_ref();
inner.position += 1;
inner.state = State::Start;
Ok(val)
}
}
State::ShortOptionCluster(_, index) | State::LongOptionWithValue(_, index) => {
let val = &self.args[inner.position].as_ref()[index..];
inner.position += 1;
inner.state = State::Start;
Ok(val)
}
}
}
pub fn value<T>(&self) -> Result<T>
where
T: FromStr,
T::Err: Display,
{
let opt = self.inner.borrow().state.last_opt();
let value = self.value_str()?;
T::from_str(value).map_err(|e| Error::InvalidValue {
opt,
desc: format!("{}", e),
value,
})
}
pub fn arg_str(&self) -> Option<&'a S> {
let mut inner = self.inner.borrow_mut();
match inner.state {
State::Start => {
if inner.position >= self.args.len() {
None
} else {
let arg = &self.args[inner.position];
inner.position += 1;
Some(arg)
}
}
_ => panic!("called arg() while option parsing hasn't finished"),
}
}
pub fn arg<T>(&self) -> Option<Result<T>>
where
T: FromStr,
T::Err: Display,
{
let arg = self.arg_str()?.as_ref();
match T::from_str(arg) {
Ok(v) => Some(Ok(v)),
Err(e) => Some(Err(Error::InvalidArg {
desc: format!("{}", e),
value: arg,
})),
}
}
pub fn args(&self) -> &'a [S] {
let inner = self.inner.borrow();
match inner.state {
State::Start => &self.args[inner.position..],
_ => panic!("called args() while option parsing hasn't finished"),
}
}
}
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)]
pub enum Opt<'a> {
Short(char),
Long(&'a str),
}
impl Display for Opt<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Opt::Short(c) => write!(f, "-{}", c),
Opt::Long(s) => write!(f, "--{}", s),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error<'a> {
UnknownOpt(Opt<'a>),
RequiresValue(Opt<'a>),
DoesNotRequireValue(Opt<'a>),
InvalidValue {
opt: Opt<'a>,
desc: String,
value: &'a str,
},
InvalidArg { desc: String, value: &'a str },
}
impl Display for Error<'_> {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
match self {
Error::UnknownOpt(opt) => write!(f, "unknown option: {}", opt),
Error::RequiresValue(opt) => write!(f, "option requires a value: {}", opt),
Error::DoesNotRequireValue(opt) => {
write!(f, "option does not require a value: {}", opt)
}
Error::InvalidValue { opt, desc, value } => {
write!(f, "invalid value for option '{}': {}: {}", opt, desc, value)
}
Error::InvalidArg { desc, value } => {
write!(f, "invalid value for argument: {}: {}", desc, value)
}
}
}
}
impl StdError for Error<'_> {}
pub type Result<'a, T> = StdResult<T, Error<'a>>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_options() {
let args = ["foo", "bar"];
let opts = Options::new(&args);
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"foo"));
assert_eq!(opts.arg_str(), Some(&"bar"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn short_options() {
let args = ["-a", "-b", "-3", "-@", "bar"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.next(), Some(Ok(Opt::Short('b'))));
assert_eq!(opts.next(), Some(Ok(Opt::Short('3'))));
assert_eq!(opts.next(), Some(Ok(Opt::Short('@'))));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"bar"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn short_cluster() {
let args = ["-ab3@", "bar"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.next(), Some(Ok(Opt::Short('b'))));
assert_eq!(opts.next(), Some(Ok(Opt::Short('3'))));
assert_eq!(opts.next(), Some(Ok(Opt::Short('@'))));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"bar"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn long_options() {
let args = ["--ay", "--bee", "--see", "--@3", "bar"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Long("ay"))));
assert_eq!(opts.next(), Some(Ok(Opt::Long("bee"))));
assert_eq!(opts.next(), Some(Ok(Opt::Long("see"))));
assert_eq!(opts.next(), Some(Ok(Opt::Long("@3"))));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"bar"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn short_option_with_value() {
let args = ["-a", "ay", "-b", "bee", "bar"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.value_str(), Ok("ay"));
assert_eq!(opts.next(), Some(Ok(Opt::Short('b'))));
assert_eq!(opts.value_str(), Ok("bee"));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"bar"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn short_cluster_with_value() {
let args = ["-aay", "-3bbee", "bar"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.value_str(), Ok("ay"));
assert_eq!(opts.next(), Some(Ok(Opt::Short('3'))));
assert_eq!(opts.next(), Some(Ok(Opt::Short('b'))));
assert_eq!(opts.value_str(), Ok("bee"));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"bar"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn long_option_with_value() {
let args = ["--ay", "Ay", "--bee=Bee", "--see", "See", "bar"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Long("ay"))));
assert_eq!(opts.value_str(), Ok("Ay"));
assert_eq!(opts.next(), Some(Ok(Opt::Long("bee"))));
assert_eq!(opts.value_str(), Ok("Bee"));
assert_eq!(opts.next(), Some(Ok(Opt::Long("see"))));
assert_eq!(opts.value_str(), Ok("See"));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"bar"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn value_with_dash() {
let args = [
"-a",
"-ay",
"--bee=--Bee",
"--see",
"--See",
"-d-dee",
"bar",
];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.value_str(), Ok("-ay"));
assert_eq!(opts.next(), Some(Ok(Opt::Long("bee"))));
assert_eq!(opts.value_str(), Ok("--Bee"));
assert_eq!(opts.next(), Some(Ok(Opt::Long("see"))));
assert_eq!(opts.value_str(), Ok("--See"));
assert_eq!(opts.next(), Some(Ok(Opt::Short('d'))));
assert_eq!(opts.value_str(), Ok("-dee"));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"bar"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn no_positional() {
let args = ["-a", "ay"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.value_str(), Ok("ay"));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), None);
}
#[test]
fn long_option_with_empty_value() {
let args = ["--ay=", "--bee", "", "bar"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Long("ay"))));
assert_eq!(opts.value_str(), Ok(""));
assert_eq!(opts.next(), Some(Ok(Opt::Long("bee"))));
assert_eq!(opts.value_str(), Ok(""));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"bar"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn short_option_with_missing_value() {
let args = ["-a"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.value_str(), Err(Error::RequiresValue(Opt::Short('a'))));
}
#[test]
fn long_option_with_unexpected_value() {
let args = ["--ay=Ay", "bar"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Long("ay"))));
assert_eq!(
opts.next(),
Some(Err(Error::DoesNotRequireValue(Opt::Long("ay")))),
);
}
#[test]
fn long_option_with_missing_value() {
let args = ["--ay"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Long("ay"))));
assert_eq!(opts.value_str(), Err(Error::RequiresValue(Opt::Long("ay"))));
}
#[test]
fn short_option_at_end() {
let args = ["-a"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), None);
}
#[test]
fn long_option_at_end() {
let args = ["--ay"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Long("ay"))));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), None);
}
#[test]
fn end_of_options() {
let args = ["-a", "--bee", "--", "--see", "-d"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.next(), Some(Ok(Opt::Long("bee"))));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"--see"));
assert_eq!(opts.arg_str(), Some(&"-d"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn lone_dash() {
let args = ["-a", "--bee", "-", "--see", "-d"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.next(), Some(Ok(Opt::Long("bee"))));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"-"));
assert_eq!(opts.arg_str(), Some(&"--see"));
assert_eq!(opts.arg_str(), Some(&"-d"));
assert_eq!(opts.arg_str(), None);
}
#[test]
fn parse_value() {
let args = ["-a", "3.14", "--bee=5"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.value::<f64>(), Ok(3.14));
assert_eq!(opts.next(), Some(Ok(Opt::Long("bee"))));
assert_eq!(opts.value::<i32>(), Ok(5));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), None);
}
#[test]
fn parse_arg() {
let args = ["-a", "3.14", "5"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg::<f64>(), Some(Ok(3.14)));
assert_eq!(opts.arg::<i32>(), Some(Ok(5)));
assert_eq!(opts.arg::<i32>(), None);
}
#[test]
fn parse_invalid_value() {
let args = ["-a", "3.14.1"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(
opts.value::<f64>(),
Err(Error::InvalidValue {
opt: Opt::Short('a'),
desc: "invalid float literal".to_string(),
value: "3.14.1",
})
);
}
#[test]
fn parse_invalid_arg() {
let args = ["-a", "3.14.1"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.next(), None);
assert_eq!(
opts.arg::<f64>(),
Some(Err(Error::InvalidArg {
desc: "invalid float literal".to_string(),
value: "3.14.1",
}))
);
}
#[test]
fn subcommand() {
let args = ["-a", "cmd", "-b", "arg"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"cmd"));
assert_eq!(opts.next(), Some(Ok(Opt::Short('b'))));
assert_eq!(opts.next(), None);
assert_eq!(opts.arg_str(), Some(&"arg"));
}
#[test]
fn keep_retrieving_options() {
let args = ["-a", "ay", "ay2", "bar"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.next(), None);
assert_eq!(opts.next(), None);
assert_eq!(opts.next(), None);
assert_eq!(opts.next(), None);
}
#[test]
fn keep_retrieving_options_2() {
let args = ["-a", "--", "-b", "--see"];
let opts = Options::new(&args);
assert_eq!(opts.next(), Some(Ok(Opt::Short('a'))));
assert_eq!(opts.next(), None);
assert_eq!(opts.next(), Some(Ok(Opt::Short('b'))));
assert_eq!(opts.next(), Some(Ok(Opt::Long("see"))));
assert_eq!(opts.next(), None);
}
#[test]
#[should_panic]
fn keep_taking_values() {
let args = ["-a", "ay", "ay2", "bar"];
let opts = Options::new(&args);
let _ = opts.next();
let _ = opts.value_str();
let _ = opts.value_str();
}
#[test]
fn keep_taking_args() {
let args = ["-a", "ay"];
let opts = Options::new(&args);
assert_eq!(opts.arg_str(), Some(&"-a"));
assert_eq!(opts.arg_str(), Some(&"ay"));
assert_eq!(opts.arg_str(), None);
assert_eq!(opts.arg_str(), None);
assert_eq!(opts.arg_str(), None);
assert_eq!(opts.arg_str(), None);
}
#[test]
#[should_panic]
fn value_without_option() {
let args = ["-a", "ay"];
let opts = Options::new(&args);
let _ = opts.value_str();
}
#[test]
#[should_panic]
fn value_after_arg() {
let args = ["ay", "bee"];
let opts = Options::new(&args);
let _ = opts.arg_str();
let _ = opts.value_str();
}
}