#![forbid(unsafe_code)]
#![forbid(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(unused_parens)]
#![allow(non_snake_case)]
#![allow(unused_doc_comments)]
#![deny(rustdoc::bare_urls)]
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(rustdoc::missing_crate_level_docs)]
#![deny(rustdoc::invalid_codeblock_attributes)]
#![deny(rustdoc::invalid_rust_codeblocks)]
use std::io::Result;
use std::io::Error;
use std::io::ErrorKind;
use std::collections::HashMap;
pub struct getopt {
pub options: HashMap<char, String>,
pub arguments: Vec<String>,
pub option_has_arg: HashMap<char, argument>,
}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub enum argument {
NO,
YES,
OPTIONAL,
}
impl getopt {
pub fn len(&self) -> usize {
self.arguments.len()
}
pub fn iter(&self) -> std::slice::Iter<'_, String> {
self.arguments.iter()
}
pub fn get(&self, option: char) -> Option<&String> {
self.options.get(&option)
}
pub fn has(&self, option: char) -> bool {
self.options.contains_key(&option)
}
pub fn is_empty(&self) -> bool {
self.arguments.is_empty()
}
}
impl std::ops::Index<usize> for getopt {
type Output = String;
fn index(&self, index: usize) -> &Self::Output {
self.arguments.index(index)
}
}
impl IntoIterator for getopt {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.arguments.into_iter()
}
}
impl<'a> IntoIterator for &'a getopt {
type Item = &'a String;
type IntoIter = std::slice::Iter<'a, String>;
fn into_iter(self) -> Self::IntoIter {
self.arguments.iter()
}
}
pub fn new(arg: impl IntoIterator<Item = impl AsRef<str>>, optstring: impl AsRef<str>) -> Result<getopt> {
let mut opts = HashMap::new();
let mut args = Vec::new();
let mut next_opt: Option<char> = None;
let mut stop_parsing = false;
let options_map: HashMap<char,argument> = build_options_map(validate_optstring(optstring.as_ref())?);
let posix: bool = optstring.as_ref().starts_with("+") || std::env::var("POSIXLY_CORRECT").is_ok();
for el in arg {
let element = el.as_ref();
if stop_parsing {
args.push(element.to_string());
} else if let Some(next_opt_char) = next_opt {
match options_map.get(&next_opt_char) {
Some(argument::YES) => {
opts.insert(next_opt_char, element.to_string());
next_opt = None;
},
Some(argument::OPTIONAL) => {
if is_option(element, &options_map) {
opts.insert(next_opt_char, String::from(""));
next_opt = Some(element.as_bytes()[1] as char);
} else if element.eq("--") {
opts.insert(next_opt_char, String::from(""));
stop_parsing = true;
next_opt = None;
} else {
opts.insert(next_opt_char, element.to_string());
next_opt = None;
}
},
Some(argument::NO) => {
opts.insert(next_opt_char, String::from(""));
if is_option(element, &options_map) {
next_opt = Some(element.as_bytes()[1] as char);
} else if element.eq("--") {
stop_parsing = true;
next_opt = None;
} else {
args.push(element.to_string());
next_opt = None;
if ( posix ) { stop_parsing = true; };
}
}
None => {
let mut s = String::from("-");
s.push(next_opt_char);
args.push(s);
}
}
} else if element.eq("--") {
stop_parsing = true;
next_opt = None;
} else {
if is_option(element, &options_map) {
next_opt = Some(element.as_bytes()[1] as char);
} else {
args.push(element.to_string());
if ( posix ) { stop_parsing = true };
}
}
}
if let Some(next_opt_char) = next_opt {
match options_map.get(&next_opt_char) {
Some(argument::YES) | None => {
let mut s = String::from("-");
s.push(next_opt_char);
args.push(s);
},
Some(argument::NO) | Some(argument::OPTIONAL) => {
opts.insert(next_opt_char, String::from(""));
}
}
}
Ok(getopt {
options: opts,
arguments: args,
option_has_arg: options_map,
})
}
fn is_option(opt: &str, options_map: &HashMap<char, argument>) -> bool {
if ( opt.chars().count() == 2 ) {
if ( opt.starts_with('-') ) {
let second_char: char = opt.chars().nth(1).unwrap();
options_map.contains_key(&second_char)
} else {
false
}
} else {
false
}
}
fn is_possible_option(opt: &str) -> bool {
if ( opt.chars().count() == 2 ) {
if ( opt.starts_with('-') ) {
let possible_optstring: String = opt.chars().skip(1).collect();
validate_optstring(&possible_optstring).is_ok()
} else {
false
}
} else {
false
}
}
fn validate_optstring(optstring: &str) -> Result<&str> {
if optstring.is_empty() {
Err(Error::new(ErrorKind::UnexpectedEof, "optstring can't be empty"))
} else if optstring.eq(":") {
Err(Error::new(ErrorKind::UnexpectedEof, "optstring can't be only ':'"))
} else if optstring.eq("+") {
Err(Error::new(ErrorKind::UnexpectedEof, "optstring can't be only '+'"))
} else if optstring.eq("+:") {
Err(Error::new(ErrorKind::UnexpectedEof, "optstring can't be only '+:'"))
} else {
for c in optstring.chars() {
match c {
'a'..='z' => Ok(()),
'A'..='Z' => Ok(()),
'0'..='9' => Ok(()),
'?' => Ok(()),
':' => Ok(()),
'+' => Ok(()),
_ => Err(Error::new(
ErrorKind::InvalidInput,
"unsupported character in optstring. Only a-z A-Z 0-9 and ?:+ are allowed",
)),
}?
}
let plus = optstring.rfind("+");
if plus.unwrap_or(0) > 0 {
Err(Error::new(ErrorKind::InvalidInput, "plus sign '+' must be first character"))
} else if optstring.contains(":::") {
Err(Error::new(ErrorKind::InvalidInput, "triple ':' are not permited in optstring"))
} else if optstring.starts_with("::") {
Err(Error::new(ErrorKind::InvalidInput, "optstring can't start with '::'"))
} else if optstring.starts_with("+::") {
Err(Error::new(ErrorKind::InvalidInput, "optstring can't start with '+::'"))
} else {
Ok(optstring)
}
}
}
fn build_options_map(optstring: &str) -> HashMap<char, argument> {
let mut rc: HashMap<char, argument> = HashMap::with_capacity(optstring.len());
let mut previous1: char = ':';
let mut previous2: char = ':';
let mut insert_one = |c: char| -> () {
match c {
':' if previous1 != ':' && previous1 != '+' => rc.insert(*&previous1, argument::YES),
':' if previous1 == ':' && previous2 != ':' => rc.insert(*&previous2, argument::OPTIONAL),
'+' => None,
_ if previous1 != ':' && previous1 != '+' => rc.insert(*&previous1, argument::NO),
_ => None,
};
previous2 = previous1;
previous1 = c;
};
for c in optstring.chars() {
insert_one(c);
}
optstring.chars().last().filter(|c| *c != ':').into_iter().for_each(|c| insert_one(c));
rc
}
pub fn validate(getopt: getopt) -> Result<getopt> {
for (opt, _) in getopt.option_has_arg.iter().filter(|(_, arg)| **arg == argument::YES) {
let mut opt_string = String::from("-");
opt_string.push(*opt);
if getopt.arguments.contains(&opt_string) {
return Err(Error::new(
ErrorKind::InvalidInput,
format!("Option -{} does not have required argument", opt),
));
}
}
for opt in getopt.arguments.iter() {
if is_possible_option(opt) {
return Err(Error::new(ErrorKind::InvalidInput, format!("Unknown option -{}", opt)));
}
}
Ok(getopt)
}
pub fn hideBin(argv: impl IntoIterator<Item = String>) -> impl IntoIterator<Item = String> {
argv.into_iter().skip(1)
}
#[cfg(test)]
#[path = "getopt_helpers_tests.rs"]
mod helpers;
#[cfg(test)]
#[path = "getopt_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "getopt_posix_tests.rs"]
mod posix_tests;
#[cfg(test)]
#[path = "getopt_validate_api_tests.rs"]
mod validateapi;
#[cfg(test)]
#[path = "getopt_hidebin_tests.rs"]
mod hidebin;
#[cfg(test)]
#[path = "getopt_doubledash_tests.rs"]
mod doubledash;
#[cfg(test)]
#[path = "getopt_iterator_tests.rs"]
mod it;
#[cfg(test)]
#[path = "getopt_index_tests.rs"]
mod index;