#![doc(html_root_url = "https://docs.rs/pico-args/0.2.0")]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
use std::ffi::{OsString, OsStr};
use std::fmt::{self, Display};
use std::str::FromStr;
#[derive(Clone, Debug)]
pub enum Error {
NonUtf8Argument,
OptionWithoutAValue(&'static str),
#[allow(missing_docs)]
Utf8ArgumentParsingFailed { value: String, cause: String },
#[allow(missing_docs)]
ArgumentParsingFailed { cause: String },
UnusedArgsLeft(Vec<String>),
}
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::NonUtf8Argument => {
write!(f, "argument is not a UTF-8 string")
}
Error::OptionWithoutAValue(key) => {
write!(f, "the '{}' option doesn't have an associated value", key)
}
Error::Utf8ArgumentParsingFailed { value, cause } => {
write!(f, "failed to parse '{}' cause {}", value, cause)
}
Error::ArgumentParsingFailed { cause } => {
write!(f, "failed to parse a binary argument cause {}", cause)
}
Error::UnusedArgsLeft(args) => {
write!(f, "unused arguments left: ")?;
for (i, arg) in args.iter().enumerate() {
write!(f, "{}", arg)?;
if i != args.len() - 1 {
write!(f, ", ")?;
}
}
Ok(())
}
}
}
}
impl std::error::Error for Error {}
#[derive(Clone, Copy, PartialEq)]
enum PairKind {
SingleArgument,
TwoArguments,
}
#[derive(Clone, Debug)]
pub struct Arguments(Vec<OsString>);
impl Arguments {
pub fn from_vec(args: Vec<OsString>) -> Self {
Arguments(args)
}
pub fn from_env() -> Self {
let mut args: Vec<_> = std::env::args_os().collect();
args.remove(0);
Arguments(args)
}
pub fn contains<A: Into<Keys>>(&mut self, keys: A) -> bool {
self.contains_impl(keys.into())
}
#[inline(never)]
fn contains_impl(&mut self, keys: Keys) -> bool {
if let Some((idx, _)) = self.index_of(keys) {
self.0.remove(idx);
return true;
}
false
}
pub fn value_from_str<A, T>(&mut self, keys: A) -> Result<Option<T>, Error>
where
A: Into<Keys>,
T: FromStr,
<T as FromStr>::Err: Display,
{
self.value_from_fn(keys, FromStr::from_str)
}
pub fn value_from_fn<A: Into<Keys>, T, E: Display>(
&mut self,
keys: A,
f: fn(&str) -> Result<T, E>,
) -> Result<Option<T>, Error> {
self.value_from_fn_impl(keys.into(), f)
}
#[inline(never)]
fn value_from_fn_impl<T, E: Display>(
&mut self,
keys: Keys,
f: fn(&str) -> Result<T, E>,
) -> Result<Option<T>, Error> {
match self.find_value(keys)? {
Some((value, kind, idx)) => {
match f(value) {
Ok(value) => {
self.0.remove(idx);
if kind == PairKind::TwoArguments {
self.0.remove(idx);
}
Ok(Some(value))
}
Err(e) => {
Err(Error::Utf8ArgumentParsingFailed {
value: value.to_string(),
cause: error_to_string(e),
})
}
}
}
None => Ok(None),
}
}
#[inline(never)]
fn find_value(
&mut self,
keys: Keys,
) -> Result<Option<(&str, PairKind, usize)>, Error> {
if let Some((idx, key)) = self.index_of(keys) {
let value = match self.0.get(idx + 1) {
Some(v) => v,
None => return Err(Error::OptionWithoutAValue(key)),
};
let value = os_to_str(value)?;
Ok(Some((value, PairKind::TwoArguments, idx)))
} else if let Some((idx, key)) = self.index_of2(keys) {
let value = &self.0[idx];
let value = value.to_str().ok_or_else(|| Error::NonUtf8Argument)?;
let mut value_range = key.len()..value.len();
if value.as_bytes().get(value_range.start) == Some(&b'=') {
value_range.start += 1;
} else {
return Err(Error::OptionWithoutAValue(key));
}
if let Some(c) = value.as_bytes().get(value_range.start).cloned() {
if c == b'"' || c == b'\'' {
value_range.start += 1;
if ends_with(&value[value_range.start..], c) {
value_range.end -= 1;
} else {
return Err(Error::OptionWithoutAValue(key));
}
}
}
if value_range.end - value_range.start == 0 {
return Err(Error::OptionWithoutAValue(key));
}
let value = &value[value_range];
if value.is_empty() {
return Err(Error::OptionWithoutAValue(key));
}
Ok(Some((value, PairKind::SingleArgument, idx)))
} else {
Ok(None)
}
}
pub fn value_from_os_str<A: Into<Keys>, T, E: Display>(
&mut self,
keys: A,
f: fn(&OsStr) -> Result<T, E>,
) -> Result<Option<T>, Error> {
self.value_from_os_str_impl(keys.into(), f)
}
#[inline(never)]
fn value_from_os_str_impl<T, E: Display>(
&mut self,
keys: Keys,
f: fn(&OsStr) -> Result<T, E>,
) -> Result<Option<T>, Error> {
if let Some((idx, key)) = self.index_of(keys) {
let value = match self.0.get(idx + 1) {
Some(v) => v,
None => return Err(Error::OptionWithoutAValue(key)),
};
match f(value) {
Ok(value) => {
self.0.remove(idx);
self.0.remove(idx);
Ok(Some(value))
}
Err(e) => {
Err(Error::ArgumentParsingFailed { cause: error_to_string(e) })
}
}
} else {
Ok(None)
}
}
#[inline(never)]
fn index_of(&self, keys: Keys) -> Option<(usize, &'static str)> {
for key in &keys.0 {
if !key.is_empty() {
if let Some(i) = self.0.iter().position(|v| v == key) {
return Some((i, key));
}
}
}
None
}
#[inline(never)]
fn index_of2(&self, keys: Keys) -> Option<(usize, &'static str)> {
if !keys.first().is_empty() {
if let Some(i) = self.0.iter().position(|v| starts_with_plus_eq(v, keys.first())) {
return Some((i, keys.first()));
}
}
if !keys.second().is_empty() {
if let Some(i) = self.0.iter().position(|v| starts_with_plus_eq(v, keys.second())) {
return Some((i, keys.second()));
}
}
None
}
pub fn free_from_str<T>(&mut self) -> Result<Option<T>, Error>
where
T: FromStr,
<T as FromStr>::Err: Display,
{
self.free_from_fn(FromStr::from_str)
}
#[inline(never)]
pub fn free_from_fn<T, E: Display>(
&mut self,
f: fn(&str) -> Result<T, E>,
) -> Result<Option<T>, Error> {
self.check_for_flags()?;
if self.0.is_empty() {
Ok(None)
} else {
let mut value = OsString::new();
std::mem::swap(self.0.first_mut().unwrap(), &mut value);
self.0.remove(0);
let value = os_to_str(value.as_os_str())?;
match f(&value) {
Ok(value) => Ok(Some(value)),
Err(e) => Err(Error::Utf8ArgumentParsingFailed {
value: value.to_string(),
cause: error_to_string(e),
}),
}
}
}
#[inline(never)]
pub fn free_from_os_str<T, E: Display>(
&mut self,
f: fn(&OsStr) -> Result<T, E>,
) -> Result<Option<T>, Error> {
self.check_for_flags()?;
if self.0.is_empty() {
Ok(None)
} else {
let mut value = OsString::new();
std::mem::swap(self.0.first_mut().unwrap(), &mut value);
self.0.remove(0);
match f(value.as_os_str()) {
Ok(value) => Ok(Some(value)),
Err(e) => Err(Error::ArgumentParsingFailed { cause: error_to_string(e) }),
}
}
}
pub fn free(self) -> Result<Vec<String>, Error> {
self.check_for_flags()?;
for arg in &self.0 {
os_to_str(arg.as_os_str())?;
}
let args = self.0.iter().map(|a| a.to_str().unwrap().to_string()).collect();
Ok(args)
}
pub fn free_os(self) -> Result<Vec<OsString>, Error> {
self.check_for_flags()?;
Ok(self.0)
}
#[inline(never)]
fn check_for_flags(&self) -> Result<(), Error> {
let mut flags_left = Vec::new();
for arg in &self.0 {
if let Some(s) = arg.to_str() {
if s.starts_with('-') && s != "-" {
flags_left.push(s.to_string());
}
}
}
if flags_left.is_empty() {
Ok(())
} else {
Err(Error::UnusedArgsLeft(flags_left))
}
}
pub fn finish(self) -> Result<(), Error> {
if !self.0.is_empty() {
let mut args = Vec::new();
for arg in &self.0 {
if let Some(s) = arg.to_str() {
args.push(s.to_string());
} else {
args.push("binary data".to_string());
}
}
return Err(Error::UnusedArgsLeft(args));
}
Ok(())
}
}
#[inline(never)]
fn error_to_string<E: Display>(e: E) -> String {
e.to_string()
}
#[inline(never)]
fn starts_with_plus_eq(text: &OsStr, prefix: &str) -> bool {
if let Some(s) = text.to_str() {
if s.get(0..prefix.len()) == Some(prefix) {
if s.as_bytes().get(prefix.len()) == Some(&b'=') {
return true;
}
}
}
false
}
#[inline]
fn ends_with(text: &str, c: u8) -> bool {
if text.is_empty() {
false
} else {
text.as_bytes()[text.len() - 1] == c
}
}
#[inline]
fn os_to_str(text: &OsStr) -> Result<&str, Error> {
text.to_str().ok_or_else(|| Error::NonUtf8Argument)
}
#[doc(hidden)]
#[derive(Clone, Copy, Debug)]
pub struct Keys([&'static str; 2]);
impl Keys {
#[inline]
fn first(&self) -> &'static str {
self.0[0]
}
#[inline]
fn second(&self) -> &'static str {
self.0[1]
}
}
impl From<[&'static str; 2]> for Keys {
#[inline]
fn from(v: [&'static str; 2]) -> Self {
debug_assert!(v[0].starts_with("-"), "an argument should start with '-'");
debug_assert!(!v[0].starts_with("--"), "the first argument should be short");
debug_assert!(v[1].starts_with("--"), "the second argument should be long");
Keys(v)
}
}
impl From<&'static str> for Keys {
#[inline]
fn from(v: &'static str) -> Self {
debug_assert!(v.starts_with("-"), "an argument should start with '-'");
Keys([v, ""])
}
}