about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2025-04-29 14:45:01 -0500
committergennyble <gen@nyble.dev>2025-04-29 14:45:01 -0500
commitd7bdd477e266864e45ee33b29adba17bf3f70b20 (patch)
tree142745a311088c6ad274006bfb150f97758deea3
downloadscurvy-d7bdd477e266864e45ee33b29adba17bf3f70b20.tar.gz
scurvy-d7bdd477e266864e45ee33b29adba17bf3f70b20.zip
initial commit
-rw-r--r--.gitignore1
-rw-r--r--.rustfmt.toml1
-rw-r--r--Cargo.lock7
-rw-r--r--Cargo.toml6
-rw-r--r--examples/copyline.rs63
-rw-r--r--src/formula.rs192
-rw-r--r--src/lib.rs203
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())
+	}
+}