From d7bdd477e266864e45ee33b29adba17bf3f70b20 Mon Sep 17 00:00:00 2001 From: gennyble Date: Tue, 29 Apr 2025 14:45:01 -0500 Subject: initial commit --- .gitignore | 1 + .rustfmt.toml | 1 + Cargo.lock | 7 ++ Cargo.toml | 6 ++ examples/copyline.rs | 63 ++++++++++++++++ src/formula.rs | 192 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 203 +++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 473 insertions(+) create mode 100644 .gitignore create mode 100644 .rustfmt.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 examples/copyline.rs create mode 100644 src/formula.rs create mode 100644 src/lib.rs 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= ofile= ln= + +ARGUMENTS + infile= | ifile= + Input file path. Where we read the line from + + outfile= | ofile= + Output file path. Where we write the line to. + + line-number= | ln= + 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 { + pub(crate) check_fn: Option<(String, Box bool>)>, + pub(crate) failure: String, + pub(crate) missing: Option, + phantom: PhantomData, +} + +impl Formula { + pub fn new>(failure: S) -> Self { + Self { + check_fn: None, + failure: failure.into(), + missing: None, + phantom: PhantomData::default(), + } + } + + pub fn check, 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>(mut self, missing: S) -> Self { + self.missing = Some(missing.into()); + self + } +} + +impl From for Formula { + fn from(value: String) -> Self { + Formula::new(value) + } +} + +impl From<&str> for Formula { + fn from(value: &str) -> Self { + Formula::new(value) + } +} + +impl From for Formula { + 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 for Formula { + 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 for Formula { + fn from(_value: PathFormula) -> Self { + Formula::new("Failed to parse [opt] as a path") + } +} + +pub struct F32Formula { + bounds: Option>, +} + +impl F32Formula { + pub fn new() -> Self { + Self { bounds: None } + } + + pub fn bounds>>(mut self, bounds: R) -> Self { + self.bounds = Some(bounds.into()); + self + } +} + +pub struct UsizeFormula { + bounds: Option>, +} + +impl UsizeFormula { + pub fn new() -> Self { + Self { bounds: None } + } + + pub fn bounds>>(mut self, bounds: R) -> Self { + self.bounds = Some(bounds.into()); + self + } +} + +pub enum Ranges { + Exlusive(Range), + Inclusive(RangeInclusive), + From(RangeFrom), + To(RangeTo), + ToInclusive(RangeToInclusive), +} + +impl From> for Ranges { + fn from(value: Range) -> Self { + Ranges::Exlusive(value) + } +} + +impl From> for Ranges { + fn from(value: RangeFrom) -> Self { + Ranges::From(value) + } +} + +impl From> for Ranges { + fn from(value: RangeTo) -> Self { + Ranges::To(value) + } +} + +impl From> for Ranges { + fn from(value: RangeInclusive) -> Self { + Ranges::Inclusive(value) + } +} + +impl From> for Ranges { + fn from(value: RangeToInclusive) -> Self { + Ranges::ToInclusive(value) + } +} + +impl fmt::Display for Ranges { + 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 Ranges { + pub fn contains(&self, item: &U) -> bool + where + T: PartialOrd, + U: PartialOrd + ?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, + pairs: Vec, + unknown: Vec<(String, String)>, + print_help: bool, + print_version: bool, +} + +struct Pair { + key: Argument, + values: Vec, +} + +impl Scurvy { + pub fn make(args: Vec) -> Self { + let mut free = vec![]; + let mut pairs: Vec = 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>>(&self, key: &str, formula: F) -> Option { + let formula = formula.into(); + let Some(got) = self.get(key) else { + return None; + }; + + match got.parse::() { + 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>>(&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, + arg_name: Option<&'static str>, + help: &'static str, +} + +impl Argument { + pub fn new<'s, K: Into>>(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 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()) + } +} -- cgit 1.4.1-3-g733a5