diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .rustfmt.toml | 1 | ||||
-rw-r--r-- | Cargo.lock | 7 | ||||
-rw-r--r-- | Cargo.toml | 6 | ||||
-rw-r--r-- | examples/copyline.rs | 63 | ||||
-rw-r--r-- | src/formula.rs | 192 | ||||
-rw-r--r-- | src/lib.rs | 203 |
7 files changed, 473 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e7dad5c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "scurvy" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6ee2bc7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "scurvy" +version = "0.1.0" +edition = "2024" + +[dependencies] diff --git a/examples/copyline.rs b/examples/copyline.rs new file mode 100644 index 0000000..f0c2b67 --- /dev/null +++ b/examples/copyline.rs @@ -0,0 +1,63 @@ +use std::{ + fs::File, + io::{self, BufRead, BufReader, ErrorKind, Write}, + path::PathBuf, +}; + +use scurvy::{ + Argument, Scurvy, + formula::{PathFormula, UsizeFormula}, +}; + +pub fn main() { + let args = vec![ + Argument::new(&["infile", "ifile"]).arg("path"), + Argument::new(&["line-number", "ln"]).arg("line#"), + Argument::new(&["outfile", "ofile"]).arg("path"), + ]; + + let scurvy = Scurvy::make(args); + + if scurvy.should_print_help() { + println!("{HELP_STR}"); + return; + } + + let input = scurvy.parse_req("infile", PathFormula::new()); + let output = scurvy.parse_req("outfile", PathFormula::new()); + let line_number = scurvy.parse_req("line-number", UsizeFormula::new().bounds(1..)); + + if let Err(e) = do_work(input, output, line_number) { + eprintln!("{e}"); + std::process::exit(-1); + } +} + +fn do_work(input: PathBuf, output: PathBuf, line_number: usize) -> io::Result<()> { + let bufr = BufReader::new(File::open(input)?); + let mut out = File::create(output)?; + + match bufr.lines().nth(line_number) { + Some(line) => { + out.write_all((line?).as_bytes())?; + Ok(()) + } + None => Err(io::Error::from(ErrorKind::UnexpectedEof)), + } +} + +// Tab width is usually eight in terminals. Our max line-length for an options +// name line is 72, the help-text itself is 66 +const HELP_STR: &'static str = "\ +usage: copyline ifile=<path> ofile=<path> ln=<line#> + +ARGUMENTS + infile=<path> | ifile=<path> + Input file path. Where we read the line from + + outfile=<path> | ofile=<path> + Output file path. Where we write the line to. + + line-number=<line#> | ln=<line#> + The line you want, starting at 1. +"; diff --git a/src/formula.rs b/src/formula.rs new file mode 100644 index 0000000..3e84015 --- /dev/null +++ b/src/formula.rs @@ -0,0 +1,192 @@ +use core::fmt; +use std::{ + marker::PhantomData, + ops::{Range, RangeBounds, RangeFrom, RangeInclusive, RangeTo, RangeToInclusive}, + path::PathBuf, + str::FromStr, +}; + +pub struct Formula<T: FromStr> { + pub(crate) check_fn: Option<(String, Box<dyn FnMut(&T) -> bool>)>, + pub(crate) failure: String, + pub(crate) missing: Option<String>, + phantom: PhantomData<T>, +} + +impl<T: FromStr> Formula<T> { + pub fn new<S: Into<String>>(failure: S) -> Self { + Self { + check_fn: None, + failure: failure.into(), + missing: None, + phantom: PhantomData::default(), + } + } + + pub fn check<S: Into<String>, F>(mut self, fail: S, check: F) -> Self + where + F: FnMut(&T) -> bool + 'static, + { + self.check_fn = Some((fail.into(), Box::new(check))); + self + } + + pub fn required<S: Into<String>>(mut self, missing: S) -> Self { + self.missing = Some(missing.into()); + self + } +} + +impl<T: FromStr> From<String> for Formula<T> { + fn from(value: String) -> Self { + Formula::new(value) + } +} + +impl<T: FromStr> From<&str> for Formula<T> { + fn from(value: &str) -> Self { + Formula::new(value) + } +} + +impl From<UsizeFormula> for Formula<usize> { + fn from(value: UsizeFormula) -> Self { + let UsizeFormula { bounds } = value; + let form = Formula::new("Failed to parse [opt] as an integer"); + + if let Some(bounds) = bounds { + let fail = format!("[opt] must be {bounds}"); + + form.check(fail, move |u| bounds.contains(&u)) + } else { + form + } + } +} + +impl From<F32Formula> for Formula<f32> { + fn from(value: F32Formula) -> Self { + let F32Formula { bounds } = value; + let form = Formula::new("Failed to parse [opt] as a number"); + + if let Some(bounds) = bounds { + let fail = format!("[opt] must be {bounds}"); + + form.check(fail, move |u| bounds.contains(&u)) + } else { + form + } + } +} + +impl From<PathFormula> for Formula<PathBuf> { + fn from(_value: PathFormula) -> Self { + Formula::new("Failed to parse [opt] as a path") + } +} + +pub struct F32Formula { + bounds: Option<Ranges<f32>>, +} + +impl F32Formula { + pub fn new() -> Self { + Self { bounds: None } + } + + pub fn bounds<R: Into<Ranges<f32>>>(mut self, bounds: R) -> Self { + self.bounds = Some(bounds.into()); + self + } +} + +pub struct UsizeFormula { + bounds: Option<Ranges<usize>>, +} + +impl UsizeFormula { + pub fn new() -> Self { + Self { bounds: None } + } + + pub fn bounds<R: Into<Ranges<usize>>>(mut self, bounds: R) -> Self { + self.bounds = Some(bounds.into()); + self + } +} + +pub enum Ranges<T> { + Exlusive(Range<T>), + Inclusive(RangeInclusive<T>), + From(RangeFrom<T>), + To(RangeTo<T>), + ToInclusive(RangeToInclusive<T>), +} + +impl<T> From<Range<T>> for Ranges<T> { + fn from(value: Range<T>) -> Self { + Ranges::Exlusive(value) + } +} + +impl<T> From<RangeFrom<T>> for Ranges<T> { + fn from(value: RangeFrom<T>) -> Self { + Ranges::From(value) + } +} + +impl<T> From<RangeTo<T>> for Ranges<T> { + fn from(value: RangeTo<T>) -> Self { + Ranges::To(value) + } +} + +impl<T> From<RangeInclusive<T>> for Ranges<T> { + fn from(value: RangeInclusive<T>) -> Self { + Ranges::Inclusive(value) + } +} + +impl<T> From<RangeToInclusive<T>> for Ranges<T> { + fn from(value: RangeToInclusive<T>) -> Self { + Ranges::ToInclusive(value) + } +} + +impl<T: fmt::Display> fmt::Display for Ranges<T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Ranges::Exlusive(range) => { + write!(f, "from {} and less than {}", range.start, range.end) + } + Ranges::Inclusive(range) => write!(f, "from {} to {}", range.start(), range.end()), + Ranges::From(range) => write!(f, "greater than {}", range.start), + Ranges::To(range) => write!(f, "less than {}", range.end), + Ranges::ToInclusive(range) => write!(f, "from 0 to {}", range.end), + } + } +} + +impl<T: PartialEq> Ranges<T> { + pub fn contains<U>(&self, item: &U) -> bool + where + T: PartialOrd<U>, + U: PartialOrd<T> + ?Sized, + { + match self { + Ranges::Exlusive(range) => range.contains(item), + Ranges::Inclusive(range) => range.contains(item), + Ranges::From(range) => range.contains(item), + Ranges::To(range) => range.contains(item), + Ranges::ToInclusive(range) => range.contains(item), + } + } +} + +pub struct PathFormula; + +impl PathFormula { + pub fn new() -> Self { + Self {} + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9f533b9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,203 @@ +use std::str::FromStr; + +use formula::Formula; + +pub mod formula; + +pub struct Scurvy { + free: Vec<String>, + pairs: Vec<Pair>, + unknown: Vec<(String, String)>, + print_help: bool, + print_version: bool, +} + +struct Pair { + key: Argument, + values: Vec<String>, +} + +impl Scurvy { + pub fn make(args: Vec<Argument>) -> Self { + let mut free = vec![]; + let mut pairs: Vec<Pair> = args.into_iter().map(|a| a.into()).collect(); + let mut unknown = vec![]; + + let mut print_help = false; + let mut print_version = false; + + for arg in std::env::args().skip(1) { + if arg == "-h" || arg == "--help" || arg == "help=" { + print_help = true; + } else if arg == "-V" || arg == "--version" || arg == "version=" { + print_version = true; + } + + match arg.split_once('=') { + None => free.push(arg), + Some((key, value)) => { + if let Some(pair) = pairs.iter_mut().find(|p| p.key.matches(key)) { + pair.values.push(value.to_owned()); + } else { + unknown.push((key.to_owned(), value.to_owned())); + } + } + } + } + + Self { + free, + pairs, + unknown, + print_help, + print_version, + } + } + + pub fn should_print_help(&self) -> bool { + self.print_help + } + + pub fn should_print_version(&self) -> bool { + self.print_version + } + + pub fn free(&self) -> &[String] { + &self.free + } + + fn get_pair(&self, key: &str) -> Option<&Pair> { + self.pairs.iter().find(|p| p.key.matches(key)) + } + + pub fn get(&self, key: &str) -> Option<&str> { + self.pairs + .iter() + .find(|p| p.key.matches(key)) + .map(|p| p.values.first().map(|s| s.as_str())) + .flatten() + } + + pub fn parse<T: FromStr, F: Into<Formula<T>>>(&self, key: &str, formula: F) -> Option<T> { + let formula = formula.into(); + let Some(got) = self.get(key) else { + return None; + }; + + match got.parse::<T>() { + Ok(o) => { + if let Some((fail, mut check)) = formula.check_fn { + if !check(&o) { + let pair = self.get_pair(key).unwrap(); + let str = fail.replace("[opt]", pair.key.preferred_key()); + + eprintln!("{str}"); + std::process::exit(-1); + } + + return Some(o); + }; + + Some(o) + } + Err(_e) => { + eprintln!("{}", formula.failure); + std::process::exit(-1); + } + } + } + + pub fn parse_req<T: FromStr, F: Into<Formula<T>>>(&self, key: &str, formula: F) -> T { + let formula = formula.into(); + let missing = formula.missing.clone(); + + match self.parse(key, formula) { + None => match missing { + None => { + let pair = self.get_pair(key).unwrap(); + eprintln!("An argument for '{}' is required", pair.key.preferred_key()); + std::process::exit(-1); + } + Some(misstr) => { + let pair = self.get_pair(key).unwrap(); + let str = misstr.replace("[opt]", pair.key.preferred_key()); + + eprintln!("{str}"); + std::process::exit(-1); + } + }, + Some(o) => o, + } + } +} + +pub struct Argument { + keys: Vec<String>, + arg_name: Option<&'static str>, + help: &'static str, +} + +impl Argument { + pub fn new<'s, K: Into<SingleOrMultiple<'s>>>(key: K) -> Self { + let keys = match key.into() { + SingleOrMultiple::Single(s) => vec![s], + SingleOrMultiple::Multiple(m) => m.to_vec(), + }; + + Self { + keys: keys.into_iter().map(<_>::to_owned).collect(), + arg_name: None, + help: "", + } + } + + pub fn arg(mut self, name: &'static str) -> Self { + self.arg_name = Some(name); + self + } + + pub fn help(mut self, help: &'static str) -> Self { + self.help = help; + self + } + + fn matches(&self, key: &str) -> bool { + self.keys.iter().find(|k| k.as_str() == key).is_some() + } + + fn preferred_key(&self) -> &str { + self.keys.first().unwrap().as_str() + } +} + +impl Into<Pair> for Argument { + fn into(self) -> Pair { + Pair { + key: self, + values: vec![], + } + } +} + +pub enum SingleOrMultiple<'s> { + Single(&'s str), + Multiple(&'s [&'s str]), +} + +impl<'s> From<&'s str> for SingleOrMultiple<'s> { + fn from(value: &'s str) -> Self { + Self::Single(value) + } +} + +impl<'s> From<&'s [&'s str]> for SingleOrMultiple<'s> { + fn from(value: &'s [&'s str]) -> Self { + Self::Multiple(value) + } +} + +impl<'s, const N: usize> From<&'s [&'s str; N]> for SingleOrMultiple<'s> { + fn from(value: &'s [&'s str; N]) -> Self { + Self::Multiple(value.as_slice()) + } +} |