#![doc(html_root_url = "https://docs.rs/pico-args/0.4.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,
MissingArgument,
MissingOption(Keys),
OptionWithoutAValue(&'static str),
#[allow(missing_docs)]
Utf8ArgumentParsingFailed { value: String, cause: String },
#[allow(missing_docs)]
ArgumentParsingFailed { cause: 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::MissingArgument => {
write!(f, "free-standing argument is missing")
}
Error::MissingOption(key) => {
if key.second().is_empty() {
write!(f, "the '{}' option must be set", key.first())
} else {
write!(f, "the '{}/{}' option must be set", key.first(), key.second())
}
}
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)
}
}
}
}
impl std::error::Error for Error {}
#[derive(Clone, Copy, PartialEq)]
enum PairKind {
#[cfg(any(feature = "eq-separator", feature = "short-space-opt"))]
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 subcommand(&mut self) -> Result<Option<String>, Error> {
if self.0.is_empty() {
return Ok(None);
}
if let Some(s) = self.0[0].to_str() {
if s.starts_with('-') {
return Ok(None);
}
}
self.0.remove(0)
.into_string()
.map_err(|_| Error::NonUtf8Argument)
.map(Some)
}
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);
true
} else {
false
}
}
pub fn value_from_str<A, T>(&mut self, keys: A) -> Result<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<T, Error> {
let keys = keys.into();
match self.opt_value_from_fn(keys, f) {
Ok(Some(v)) => Ok(v),
Ok(None) => Err(Error::MissingOption(keys)),
Err(e) => Err(e),
}
}
pub fn opt_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.opt_value_from_fn(keys, FromStr::from_str)
}
pub fn opt_value_from_fn<A: Into<Keys>, T, E: Display>(
&mut self,
keys: A,
f: fn(&str) -> Result<T, E>,
) -> Result<Option<T>, Error> {
self.opt_value_from_fn_impl(keys.into(), f)
}
#[inline(never)]
fn opt_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),
}
}
#[cfg(any(feature = "eq-separator", feature = "short-space-opt"))]
#[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'=') {
#[cfg(feature = "eq-separator")]
{
value_range.start += 1;
}
#[cfg(not(feature = "eq-separator"))]
return Err(Error::OptionWithoutAValue(key));
} else {
#[cfg(not(feature = "short-space-opt"))]
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)
}
}
#[cfg(not(any(feature = "eq-separator", feature = "short-space-opt")))]
#[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 {
Ok(None)
}
}
pub fn values_from_str<A, T>(&mut self, keys: A) -> Result<Vec<T>, Error>
where
A: Into<Keys>,
T: FromStr,
<T as FromStr>::Err: Display,
{
self.values_from_fn(keys, FromStr::from_str)
}
pub fn values_from_fn<A: Into<Keys>, T, E: Display>(
&mut self,
keys: A,
f: fn(&str) -> Result<T, E>,
) -> Result<Vec<T>, Error> {
let keys = keys.into();
let mut values = Vec::new();
loop {
match self.opt_value_from_fn(keys, f) {
Ok(Some(v)) => values.push(v),
Ok(None) => break,
Err(e) => return Err(e),
}
}
Ok(values)
}
pub fn value_from_os_str<A: Into<Keys>, T, E: Display>(
&mut self,
keys: A,
f: fn(&OsStr) -> Result<T, E>,
) -> Result<T, Error> {
let keys = keys.into();
match self.opt_value_from_os_str(keys, f) {
Ok(Some(v)) => Ok(v),
Ok(None) => Err(Error::MissingOption(keys)),
Err(e) => Err(e),
}
}
pub fn opt_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.opt_value_from_os_str_impl(keys.into(), f)
}
#[inline(never)]
fn opt_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)
}
}
pub fn values_from_os_str<A: Into<Keys>, T, E: Display>(
&mut self,
keys: A,
f: fn(&OsStr) -> Result<T, E>,
) -> Result<Vec<T>, Error> {
let keys = keys.into();
let mut values = Vec::new();
loop {
match self.opt_value_from_os_str(keys, f) {
Ok(Some(v)) => values.push(v),
Ok(None) => break,
Err(e) => return Err(e),
}
}
Ok(values)
}
#[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
}
#[cfg(any(feature = "eq-separator", feature = "short-space-opt"))]
#[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| index_predicate(v, keys.first())) {
return Some((i, keys.first()));
}
}
if !keys.second().is_empty() {
if let Some(i) = self.0.iter().position(|v| index_predicate(v, keys.second())) {
return Some((i, keys.second()));
}
}
None
}
pub fn free_from_str<T>(&mut self) -> Result<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<T, Error> {
self.opt_free_from_fn(f)?.ok_or(Error::MissingArgument)
}
#[inline(never)]
pub fn free_from_os_str<T, E: Display>(
&mut self,
f: fn(&OsStr) -> Result<T, E>,
) -> Result<T, Error> {
self.opt_free_from_os_str(f)?.ok_or(Error::MissingArgument)
}
pub fn opt_free_from_str<T>(&mut self) -> Result<Option<T>, Error>
where
T: FromStr,
<T as FromStr>::Err: Display,
{
self.opt_free_from_fn(FromStr::from_str)
}
#[inline(never)]
pub fn opt_free_from_fn<T, E: Display>(
&mut self,
f: fn(&str) -> Result<T, E>,
) -> Result<Option<T>, Error> {
if self.0.is_empty() {
Ok(None)
} else {
let 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 opt_free_from_os_str<T, E: Display>(
&mut self,
f: fn(&OsStr) -> Result<T, E>,
) -> Result<Option<T>, Error> {
if self.0.is_empty() {
Ok(None)
} else {
let 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 finish(self) -> Vec<OsString> {
self.0
}
}
#[inline(never)]
fn error_to_string<E: Display>(e: E) -> String {
e.to_string()
}
#[cfg(feature = "eq-separator")]
#[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
}
#[cfg(feature = "short-space-opt")]
#[inline(never)]
fn starts_with_short_prefix(text: &OsStr, prefix: &str) -> bool {
if prefix.starts_with("--") {
return false;
}
if let Some(s) = text.to_str() {
if s.get(0..prefix.len()) == Some(prefix) {
return true;
}
}
false
}
#[cfg(all(feature = "eq-separator", feature = "short-space-opt"))]
#[inline]
fn index_predicate(text: &OsStr, prefix: &str) -> bool {
starts_with_plus_eq(text, prefix) || starts_with_short_prefix(text, prefix)
}
#[cfg(all(feature = "eq-separator", not(feature = "short-space-opt")))]
#[inline]
fn index_predicate(text: &OsStr, prefix: &str) -> bool {
starts_with_plus_eq(text, prefix)
}
#[cfg(all(feature = "short-space-opt", not(feature = "eq-separator")))]
#[inline]
fn index_predicate(text: &OsStr, prefix: &str) -> bool {
starts_with_short_prefix(text, prefix)
}
#[cfg(any(feature = "eq-separator", feature = "short-space-opt"))]
#[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, ""])
}
}