From c83b38a87432a85b0323c153c328030842919aa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20M=2E=20Bezerra?= Date: Sun, 4 Apr 2021 01:01:32 -0300 Subject: [PATCH] Add `oof` complete base implementation. --- oof/src/error.rs | 35 ++++ oof/src/flags.rs | 39 +++++ oof/src/lib.rs | 409 ++++++++++++++++++++++++++++++++++++++++++++++- oof/src/util.rs | 13 ++ 4 files changed, 491 insertions(+), 5 deletions(-) create mode 100644 oof/src/error.rs create mode 100644 oof/src/flags.rs create mode 100644 oof/src/util.rs diff --git a/oof/src/error.rs b/oof/src/error.rs new file mode 100644 index 0000000..abc8671 --- /dev/null +++ b/oof/src/error.rs @@ -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 + ), + } + } +} diff --git a/oof/src/flags.rs b/oof/src/flags.rs new file mode 100644 index 0000000..38ff31f --- /dev/null +++ b/oof/src/flags.rs @@ -0,0 +1,39 @@ +use std::ffi::OsStr; + +pub enum FlagType { + None, + Short, + Long, +} + +impl FlagType { + pub fn from(text: impl AsRef) -> 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 + } + } +} diff --git a/oof/src/lib.rs b/oof/src/lib.rs index 31e1bb2..7dcea69 100644 --- a/oof/src/lib.rs +++ b/oof/src/lib.rs @@ -1,7 +1,406 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); +//! 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` (or `BTreeSet<&str>`). +pub fn pop_subcommand<'a, T, I, II>(args: &mut Vec, subcommands: I) -> Option<&'a II> +where + I: IntoIterator, + II: AsRef, + T: AsRef, +{ + 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, + 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 { + 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, + flags_info: &[Flag], +) -> Result<(Vec, Flags), OofError> { + let mut short_flags_info = BTreeMap::::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::>(); + + // 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 { + let args = text.split_whitespace(); + args.map(OsString::from).collect() + } + + // asdasdsa + #[test] + 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) + }; +} diff --git a/oof/src/util.rs b/oof/src/util.rs new file mode 100644 index 0000000..5f301aa --- /dev/null +++ b/oof/src/util.rs @@ -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() +}