xflag 0.1.0

Flag parsing in the X toolkit tradition
Documentation
//! xflag is a minimal and opinionated command-line flag parsing library for
//! Rust, following in the tradition of the X toolkit.

use std::error::Error;
use std::ffi::OsStr;
use std::fmt;

pub type FlagResult<T> = Result<T, ParseError>;

#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ParseError {
	pub kind: ErrorKind,
}

#[derive(Debug, Clone, Eq, PartialEq)]
pub enum ErrorKind {
	Overflow,
	Unexpected(char),
}

impl fmt::Display for ParseError {
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
		use self::ErrorKind::*;
		let s = match self.kind {
			Overflow => self.description().to_string(),
			Unexpected(ch) => format!("{}: {:?}", self.description(), ch),
		};
		write!(f, "{}", s)
	}
}

impl Error for ParseError {
	fn description(&self) -> &str {
		use self::ErrorKind::*;
		match self.kind {
			Unexpected(_) => "unexpected",
			Overflow => "overflow",
		}
	}
}

/// The type of argument encountered.
#[derive(Clone, Debug, PartialEq)]
pub enum Argument {
	/// Flag without a value, e.g. `-h`.
	Flag(char),

	/// An option, or a flag with a value, e.g. `-fbar`.
	///
	/// Since a flag is always exactly the width of one character,
	/// the value will be the strong that follows until a space separator is found.
	/// For example `-fbar` will mean the flag is `f` with a value of `bar`.
	Option(char, String),

	/// Flag indicating a line number location.
	///
	/// This is recognised by a `+` followed by a positive integer, for example `+42`.
	Line(i64),

	/// Position argument that is not a flag or a value associated with a flag.
	Free(String),

	/// Position argument indicating `/dev/stdin`, e.g. `-`.
	Stdin,
}

impl Argument {
	/// Get the flag's value, if it has one.
	pub fn as_str(&self) -> Option<&str> {
		match *self {
			Argument::Free(ref s) => Some(s),
			Argument::Option(_, ref s) => Some(s),
			_ => None,
		}
	}
}

impl fmt::Display for Argument {
	fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
		let repr = match *self {
			Argument::Flag(flag) => format!("-{}", flag),
			Argument::Option(flag, ref value) => format!("-{} {}", flag, value),
			Argument::Line(line) => format!("+{}", line),
			Argument::Free(ref s) => s.clone(),
			Argument::Stdin => "-".into(),
		};
		write!(f, "{}", repr)
	}
}

/// A sequence of arguments.
#[derive(Clone, Debug, PartialEq)]
pub struct Arguments(Vec<Argument>);

impl Arguments {
	/// Get the first named flag, if any.
	pub fn first(&self, flag: char) -> Option<&Argument> {
		for arg in self.0.iter() {
			match arg {
				Argument::Flag(ch) if ch == &flag => return Some(arg),
				Argument::Option(ch, _) if ch == &flag => return Some(arg),
				_ => {}
			}
		}
		None
	}

	/// Determine if `flag` is present.
	pub fn present(&self, flag: char) -> bool {
		self.first(flag).is_some()
	}

	/// Get sequence of free-standing positional arguments.
	pub fn free(&self) -> Vec<String> {
		self.0
			.iter()
			.filter_map(|arg| match arg {
				Argument::Free(s) => Some(s.clone()),
				_ => None,
			})
			.collect()
	}
}

impl IntoIterator for Arguments {
	type Item = Argument;
	type IntoIter = ::std::vec::IntoIter<Argument>;

	fn into_iter(self) -> Self::IntoIter {
		self.0.into_iter()
	}
}

struct Lexer<'a> {
	input: &'a str,
	idx: usize,
}

impl<'a> Lexer<'a> {
	fn new(input: &'a str) -> Lexer<'a> {
		Lexer { input, idx: 0 }
	}

	fn byte(&self, idx: usize) -> u8 {
		if idx + self.idx < self.input.len() {
			self.input.as_bytes()[idx + self.idx]
		} else {
			0
		}
	}

	fn rest(&self) -> &str {
		&self.input[self.idx..]
	}

	fn peek_char(&self) -> Option<char> {
		self.rest().chars().next()
	}

	fn take_char(&mut self) -> char {
		if let Some(ch) = self.peek_char() {
			self.idx += 1;
			ch
		} else {
			'\0'
		}
	}

	fn take_rest(&mut self) -> String {
		let mut s = String::new();
		while self.peek_char().is_some() {
			s.push(self.take_char());
		}
		s
	}

	fn is_integer(&self, ns: &str) -> bool {
		for b in ns.bytes() {
			match b {
				b'0'...b'9' => {}
				_ => return false,
			}
		}
		true
	}

	fn integer(&mut self) -> FlagResult<i64> {
		let base = 10;
		let mut value = 0i64;

		loop {
			let b = self.byte(0);
			let digit = match b {
				b'0'...b'9' => i64::from(b - b'0'),
				_ => break,
			};

			if let Some(v) = value.checked_mul(base).and_then(|v| v.checked_add(digit)) {
				value = v;
			} else {
				return Err(ParseError {
					kind: ErrorKind::Overflow,
				});
			}
			self.idx += 1;
		}

		Ok(value)
	}

	fn is_alphanumeric(&self, ch: Option<char>) -> bool {
		if let Some(ch) = ch {
			ch.is_alphanumeric()
		} else {
			false
		}
	}

	fn tokenize(&mut self) -> FlagResult<Argument> {
		Ok(match self.take_char() {
			'+' if self.is_integer(self.rest()) => Argument::Line(self.integer()?),
			'-' if self.is_alphanumeric(self.peek_char()) && self.rest().len() > 1 => {
				Argument::Option(self.take_char(), self.take_rest())
			}
			'-' if self.is_alphanumeric(self.peek_char()) => Argument::Flag(self.take_char()),
			'-' => Argument::Stdin,
			x => Argument::Free(format!("{}{}", x, self.take_rest())),
		})
	}
}

/// Parse flags from a type that can be turned into an iterator,
/// for example a [`Vec`], and return a sequence of arguments.
///
/// To find if an argument is present:
///
/// ```
/// let flags = xflag::parse(vec!["-h"]).unwrap();
/// assert!(flags.present('h'));
/// ```
///
/// Finding the first option:
///
/// ```
/// use xflag::{Argument, parse};
///
/// let flags = parse(vec!["-fbar"]).unwrap();
/// assert_eq!(flags.first('f'), Some(&Argument::Option('f', "bar".into())));
/// ```
///
/// To cycle through the arguments:
///
/// ```
/// for arg in xflag::parse(vec!["-z", "-fbar", "baz", "+42", "-"]).unwrap() {
/// 	println!("{}", arg);
/// }
/// ```
///
/// Asserting a particular order to the arguments:
///
/// ```
/// use xflag::{Argument, parse};
///
/// let args = parse(vec!["-z", "-fbar", "baz", "+42", "-"]).unwrap();
/// let mut iter = args.into_iter();
///
/// assert_eq!(iter.next(), Some(Argument::Flag('z')));
/// assert_eq!(iter.next(), Some(Argument::Option('f', "bar".into())));
/// assert_eq!(iter.next(), Some(Argument::Free("baz".into())));
/// assert_eq!(iter.next(), Some(Argument::Line(42)));
/// assert_eq!(iter.next(), Some(Argument::Stdin));
/// assert_eq!(iter.next(), None);
/// ```
///
/// [`Vec`]: https://doc.rust-lang.org/std/vec/struct.Vec.html
pub fn parse<C: IntoIterator>(args: C) -> FlagResult<Arguments>
where
	C::Item: AsRef<OsStr>,
{
	let args: Vec<String> = args
		.into_iter()
		.map(|arg| arg.as_ref().to_str().unwrap().to_owned())
		.collect();

	let mut toks = Arguments(Vec::new());
	for arg in args {
		let tok = Lexer::new(&arg).tokenize()?;
		toks.0.push(tok);
	}

	Ok(toks)
}

#[cfg(test)]
mod tests {
	use super::{parse, Argument};

	#[test]
	fn test_empty() {
		let empty: Vec<&str> = vec![];
		let flags = parse(empty).unwrap();
		let mut iter = flags.into_iter();
		assert!(iter.next().is_none());
	}

	#[test]
	fn test_line() {
		let flags = parse(vec!["+42"]).unwrap();
		let mut iter = flags.into_iter();
		assert_eq!(Some(Argument::Line(42)), iter.next());
	}

	#[test]
	fn test_flag() {
		let flags = parse(vec!["-x"]).unwrap();
		let mut iter = flags.into_iter();
		assert_eq!(Some(Argument::Flag('x')), iter.next());
	}

	#[test]
	fn test_option() {
		let flags = parse(vec!["-fvalue"]).unwrap();
		let mut iter = flags.into_iter();
		assert_eq!(
			Some(Argument::Option('f', "value".to_string())),
			iter.next()
		);
	}

	#[test]
	fn test_free() {
		let flags = parse(vec!["foo"]).unwrap();
		let mut iter = flags.into_iter();
		assert_eq!(Some(Argument::Free("foo".to_string())), iter.next());
	}

	#[test]
	fn test_stdin() {
		let flags = parse(vec!["-"]).unwrap();
		let mut iter = flags.into_iter();
		assert_eq!(Some(Argument::Stdin), iter.next());
	}

	#[test]
	fn test_present() {
		let flags = parse(vec!["-x", "-fvalue"]).unwrap();
		assert!(flags.present('x'));
		assert!(flags.present('f'));
		assert!(!flags.present('v'));
	}

	#[test]
	fn test_first() {
		let flags = parse(vec!["-x", "-ffoo", "-fbar"]).unwrap();
		assert_eq!(Some(&Argument::Flag('x')), flags.first('x'));
		assert_eq!(
			Some(&Argument::Option('f', "foo".to_string())),
			flags.first('f')
		);
		assert_eq!(None, flags.first('v'));
	}

	#[test]
	fn test_line_overflow() {
		assert!(parse(vec!["+9999999999999999999"]).is_err());
	}
}