#![doc(
html_logo_url = "https://www.rust-lang.org/logos/rust-logo-128x128-blk-v2.png",
html_favicon_url = "https://www.rust-lang.org/favicon.ico"
)]
#![deny(missing_docs)]
#![cfg_attr(test, deny(warnings))]
#[cfg(test)]
#[macro_use]
extern crate log;
use self::Fail::*;
use self::HasArg::*;
use self::Name::*;
use self::Occur::*;
use self::Optval::*;
use std::error::Error;
use std::ffi::OsStr;
use std::fmt;
use std::iter::{repeat, IntoIterator};
use std::result;
use std::str::FromStr;
#[cfg(feature = "unicode")]
use unicode_width::UnicodeWidthStr;
#[cfg(not(feature = "unicode"))]
trait UnicodeWidthStr {
fn width(&self) -> usize;
}
#[cfg(not(feature = "unicode"))]
impl UnicodeWidthStr for str {
fn width(&self) -> usize {
self.len()
}
}
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Options {
grps: Vec<OptGroup>,
parsing_style: ParsingStyle,
long_only: bool,
}
impl Default for Options {
fn default() -> Self {
Self::new()
}
}
impl Options {
pub fn new() -> Options {
Options {
grps: Vec::new(),
parsing_style: ParsingStyle::FloatingFrees,
long_only: false,
}
}
pub fn parsing_style(&mut self, style: ParsingStyle) -> &mut Options {
self.parsing_style = style;
self
}
pub fn long_only(&mut self, long_only: bool) -> &mut Options {
self.long_only = long_only;
self
}
pub fn opt(
&mut self,
short_name: &str,
long_name: &str,
desc: &str,
hint: &str,
hasarg: HasArg,
occur: Occur,
) -> &mut Options {
validate_names(short_name, long_name);
self.grps.push(OptGroup {
short_name: short_name.to_string(),
long_name: long_name.to_string(),
hint: hint.to_string(),
desc: desc.to_string(),
hasarg,
occur,
});
self
}
pub fn optflag(&mut self, short_name: &str, long_name: &str, desc: &str) -> &mut Options {
validate_names(short_name, long_name);
self.grps.push(OptGroup {
short_name: short_name.to_string(),
long_name: long_name.to_string(),
hint: "".to_string(),
desc: desc.to_string(),
hasarg: No,
occur: Optional,
});
self
}
pub fn optflagmulti(&mut self, short_name: &str, long_name: &str, desc: &str) -> &mut Options {
validate_names(short_name, long_name);
self.grps.push(OptGroup {
short_name: short_name.to_string(),
long_name: long_name.to_string(),
hint: "".to_string(),
desc: desc.to_string(),
hasarg: No,
occur: Multi,
});
self
}
pub fn optflagopt(
&mut self,
short_name: &str,
long_name: &str,
desc: &str,
hint: &str,
) -> &mut Options {
validate_names(short_name, long_name);
self.grps.push(OptGroup {
short_name: short_name.to_string(),
long_name: long_name.to_string(),
hint: hint.to_string(),
desc: desc.to_string(),
hasarg: Maybe,
occur: Optional,
});
self
}
pub fn optmulti(
&mut self,
short_name: &str,
long_name: &str,
desc: &str,
hint: &str,
) -> &mut Options {
validate_names(short_name, long_name);
self.grps.push(OptGroup {
short_name: short_name.to_string(),
long_name: long_name.to_string(),
hint: hint.to_string(),
desc: desc.to_string(),
hasarg: Yes,
occur: Multi,
});
self
}
pub fn optopt(
&mut self,
short_name: &str,
long_name: &str,
desc: &str,
hint: &str,
) -> &mut Options {
validate_names(short_name, long_name);
self.grps.push(OptGroup {
short_name: short_name.to_string(),
long_name: long_name.to_string(),
hint: hint.to_string(),
desc: desc.to_string(),
hasarg: Yes,
occur: Optional,
});
self
}
pub fn reqopt(
&mut self,
short_name: &str,
long_name: &str,
desc: &str,
hint: &str,
) -> &mut Options {
validate_names(short_name, long_name);
self.grps.push(OptGroup {
short_name: short_name.to_string(),
long_name: long_name.to_string(),
hint: hint.to_string(),
desc: desc.to_string(),
hasarg: Yes,
occur: Req,
});
self
}
pub fn parse<C: IntoIterator>(&self, args: C) -> Result
where
C::Item: AsRef<OsStr>,
{
let opts: Vec<Opt> = self.grps.iter().map(|x| x.long_to_short()).collect();
let mut vals = (0..opts.len())
.map(|_| Vec::new())
.collect::<Vec<Vec<(usize, Optval)>>>();
let mut free: Vec<String> = Vec::new();
let mut args_end = None;
let args = args
.into_iter()
.map(|i| {
i.as_ref()
.to_str()
.ok_or_else(|| Fail::UnrecognizedOption(format!("{:?}", i.as_ref())))
.map(|s| s.to_owned())
})
.collect::<::std::result::Result<Vec<_>, _>>()?;
let mut args = args.into_iter().peekable();
let mut arg_pos = 0;
while let Some(cur) = args.next() {
if !is_arg(&cur) {
free.push(cur);
match self.parsing_style {
ParsingStyle::FloatingFrees => {}
ParsingStyle::StopAtFirstFree => {
free.extend(args);
break;
}
}
} else if cur == "--" {
args_end = Some(free.len());
free.extend(args);
break;
} else {
let mut name = None;
let mut i_arg = None;
let mut was_long = true;
if cur.as_bytes()[1] == b'-' || self.long_only {
let tail = if cur.as_bytes()[1] == b'-' {
&cur[2..]
} else {
assert!(self.long_only);
&cur[1..]
};
let mut parts = tail.splitn(2, '=');
name = Some(Name::from_str(parts.next().unwrap()));
if let Some(rest) = parts.next() {
i_arg = Some(rest.to_string());
}
} else {
was_long = false;
for (j, ch) in cur.char_indices().skip(1) {
let opt = Short(ch);
let opt_id = match find_opt(&opts, &opt) {
Some(id) => id,
None => return Err(UnrecognizedOption(opt.to_string())),
};
let arg_follows = match opts[opt_id].hasarg {
Yes | Maybe => true,
No => false,
};
if arg_follows {
name = Some(opt);
let next = j + ch.len_utf8();
if next < cur.len() {
i_arg = Some(cur[next..].to_string());
break;
}
} else {
vals[opt_id].push((arg_pos, Given));
}
}
}
if let Some(nm) = name {
let opt_id = match find_opt(&opts, &nm) {
Some(id) => id,
None => return Err(UnrecognizedOption(nm.to_string())),
};
match opts[opt_id].hasarg {
No => {
if i_arg.is_some() {
return Err(UnexpectedArgument(nm.to_string()));
}
vals[opt_id].push((arg_pos, Given));
}
Maybe => {
if let Some(i_arg) = i_arg.take() {
vals[opt_id].push((arg_pos, Val(i_arg)));
} else if was_long || args.peek().map_or(true, |n| is_arg(&n)) {
vals[opt_id].push((arg_pos, Given));
} else {
vals[opt_id].push((arg_pos, Val(args.next().unwrap())));
}
}
Yes => {
if let Some(i_arg) = i_arg.take() {
vals[opt_id].push((arg_pos, Val(i_arg)));
} else if let Some(n) = args.next() {
vals[opt_id].push((arg_pos, Val(n)));
} else {
return Err(ArgumentMissing(nm.to_string()));
}
}
}
}
}
arg_pos += 1;
}
debug_assert_eq!(vals.len(), opts.len());
for (vals, opt) in vals.iter().zip(opts.iter()) {
if opt.occur == Req && vals.is_empty() {
return Err(OptionMissing(opt.name.to_string()));
}
if opt.occur != Multi && vals.len() > 1 {
return Err(OptionDuplicated(opt.name.to_string()));
}
}
args_end = args_end.filter(|pos| pos != &free.len());
Ok(Matches {
opts,
vals,
free,
args_end,
})
}
pub fn short_usage(&self, program_name: &str) -> String {
let mut line = format!("Usage: {} ", program_name);
line.push_str(
&self
.grps
.iter()
.map(format_option)
.collect::<Vec<String>>()
.join(" "),
);
line
}
pub fn usage(&self, brief: &str) -> String {
self.usage_with_format(|opts| {
format!(
"{}\n\nOptions:\n{}\n",
brief,
opts.collect::<Vec<String>>().join("\n")
)
})
}
pub fn usage_with_format<F: FnMut(&mut dyn Iterator<Item = String>) -> String>(
&self,
mut formatter: F,
) -> String {
formatter(&mut self.usage_items())
}
fn usage_items<'a>(&'a self) -> Box<dyn Iterator<Item = String> + 'a> {
let desc_sep = format!("\n{}", repeat(" ").take(24).collect::<String>());
let any_short = self.grps.iter().any(|optref| !optref.short_name.is_empty());
let rows = self.grps.iter().map(move |optref| {
let OptGroup {
short_name,
long_name,
hint,
desc,
hasarg,
..
} = (*optref).clone();
let mut row = " ".to_string();
match short_name.width() {
0 => {
if any_short {
row.push_str(" ");
}
}
1 => {
row.push('-');
row.push_str(&short_name);
if long_name.width() > 0 {
row.push_str(", ");
} else {
row.push(' ');
}
}
_ => panic!("the short name should only be 1 ascii char long"),
}
match long_name.width() {
0 => {}
_ => {
row.push_str(if self.long_only { "-" } else { "--" });
row.push_str(&long_name);
row.push(' ');
}
}
match hasarg {
No => {}
Yes => row.push_str(&hint),
Maybe => {
row.push('[');
row.push_str(&hint);
row.push(']');
}
}
let rowlen = row.width();
if rowlen < 24 {
for _ in 0..24 - rowlen {
row.push(' ');
}
} else {
row.push_str(&desc_sep)
}
let desc_rows = each_split_within(&desc, 54);
row.push_str(&desc_rows.join(&desc_sep));
row
});
Box::new(rows)
}
}
fn validate_names(short_name: &str, long_name: &str) {
let len = short_name.len();
assert!(
len == 1 || len == 0,
"the short_name (first argument) should be a single character, \
or an empty string for none"
);
let len = long_name.len();
assert!(
len == 0 || len > 1,
"the long_name (second argument) should be longer than a single \
character, or an empty string for none"
);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParsingStyle {
FloatingFrees,
StopAtFirstFree,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Name {
Long(String),
Short(char),
}
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum HasArg {
Yes,
No,
Maybe,
}
#[derive(Clone, Debug, Copy, PartialEq, Eq)]
pub enum Occur {
Req,
Optional,
Multi,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Opt {
name: Name,
hasarg: HasArg,
occur: Occur,
aliases: Vec<Opt>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct OptGroup {
short_name: String,
long_name: String,
hint: String,
desc: String,
hasarg: HasArg,
occur: Occur,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Optval {
Val(String),
Given,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Matches {
opts: Vec<Opt>,
vals: Vec<Vec<(usize, Optval)>>,
pub free: Vec<String>,
args_end: Option<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Fail {
ArgumentMissing(String),
UnrecognizedOption(String),
OptionMissing(String),
OptionDuplicated(String),
UnexpectedArgument(String),
}
impl Error for Fail {}
pub type Result = result::Result<Matches, Fail>;
impl Name {
fn from_str(nm: &str) -> Name {
if nm.len() == 1 {
Short(nm.as_bytes()[0] as char)
} else {
Long(nm.to_string())
}
}
fn to_string(&self) -> String {
match *self {
Short(ch) => ch.to_string(),
Long(ref s) => s.to_string(),
}
}
}
impl OptGroup {
fn long_to_short(&self) -> Opt {
let OptGroup {
short_name,
long_name,
hasarg,
occur,
..
} = (*self).clone();
match (short_name.len(), long_name.len()) {
(0, 0) => panic!("this long-format option was given no name"),
(0, _) => Opt {
name: Long(long_name),
hasarg,
occur,
aliases: Vec::new(),
},
(1, 0) => Opt {
name: Short(short_name.as_bytes()[0] as char),
hasarg,
occur,
aliases: Vec::new(),
},
(1, _) => Opt {
name: Long(long_name),
hasarg,
occur,
aliases: vec![Opt {
name: Short(short_name.as_bytes()[0] as char),
hasarg: hasarg,
occur: occur,
aliases: Vec::new(),
}],
},
(_, _) => panic!("something is wrong with the long-form opt"),
}
}
}
impl Matches {
fn opt_vals(&self, nm: &str) -> Vec<(usize, Optval)> {
match find_opt(&self.opts, &Name::from_str(nm)) {
Some(id) => self.vals[id].clone(),
None => panic!("No option '{}' defined", nm),
}
}
fn opt_val(&self, nm: &str) -> Option<Optval> {
self.opt_vals(nm).into_iter().map(|(_, o)| o).next()
}
pub fn opt_defined(&self, name: &str) -> bool {
find_opt(&self.opts, &Name::from_str(name)).is_some()
}
pub fn opt_present(&self, name: &str) -> bool {
!self.opt_vals(name).is_empty()
}
pub fn opt_count(&self, name: &str) -> usize {
self.opt_vals(name).len()
}
pub fn opt_positions(&self, name: &str) -> Vec<usize> {
self.opt_vals(name)
.into_iter()
.map(|(pos, _)| pos)
.collect()
}
pub fn opts_present(&self, names: &[String]) -> bool {
names
.iter()
.any(|nm| match find_opt(&self.opts, &Name::from_str(&nm)) {
Some(id) if !self.vals[id].is_empty() => true,
_ => false,
})
}
pub fn opts_present_any<C: IntoIterator>(&self, names: C) -> bool
where
C::Item: AsRef<str>,
{
names
.into_iter()
.any(|nm| !self.opt_vals(nm.as_ref()).is_empty())
}
pub fn opts_str(&self, names: &[String]) -> Option<String> {
names
.iter()
.filter_map(|nm| match self.opt_val(&nm) {
Some(Val(s)) => Some(s),
_ => None,
})
.next()
}
pub fn opts_str_first<C: IntoIterator>(&self, names: C) -> Option<String>
where
C::Item: AsRef<str>,
{
names
.into_iter()
.filter_map(|nm| match self.opt_val(nm.as_ref()) {
Some(Val(s)) => Some(s),
_ => None,
})
.next()
}
pub fn opt_strs(&self, name: &str) -> Vec<String> {
self.opt_vals(name)
.into_iter()
.filter_map(|(_, v)| match v {
Val(s) => Some(s),
_ => None,
})
.collect()
}
pub fn opt_strs_pos(&self, name: &str) -> Vec<(usize, String)> {
self.opt_vals(name)
.into_iter()
.filter_map(|(p, v)| match v {
Val(s) => Some((p, s)),
_ => None,
})
.collect()
}
pub fn opt_str(&self, name: &str) -> Option<String> {
match self.opt_val(name) {
Some(Val(s)) => Some(s),
_ => None,
}
}
pub fn opt_default(&self, name: &str, def: &str) -> Option<String> {
match self.opt_val(name) {
Some(Val(s)) => Some(s),
Some(_) => Some(def.to_string()),
None => None,
}
}
pub fn opt_get<T>(&self, name: &str) -> result::Result<Option<T>, T::Err>
where
T: FromStr,
{
match self.opt_val(name) {
Some(Val(s)) => Ok(Some(s.parse()?)),
Some(Given) => Ok(None),
None => Ok(None),
}
}
pub fn opt_get_default<T>(&self, name: &str, def: T) -> result::Result<T, T::Err>
where
T: FromStr,
{
match self.opt_val(name) {
Some(Val(s)) => s.parse(),
Some(Given) => Ok(def),
None => Ok(def),
}
}
pub fn free_trailing_start(&self) -> Option<usize> {
self.args_end
}
}
fn is_arg(arg: &str) -> bool {
arg.as_bytes().get(0) == Some(&b'-') && arg.len() > 1
}
fn find_opt(opts: &[Opt], nm: &Name) -> Option<usize> {
let pos = opts.iter().position(|opt| &opt.name == nm);
if pos.is_some() {
return pos;
}
for candidate in opts.iter() {
if candidate.aliases.iter().any(|opt| &opt.name == nm) {
return opts.iter().position(|opt| opt.name == candidate.name);
}
}
None
}
impl fmt::Display for Fail {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ArgumentMissing(ref nm) => write!(f, "Argument to option '{}' missing", *nm),
UnrecognizedOption(ref nm) => write!(f, "Unrecognized option: '{}'", *nm),
OptionMissing(ref nm) => write!(f, "Required option '{}' missing", *nm),
OptionDuplicated(ref nm) => write!(f, "Option '{}' given more than once", *nm),
UnexpectedArgument(ref nm) => write!(f, "Option '{}' does not take an argument", *nm),
}
}
}
fn format_option(opt: &OptGroup) -> String {
let mut line = String::new();
if opt.occur != Req {
line.push('[');
}
if !opt.short_name.is_empty() {
line.push('-');
line.push_str(&opt.short_name);
} else {
line.push_str("--");
line.push_str(&opt.long_name);
}
if opt.hasarg != No {
line.push(' ');
if opt.hasarg == Maybe {
line.push('[');
}
line.push_str(&opt.hint);
if opt.hasarg == Maybe {
line.push(']');
}
}
if opt.occur != Req {
line.push(']');
}
if opt.occur == Multi {
line.push_str("..");
}
line
}
fn each_split_within(desc: &str, lim: usize) -> Vec<String> {
let mut rows = Vec::new();
for line in desc.trim().lines() {
let line_chars = line.chars().chain(Some(' '));
let words = line_chars
.fold((Vec::new(), 0, 0), |(mut words, a, z), c| {
let idx = z + c.len_utf8();
if c.is_whitespace() {
if a != z {
words.push(&line[a..z]);
}
(words, idx, idx)
}
else {
(words, a, idx)
}
})
.0;
let mut row = String::new();
for word in words.iter() {
let sep = if !row.is_empty() { Some(" ") } else { None };
let width = row.width() + word.width() + sep.map(UnicodeWidthStr::width).unwrap_or(0);
if width <= lim {
if let Some(sep) = sep {
row.push_str(sep)
}
row.push_str(word);
continue;
}
if !row.is_empty() {
rows.push(row.clone());
row.clear();
}
row.push_str(word);
}
if !row.is_empty() {
rows.push(row);
}
}
rows
}