mirror of
https://github.com/ouch-org/ouch.git
synced 2025-06-07 03:55:28 +00:00
Add oof
complete base implementation.
This commit is contained in:
parent
9fb48fd348
commit
c83b38a874
35
oof/src/error.rs
Normal file
35
oof/src/error.rs
Normal 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
39
oof/src/flags.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
403
oof/src/lib.rs
403
oof/src/lib.rs
@ -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
13
oof/src/util.rs
Normal 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()
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user