Add oof complete base implementation.

This commit is contained in:
João M. Bezerra 2021-04-04 01:01:32 -03:00
parent 9fb48fd348
commit c83b38a874
4 changed files with 491 additions and 5 deletions

35
oof/src/error.rs Normal file
View File

@ -0,0 +1,35 @@
use std::{error, ffi::OsString, fmt};
use crate::Flag;
#[derive(Debug)]
pub enum OofError {
FlagValueConflict {
flag: Flag,
previous_value: OsString,
new_value: OsString,
},
}
impl error::Error for OofError {
fn source(&self) -> Option<&(dyn error::Error + 'static)> {
None
}
}
impl fmt::Display for OofError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// TODO: implement proper debug messages
match self {
OofError::FlagValueConflict {
flag,
previous_value,
new_value,
} => write!(
f,
"CLI flag value conflicted for flag '--{}', previous: {:?}, new: {:?}.",
flag.long, previous_value, new_value
),
}
}
}

39
oof/src/flags.rs Normal file
View File

@ -0,0 +1,39 @@
use std::ffi::OsStr;
pub enum FlagType {
None,
Short,
Long,
}
impl FlagType {
pub fn from(text: impl AsRef<OsStr>) -> Self {
let text = text.as_ref();
let mut iter;
#[cfg(target_family = "unix")]
{
use std::os::unix::ffi::OsStrExt;
iter = text.as_bytes().iter();
}
#[cfg(target_family = "windows")]
{
use std::os::windows::ffi::OsStrExt;
iter = text.encode_wide
}
// 45 is the code for a hyphen
// Typed as 45_u16 for Windows
// Typed as 45_u8 for Unix
if let Some(45) = iter.next() {
if let Some(45) = iter.next() {
Self::Long
} else {
Self::Short
}
} else {
Self::None
}
}
}

View File

@ -1,7 +1,406 @@
//! Ouch's argparsing crate.
//!
//! The usage of this crate is heavily based on boolean_flags and
//! argument_flags, there should be an more _obvious_ naming.
mod error;
mod flags;
pub mod util;
use std::{
collections::{BTreeMap, BTreeSet},
ffi::{OsStr, OsString},
str,
};
pub use error::OofError;
pub use flags::FlagType;
use util::trim_double_hyphen;
/// Pop leading application `subcommand`, if valid.
///
/// `args` can be a Vec of `OsString` or `OsStr`
/// `subcommands` is any container that can yield `&str` through `AsRef`, can be `Vec<&str>` or
/// a GREAT `BTreeSet<String>` (or `BTreeSet<&str>`).
pub fn pop_subcommand<'a, T, I, II>(args: &mut Vec<T>, subcommands: I) -> Option<&'a II>
where
I: IntoIterator<Item = &'a II>,
II: AsRef<str>,
T: AsRef<OsStr>,
{
if args.is_empty() {
return None;
}
for subcommand in subcommands.into_iter() {
if subcommand.as_ref() == args[0].as_ref() {
args.remove(0);
return Some(subcommand);
}
}
None
}
/// Shallow type, created to indicate a `Flag` that accepts a argument.
///
/// ArgFlag::long(), is actually a Flag::long(), but sets a internal attribute.
///
/// Examples in here pls
pub struct ArgFlag;
#[allow(clippy::new_ret_no_self)]
impl ArgFlag {
pub fn long(name: &'static str) -> Flag {
Flag {
long: name,
short: None,
takes_value: true,
}
}
}
#[derive(Debug, PartialEq, Clone)]
pub struct Flag {
// Also the name
pub long: &'static str,
pub short: Option<char>,
pub takes_value: bool,
}
impl Flag {
pub fn long(name: &'static str) -> Self {
Self {
long: name,
short: None,
takes_value: false,
}
}
pub fn short(mut self, short_flag_char: char) -> Self {
self.short = Some(short_flag_char);
self
}
}
#[derive(Default, Debug)]
pub struct Flags {
pub boolean_flags: BTreeSet<&'static str>,
pub argument_flags: BTreeMap<&'static str, OsString>,
}
impl Flags {
pub fn new() -> Self {
Self::default()
}
}
impl Flags {
pub fn is_present(&self, flag_name: &str) -> bool {
self.boolean_flags.contains(flag_name) || self.argument_flags.contains_key(flag_name)
}
pub fn arg(&self, flag_name: &str) -> Option<&OsString> {
self.argument_flags.get(flag_name)
}
pub fn take_arg(&mut self, flag_name: &str) -> Option<OsString> {
self.argument_flags.remove(flag_name)
}
}
/// Detect flags from args and filter from args.
///
/// Each flag received via flags_info should must have unique long and short identifiers.
///
/// # Panics (Developer errors)
/// - If there are duplicated short flag identifiers.
/// - If there are duplicated long flag identifiers.
///
/// Both conditions cause panic because your program's flags specification is meant to have unique
/// flags. There shouldn't be two "--verbose" flags, for example.
/// Caller should guarantee it, fortunately, this can almost always be caught while prototyping in
/// debug mode, test your CLI flags once, if it works once, you're good,
///
/// # Errors (User errors)
/// - Argument flag comes at last arg, so there's no way to provide an argument.
/// - Or if it doesn't comes at last, but the rest are just flags, no possible and valid arg.
/// - Short flags with multiple letters in the same arg contain a argument flag that does not come
/// as the last one in the list (example "-oahc", where 'o', 'a', or 'h' is a argument flag, but do
/// not comes at last, so it is impossible for them to receive the required argument.
/// - User passes same flag twice (short or long, boolean or arg).
///
/// ...
pub fn filter_flags(
args: Vec<OsString>,
flags_info: &[Flag],
) -> Result<(Vec<OsString>, Flags), OofError> {
let mut short_flags_info = BTreeMap::<char, &Flag>::new();
let mut long_flags_info = BTreeMap::<&'static str, &Flag>::new();
for flag in flags_info.iter() {
// Panics if duplicated/conflicts
assert!(
!long_flags_info.contains_key(flag.long),
"DEV ERROR: duplicated long flag '{}'.",
flag.long
);
long_flags_info.insert(flag.long, &flag);
if let Some(short) = flag.short {
// Panics if duplicated/conflicts
assert!(
!short_flags_info.contains_key(&short),
"DEV ERROR: duplicated short flag '-{}'.",
short
);
short_flags_info.insert(short, &flag);
}
}
// Consume args, filter out flags, and add back to args new vec
let mut iter = args.into_iter();
let mut new_args = vec![];
let mut result_flags = Flags::new();
while let Some(arg) = iter.next() {
let flag_type = FlagType::from(&arg);
// If it isn't a flag, retrieve to `args` and skip this iteration
if let FlagType::None = flag_type {
new_args.push(arg);
continue;
}
// If it is a flag, now we try to interpret as valid utf-8
let flag: &str = arg
.to_str()
.unwrap_or_else(|| panic!("User error: The flag needs to be valid utf8"));
// Only one hyphen in the flag
// A short flag can be of form "-", "-abcd", "-h", "-v", etc
if let FlagType::Short = flag_type {
assert_eq!(flag.chars().next(), Some('-'));
// TODO
// TODO: what should happen if the flag is empty?????
// if flags.chars().skip(1).next().is_none() {
// panic!("User error: flag is empty???");
// }
// Skip hyphen and get all letters
let letters = flag.chars().skip(1).collect::<Vec<char>>();
// For each letter in the short arg, except the last one
for (i, letter) in letters.iter().copied().enumerate() {
// Safety: this loop only runs when len >= 1
let is_last_letter = i == letters.len() - 1;
let flag_info = short_flags_info.get(&letter).unwrap_or_else(|| {
panic!("User error: Unexpected/UNKNOWN flag `letter`, error")
});
if !is_last_letter && flag_info.takes_value {
panic!("User error: Only the last letter can refer to flag that takes values");
// Because "-AB argument" only works if B takes values, not A.
// That is, the short flag that takes values need to come at the end
// of this piece of text
}
let flag_name: &'static str = flag_info.long;
if flag_info.takes_value {
// If it was already inserted
if result_flags.argument_flags.contains_key(flag_name) {
panic!("User error: duplicated, found this flag TWICE!");
}
// pop the next one
let flag_argument = iter.next();
flag_argument.unwrap_or_else(|| {
panic!(
"USer errror: argument flag `argument_flag` came at last, but it \
requires an argument"
)
});
// Otherwise, insert it (TODO: grab next one and add it)
// result_flags.argument_flags.insert(flag_info.long);
} else {
// If it was already inserted
if result_flags.boolean_flags.contains(flag_name) {
panic!("User error: duplicated, found this flag TWICE!");
}
// Otherwise, insert it
result_flags.boolean_flags.insert(flag_name);
}
}
}
if let FlagType::Long = flag_type {
let flag = trim_double_hyphen(flag);
let flag_info = long_flags_info
.get(flag)
.unwrap_or_else(|| panic!("User error: Unexpected/UNKNOWN flag '{}'", flag));
let flag_name = flag_info.long;
if flag_info.takes_value {
// If it was already inserted
if result_flags.argument_flags.contains_key(&flag_name) {
panic!("User error: duplicated, found this flag TWICE!");
}
let flag_argument = iter.next().unwrap_or_else(|| {
panic!(
"USer errror: argument flag `argument_flag` came at last, but it requires \
an argument"
)
});
result_flags.argument_flags.insert(flag_name, flag_argument);
} else {
// If it was already inserted
if result_flags.boolean_flags.contains(&flag_name) {
panic!("User error: duplicated, found this flag TWICE!");
}
// Otherwise, insert it
result_flags.boolean_flags.insert(&flag_name);
}
// // TODO
// TODO: what should happen if the flag is empty?????
// if flag.is_empty() {
// panic!("Is this an error?");
// }
}
}
Ok((new_args, result_flags))
}
#[cfg(test)]
mod tests {
use crate::*;
fn gen_args(text: &str) -> Vec<OsString> {
let args = text.split_whitespace();
args.map(OsString::from).collect()
}
// asdasdsa
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
fn test_filter_flags() {
let flags_info = [
ArgFlag::long("output_file").short('o'),
Flag::long("verbose").short('v'),
Flag::long("help").short('h'),
];
let args = gen_args("ouch a.zip -v b.tar.gz --output_file new_folder c.tar");
let (args, mut flags) = filter_flags(args, &flags_info).unwrap();
assert_eq!(args, gen_args("ouch a.zip b.tar.gz c.tar"));
assert!(flags.is_present("output_file"));
assert_eq!(
Some(&OsString::from("new_folder")),
flags.arg("output_file")
);
assert_eq!(
Some(OsString::from("new_folder")),
flags.take_arg("output_file")
);
assert!(!flags.is_present("output_file"));
}
#[test]
fn test_pop_subcommand() {
let subcommands = &["commit", "add", "push", "remote"];
let mut args = gen_args("add a b c");
let result = pop_subcommand(&mut args, subcommands);
assert_eq!(result, Some(&"add"));
assert_eq!(args[0], "a");
// Check when no subcommand matches
let mut args = gen_args("a b c");
let result = pop_subcommand(&mut args, subcommands);
assert_eq!(result, None);
assert_eq!(args[0], "a");
}
#[test]
fn test_flag_info_macros() {
let flags_info = [
arg_flag!('o', "output_file"),
arg_flag!("delay"),
flag!('v', "verbose"),
flag!('h', "help"),
flag!("version"),
];
let expected = [
ArgFlag::long("output_file").short('o'),
ArgFlag::long("delay"),
Flag::long("verbose").short('v'),
Flag::long("help").short('h'),
Flag::long("version"),
];
assert_eq!(flags_info, expected);
}
#[test]
// TODO: remove should_panic and use proper error handling inside of filter_args
#[should_panic]
fn test_flag_info_with_long_flag_conflict() {
let flags_info = [
ArgFlag::long("verbose").short('a'),
Flag::long("verbose").short('b'),
];
// Should panic here
let result = filter_flags(vec![], &flags_info);
assert!(matches!(result, Err(OofError::FlagValueConflict { .. })));
}
#[test]
// TODO: remove should_panic and use proper error handling inside of filter_args
#[should_panic]
fn test_flag_info_with_short_flag_conflict() {
let flags_info = [
ArgFlag::long("output_file").short('o'),
Flag::long("verbose").short('o'),
];
// Should panic here
filter_flags(vec![], &flags_info).unwrap_err();
}
}
/// Create a flag with long flag (?).
#[macro_export]
macro_rules! flag {
($short:expr, $long:expr) => {
Flag::long($long).short($short)
};
($long:expr) => {
Flag::long($long)
};
}
/// Create a flag with long flag (?), receives argument (?).
#[macro_export]
macro_rules! arg_flag {
($short:expr, $long:expr) => {
ArgFlag::long($long).short($short)
};
($long:expr) => {
ArgFlag::long($long)
};
}

13
oof/src/util.rs Normal file
View File

@ -0,0 +1,13 @@
/// Util function to skip the two leading long flag hyphens.
pub fn trim_double_hyphen(flag_text: &str) -> &str {
let mut chars = flag_text.chars();
chars.nth(1); // Skipping 2 chars
chars.as_str()
}
/// Util function to skip the single leading short flag hyphen.
pub fn trim_single_hyphen(flag_text: &str) -> &str {
let mut chars = flag_text.chars();
chars.next(); // Skipping 1 char
chars.as_str()
}