I've moved to workspaces+ added homepage & authors

This commit is contained in:
2025-11-04 19:25:33 +11:00
parent a013e86067
commit 3953ac06c8
13 changed files with 19 additions and 6 deletions

12
jaarg/Cargo.toml Normal file
View File

@@ -0,0 +1,12 @@
[package]
name = "jaarg"
version.workspace = true
license.workspace = true
edition.workspace = true
description.workspace = true
homepage.workspace = true
authors.workspace = true
[features]
default = ["std"]
std = []

48
jaarg/examples/basic.rs Normal file
View File

@@ -0,0 +1,48 @@
/* basic - jaarg example program using parse_easy
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
use jaarg::{Opt, Opts, ParseControl, ParseResult};
use std::path::PathBuf;
fn main() {
// Variables for arguments to fill
let mut file = PathBuf::new();
let mut out: Option<PathBuf> = None;
let mut number = 0;
// Set up arguments table
enum Arg { Help, Number, File, Out }
const OPTIONS: Opts<Arg> = Opts::new(&[
Opt::help_flag(Arg::Help, &["-h", "--help"]).help_text("Show this help and exit."),
Opt::value(Arg::Number, &["-n", "--number"], "value")
.help_text("Optionally specify a number (default: 0)"),
Opt::positional(Arg::File, "file").required()
.help_text("Input file."),
Opt::positional(Arg::Out, "out")
.help_text("Output destination (optional).")
]).with_description("My simple utility.");
// Parse command-line arguments from `std::env::args()`
match OPTIONS.parse_easy(|program_name, id, _opt, _name, arg| {
match id {
Arg::Help => {
OPTIONS.print_full_help(program_name);
return Ok(ParseControl::Quit);
}
Arg::Number => { number = str::parse(arg)?; }
Arg::File => { file = arg.into(); }
Arg::Out => { out = Some(arg.into()); }
}
Ok(ParseControl::Continue)
}) {
ParseResult::ContinueSuccess => (),
ParseResult::ExitSuccess => std::process::exit(0),
ParseResult::ExitError => std::process::exit(1),
}
// Print the result variables
println!("{file:?} -> {out:?} (number: {number:?})",
out = out.unwrap_or(file.with_extension("out")));
}

239
jaarg/examples/bin2h.rs Normal file
View File

@@ -0,0 +1,239 @@
/* bin2c - jaarg example application
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
use jaarg::{Opt, Opts, ParseControl, ParseResult};
use std::fs::File;
use std::io::Write;
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::path::PathBuf;
use std::process::ExitCode;
/// Strip disallowed characters from a C preprocessor label
fn sanitise_label(ident: &str) -> String {
let mut out = String::new();
out.reserve(ident.len());
// Prevent leading underscore
let mut last = '_';
for mut i in ident.chars() {
if !out.is_empty() || !i.is_ascii_digit() {
if !i.is_alphanumeric() {
i = '_';
}
if i != '_' || last != '_' {
out.push(i);
}
last = i;
}
}
// Prevent trailing underscore
if last == '_' {
out.pop();
}
out
}
/// Turn filename into an include guard label
fn guard_name(name: &str) -> String {
let mut out = "BIN2H_".to_owned();
out.reserve(name.len());
out.extend(sanitise_label(name).chars().flat_map(|c| c.to_uppercase()));
// Ensure guard ends with _H
if !out.ends_with("_H") {
out += "_H";
}
out
}
/// If the job is for a plain text file or a binary file
enum JobType {
Binary,
Text,
}
/// Structure for reading jobs, containing the path and type of job
struct Job {
job_type: JobType,
path: PathBuf
}
struct Arguments {
out: PathBuf,
whitespace: String,
}
impl Default for Arguments {
fn default() -> Self {
Self {
out: PathBuf::new(),
whitespace: "\t".into(),
}
}
}
/// Write an array from a binary file
fn bin2h(name: &str, mut file: File, out: &mut File, whitespace: &str) -> std::io::Result<()> {
let ident = sanitise_label(name);
// Write length
let length = file.seek(SeekFrom::End(0))?;
file.seek(SeekFrom::Start(0))?;
writeln!(out, "#define {}_SIZE {}", ident.to_uppercase(), length)?;
// Write signature
writeln!(out, "static const unsigned char {ident}[{length}] = {{")?;
// Write values
let mut reader = BufReader::with_capacity(16, file);
let mut first_line = true;
loop {
// Get the next row of bytes
let bytes = reader.fill_buf()?;
let bytes_len = bytes.len();
if bytes.is_empty() {
writeln!(out)?;
break;
}
// Terminate the previous row
if first_line {
first_line = false;
} else {
writeln!(out, ",")?;
}
// Write row as hex bytes
for (col, byte) in bytes.iter().enumerate() {
let prefix = if col == 0 { whitespace } else { ", " };
write!(out, "{prefix}0x{byte:02X}")?;
}
reader.consume(bytes_len);
}
// Write array terminator
writeln!(out, "}};")?;
Ok(())
}
/// Write a C-string from a plain text file
fn txt2h(name: &str, file: File, out: &mut File, whitespace: &str) -> std::io::Result<()> {
let ident = sanitise_label(name);
// Write signature
writeln!(out, "static const char* const {ident} =")?;
// Write lines
let mut reader = BufReader::new(file);
let mut line = String::new();
let mut first_line = true;
loop {
if reader.read_line(&mut line)? == 0 {
// End of file
writeln!(out, ";")?;
break;
}
// Separate lines
if first_line {
first_line = false;
} else {
writeln!(out)?;
}
// Write line
write!(out, "{whitespace}\"")?;
for c in line.chars() {
match c {
// Escape backslash and double-quotes
'\\' => write!(out, "\\\\")?,
'"' => write!(out, "\\\"")?,
// Write control codes as character escapes
'\x07' => write!(out, "\\a")?,
'\x08' => write!(out, "\\b")?,
'\x0C' => write!(out, "\\f")?,
'\n' => write!(out, "\\n")?,
'\r' => write!(out, "\\r")?,
'\t' => write!(out, "\\t")?,
'\x0B' => write!(out, "\\v")?,
// Write ASCII control codes that don't have C character escapes as hex codes
_ if c.is_ascii_control() => write!(out, "\\x{:02X}", c as u32)?,
// Write remaining ASCII characters verbatim
_ if c.is_ascii() => write!(out, "{c}")?,
// Write non-ASCII characters as unicode escapes
..'\u{10000}' => write!(out, "\\u{:04X}", c as u32)?,
_ => write!(out, "\\U{:08X}", c as u32)?,
}
}
write!(out, "\"")?;
line.clear();
}
Ok(())
}
/// Generates and writes out a header file
fn write_h<'a, I: Iterator<Item = &'a Job>>(opt: &Arguments, jobs: I) -> std::io::Result<()> {
let mut out = File::create(&opt.out)?;
let guard = guard_name(&opt.out.file_name().unwrap().to_string_lossy());
writeln!(out, "/*DO NOT EDIT")?;
writeln!(out, " * Autogenerated by bin2h")?;
writeln!(out, " */")?;
writeln!(out)?;
writeln!(out, "#ifndef {guard}")?;
writeln!(out, "#define {guard}")?;
writeln!(out)?;
for job in jobs {
let name = job.path.file_stem().unwrap().to_string_lossy();
let file = File::open(&job.path)?;
match job.job_type {
JobType::Binary => bin2h(&name, file, &mut out, &opt.whitespace)?,
JobType::Text => txt2h(&name, file, &mut out, &opt.whitespace)?,
}
writeln!(out)?;
}
writeln!(out, "#endif/*{guard}*/")?;
Ok(())
}
pub fn main() -> ExitCode {
// Program arguments
let mut arguments = Arguments::default();
let mut jobs = vec![];
// Read & parse arguments from the command line, store results into the above structure
enum Arg { Out, Bin, Txt, Whitespace, Help }
const OPTIONS: Opts<Arg> = Opts::new(&[
Opt::help_flag(Arg::Help, &["--help", "-h"]).help_text("Show this help message and exit"),
Opt::positional(Arg::Out, "out").help_text("Path to generated header file").required(),
Opt::value(Arg::Bin, &["--bin", "-b"], "data.bin").help_text("Add a binary file"),
Opt::value(Arg::Txt, &["--txt", "-t"], "text.txt").help_text("Add a text file"),
Opt::value(Arg::Whitespace, &["--whitespace"], "\" \"").help_text("Emitted indentation (Default: \"\\t\")"),
]).with_description("Convert one or more binary and text file(s) to a C header file,\n\
as arrays and C strings respectively.");
match OPTIONS.parse_easy(|program_name, id, _opt, _name, arg| {
match id {
Arg::Out => { arguments.out = arg.into(); }
Arg::Bin => { jobs.push(Job { job_type: JobType::Binary, path: arg.into() }); }
Arg::Txt => { jobs.push(Job { job_type: JobType::Text, path: arg.into() }); }
Arg::Whitespace => { arguments.whitespace = arg.into(); }
Arg::Help => {
OPTIONS.print_full_help(program_name);
return Ok(ParseControl::Quit);
}
}
Ok(ParseControl::Continue)
}) {
ParseResult::ContinueSuccess => {
// Generate header
match write_h(&arguments, jobs.iter()) {
Ok(_) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("error: {err}");
ExitCode::FAILURE
}
}
},
ParseResult::ExitSuccess => { ExitCode::SUCCESS }
ParseResult::ExitError => { ExitCode::FAILURE }
}
}

View File

@@ -0,0 +1,24 @@
/* btreemap - jaarg example program using BTreeMap
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
use jaarg::{std::ParseMapResult, Opt, Opts};
use std::process::ExitCode;
fn main() -> ExitCode {
const OPTIONS: Opts<&'static str> = Opts::new(&[
Opt::help_flag("help", &["--help"]).help_text("Show this help"),
Opt::positional("positional", "positional").help_text("Positional argument"),
Opt::value("value", &["-v", "--value"], "string").help_text("Value option"),
Opt::flag("flag", &["-f", "--flag"]).help_text("Flag option"),
]);
let map = match OPTIONS.parse_map_easy() {
ParseMapResult::Map(map) => map,
ParseMapResult::Exit(code) => { return code; }
};
println!("{:?}", map);
ExitCode::SUCCESS
}

240
jaarg/src/argparse.rs Normal file
View File

@@ -0,0 +1,240 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
/// Enum describing the result of parsing arguments, and how the program should behave.
#[derive(Debug)]
pub enum ParseResult {
/// Parsing succeeded and program execution should continue.
ContinueSuccess,
/// Parsing succeeded and program should exit with success (eg; [std::process::ExitCode::SUCCESS]).
ExitSuccess,
/// There was an error while parsing and program should exit with failure (eg; [std::process::ExitCode::FAILURE]).
ExitError,
}
/// Execution control for parser handlers.
pub enum ParseControl {
/// Continue parsing arguments
Continue,
/// Tell the parser to stop consuming tokens (treat as end of token stream)
Stop,
/// Tell the parser to stop parsing and quit early, this will skip end of parsing checks
Quit,
}
/// Result type used by the handler passed to the parser.
type HandlerResult<'a, T> = core::result::Result<T, ParseError<'a>>;
#[derive(Debug)]
pub enum ParseError<'a> {
UnknownOption(&'a str),
UnexpectedToken(&'a str),
ExpectArgument(&'a str),
UnexpectedArgument(&'a str),
ArgumentError(&'static str, &'a str, ParseErrorKind),
//TODO
//Exclusive(&'static str, &'a str),
RequiredPositional(&'static str),
RequiredParameter(&'static str),
}
/// The type of parsing error
#[derive(Debug)]
pub enum ParseErrorKind {
IntegerEmpty,
IntegerRange,
InvalidInteger,
InvalidFloat,
}
impl core::fmt::Display for ParseError<'_> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::UnknownOption(o) => write!(f, "Unrecognised option '{o}'"),
Self::UnexpectedToken(t) => write!(f, "Unexpected positional argument '{t}'"),
Self::ExpectArgument(o) => write!(f, "Option '{o}' requires an argument"),
Self::UnexpectedArgument(o) => write!(f, "Flag '{o}' doesn't take an argument"),
Self::ArgumentError(o, a, ParseErrorKind::IntegerRange)
=> write!(f, "Argument '{a}' out of range for option '{o}'"),
Self::ArgumentError(o, a, ParseErrorKind::InvalidInteger | ParseErrorKind::InvalidFloat)
=> write!(f, "Invalid argument '{a}' for option '{o}'"),
Self::ArgumentError(o, _, ParseErrorKind::IntegerEmpty)
=> write!(f, "Argument for option '{o}' cannot be empty"),
//Self::Exclusive(l, r) => write!(f, "Argument {l}: not allowed with argument {r}"),
Self::RequiredPositional(o) => write!(f, "Missing required positional argument '{o}'"),
Self::RequiredParameter(o) => write!(f, "Missing required option '{o}'"),
}
}
}
/// Convenience coercion for dealing with integer parsing errors.
impl From<core::num::ParseIntError> for ParseError<'_> {
fn from(err: core::num::ParseIntError) -> Self {
use core::num::IntErrorKind;
// HACK: The empty option & argument fields will be fixed up by the parser
Self::ArgumentError("", "", match err.kind() {
IntErrorKind::Empty => ParseErrorKind::IntegerEmpty,
IntErrorKind::PosOverflow | IntErrorKind::NegOverflow | IntErrorKind::Zero
=> ParseErrorKind::IntegerRange,
IntErrorKind::InvalidDigit | _ => ParseErrorKind::InvalidInteger,
})
}
}
/// Convenience coercion for dealing with floating-point parsing errors.
impl From<core::num::ParseFloatError> for ParseError<'_> {
fn from(_err: core::num::ParseFloatError) -> Self {
// HACK: The empty option & argument fields will be fixed up by the parser
// NOTE: Unlike ParseIntError, ParseFloatError does not expose kind publicly yet
Self::ArgumentError("", "", ParseErrorKind::InvalidFloat)
}
}
impl core::error::Error for ParseError<'_> {}
/// Internal state tracked by the parser.
struct ParserState<ID: 'static> {
positional_index: usize,
expects_arg: Option<(&'static str, &'static Opt<ID>)>,
required_param_presences: RequiredParamsBitSet,
}
impl<ID> Default for ParserState<ID> {
fn default() -> Self {
Self {
positional_index: 0,
expects_arg: None,
required_param_presences: Default::default(),
}
}
}
impl<ID: 'static> Opts<ID> {
/// Parses an iterator of strings as argument tokens.
pub fn parse<'a, S: AsRef<str> + 'a, I: Iterator<Item = S>>(&self, program_name: &str, args: I,
mut handler: impl FnMut(&str, &ID, &Opt<ID>, &str, &str) -> HandlerResult<'a, ParseControl>,
error: impl FnOnce(&str, ParseError),
) -> ParseResult {
let mut state = ParserState::default();
for arg in args {
// Fetch the next token
match self.next(&mut state, arg.as_ref(), program_name, &mut handler) {
Ok(ParseControl::Continue) => {}
Ok(ParseControl::Stop) => { break; }
Ok(ParseControl::Quit) => { return ParseResult::ExitSuccess; }
Err(err) => {
// Call the error handler
error(program_name, err);
return ParseResult::ExitError;
}
}
}
// Ensure that value options are provided a value
if let Some((name, _)) = state.expects_arg.take() {
error(program_name, ParseError::ExpectArgument(name));
return ParseResult::ExitError;
}
// Ensure that all required arguments have been provided
let mut required_flag_idx = 0;
for (i, option) in self.options.iter().enumerate() {
match option.r#type {
OptType::Positional => if i >= state.positional_index && option.is_required() {
error(program_name, ParseError::RequiredPositional(option.first_name()));
return ParseResult::ExitError;
}
OptType::Flag | OptType::Value => if option.is_required() {
if !state.required_param_presences.get(required_flag_idx) {
error(program_name, ParseError::RequiredParameter(option.first_name()));
return ParseResult::ExitError;
}
required_flag_idx += 1;
}
}
}
// All arguments parsed successfully
ParseResult::ContinueSuccess
}
/// Parse the next token in the argument stream
fn next<'a, 'b>(&self, state: &mut ParserState<ID>, token: &'b str, program_name: &str,
handler: &mut impl FnMut(&str, &ID, &Opt<ID>, &str, &str) -> HandlerResult<'a, ParseControl>
) -> HandlerResult<'b, ParseControl> where 'a: 'b {
let mut call_handler = |option: &Opt<ID>, name, value| {
match handler(program_name, &option.id, option, name, value) {
// HACK: Ensure the string fields are set properly, because coerced
// ParseIntError/ParseFloatError will have the string fields blanked.
Err(ParseError::ArgumentError("", "", kind))
=> Err(ParseError::ArgumentError(name, value, kind)),
Err(err) => Err(err),
Ok(ctl) => Ok(ctl),
}
};
// If the previous token is expecting an argument, ie: value a value option
// was matched and didn't have an equals sign separating a value,
// then call the handler here.
if let Some((name, option)) = state.expects_arg.take() {
call_handler(option, name, token)
} else {
// Check if the next argument token starts with an option flag
if self.flag_chars.chars().any(|c| token.starts_with(c)) {
// Value options can have their value delineated by an equals sign or with whitespace.
// In the latter case; the value will be in the next token.
let (option_str, value_str) = token.split_once("=")
.map_or((token, None), |(k, v)| (k, Some(v)));
// Keep track of how many required options we've seen
let mut required_idx = 0;
// Match a suitable option by name (ignoring the first flag character & skipping positional arguments)
let (name, option) = self.options.iter()
.filter(|opt| matches!(opt.r#type, OptType::Flag | OptType::Value)).find_map(|opt| {
if let Some(name) = opt.match_name(option_str, 1) {
Some((name, opt))
} else {
if opt.is_required() {
required_idx += 1
}
None
}
}).ok_or(ParseError::UnknownOption(option_str))?;
// Mark required option as visited
if option.is_required() {
state.required_param_presences.insert(required_idx, true);
}
match (&option.r#type, value_str) {
// Call handler for flag-only options
(OptType::Flag, None) => call_handler(option, name, ""),
// Value was provided this token, so call the handler right now
(OptType::Value, Some(value)) => call_handler(option, name, value),
// No value available in this token, delay handling to next token
(OptType::Value, None) => {
state.expects_arg = Some((name, option));
Ok(ParseControl::Continue)
}
// Flag-only options do not support arguments
(OptType::Flag, Some(_)) => Err(ParseError::UnexpectedArgument(option_str)),
// Positional arguments are filtered out so this is impossible
(OptType::Positional, _) => unreachable!("Won't parse a positional argument as an option"),
}
} else {
// Find the next positional argument
for (i, option) in self.options[state.positional_index..].iter().enumerate() {
if matches!(option.r#type, OptType::Positional) {
handler(program_name, &option.id, option, option.first_name(), token)?;
state.positional_index += i + 1;
return Ok(ParseControl::Continue);
}
}
Err(ParseError::UnexpectedToken(token))
}
}
}
}

141
jaarg/src/const_utf8.rs Normal file
View File

@@ -0,0 +1,141 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
/// Fully const fn nostd UTF-8 character iterator.
/// Assumes a well-formed UTF-8 input string. Doesn't take into account graphemes.
pub(crate) struct CharIterator<'a> {
bytes: &'a [u8],
index: usize
}
impl<'a> CharIterator<'a> {
/// Create a char iterator from an immutable string slice.
#[inline]
pub(crate) const fn from(value: &'a str) -> Self {
Self {
bytes: value.as_bytes(),
index: 0,
}
}
}
impl CharIterator<'_> {
/// Gets a count of the number of Unicode characters (not graphemes) in the string.
pub(crate) const fn count(&self) -> usize {
let len = self.bytes.len();
let mut count = 0;
let mut i = 0;
while i < len {
// Count all bytes that don't start with 0b10xx_xxxx (UTF-8 continuation byte)
if (self.bytes[i] as i8) >= -64 {
count += 1;
}
i += 1;
}
count
}
/// Gets the next character in a well-formed UTF-8 string, or None for end of string or errors.
pub(crate) const fn next(&mut self) -> Option<char> {
/// UTF-8 2-byte flag bits
const MULTIBYTE_2: u8 = 0b1100_0000;
/// UTF-8 3-byte flag bits
const MULTIBYTE_3: u8 = 0b1110_0000;
/// UTF-8 4-byte flag bits
const MULTIBYTE_4: u8 = 0b1111_0000;
/// Mask for UTF-8 2-byte flag bits
const MULTIBYTE_2_MASK: u8 = 0b1110_0000;
/// Mask for UTF-8 3-byte flag bits
const MULTIBYTE_3_MASK: u8 = 0b1111_0000;
/// Mask for UTF-8 4-byte flag bits
const MULTIBYTE_4_MASK: u8 = 0b1111_1000;
/// UTF-8 continuation flag bits
const CONTINUATION: u8 = 0b1000_0000;
/// Mask for the UTF-8 continuation flag bits
const CONTINUATION_MASK: u8 = 0b1100_0000;
/// Checks if a byte begins with the UTF-8 continuation bits
#[inline] const fn is_continuation(b: u8) -> bool { b & CONTINUATION_MASK == CONTINUATION }
/// Gets the value bits of a UTF-8 continuation byte as u32
#[inline] const fn cont_bits(b: u8) -> u32 { (b & !CONTINUATION_MASK) as u32 }
// Return early if we reached the end of the string
if self.index >= self.bytes.len() {
return None;
}
let byte0 = self.bytes[self.index];
// Get the length of the next multibyte UTF-8 character
let len = match byte0 {
..0x80 => 1,
_ if (byte0 & MULTIBYTE_2_MASK) == MULTIBYTE_2 => 2,
_ if (byte0 & MULTIBYTE_3_MASK) == MULTIBYTE_3 => 3,
_ if (byte0 & MULTIBYTE_4_MASK) == MULTIBYTE_4 => 4,
_ => {
return None;
}
};
// Return early for incomplete sequences
if len > self.bytes.len() - self.index {
return None;
}
// Try to read the next multibyte character
let Some(result) = (match len {
1 => Some(byte0 as char),
2 if is_continuation(self.bytes[self.index + 1])
=> {
let cp = (((byte0 & !MULTIBYTE_2_MASK) as u32) << 6) | cont_bits(self.bytes[self.index + 1]);
char::from_u32(cp)
},
3 if is_continuation(self.bytes[self.index + 1])
&& is_continuation(self.bytes[self.index + 2])
=> {
let cp = (((byte0 & !MULTIBYTE_3_MASK) as u32) << 12)
| (cont_bits(self.bytes[self.index + 1]) << 6)
| cont_bits(self.bytes[self.index + 2]);
char::from_u32(cp)
}
4 if is_continuation(self.bytes[self.index + 1])
&& is_continuation(self.bytes[self.index + 2])
&& is_continuation(self.bytes[self.index + 3])
=> {
let cp = (((byte0 & !MULTIBYTE_4_MASK) as u32) << 18)
| (cont_bits(self.bytes[self.index + 1]) << 12)
| (cont_bits(self.bytes[self.index + 2]) << 6)
| cont_bits(self.bytes[self.index + 3]);
char::from_u32(cp)
}
_ => None,
}) else {
return None
};
// Advance the internal character index and return success
self.index += len;
Some(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test() {
for s in ["pizza", "/ˈpitt͡sə/", "pizzaskjærer", "🍕", "比薩", "ピザ", "Ćevapi", "🏳️‍⚧️"] {
let mut it = CharIterator::from(s);
assert_eq!(it.count(), s.chars().count());
s.chars().for_each(|c| assert_eq!(it.next(), Some(c)));
assert_eq!(it.next(), None);
}
}
}

186
jaarg/src/help.rs Normal file
View File

@@ -0,0 +1,186 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
pub struct HelpWriterContext<'a, ID: 'static> {
pub options: &'a Opts<ID>,
pub program_name: &'a str,
}
pub trait HelpWriter<'a, ID: 'static>: core::fmt::Display {
fn new(ctx: HelpWriterContext<'a, ID>) -> Self;
}
pub struct StandardShortUsageWriter<'a, ID: 'static>(HelpWriterContext<'a, ID>);
impl<'a, ID: 'static> HelpWriter<'a, ID> for StandardShortUsageWriter<'a, ID> {
fn new(ctx: HelpWriterContext<'a, ID>) -> Self { Self(ctx) }
}
impl<ID: 'static> core::fmt::Display for StandardShortUsageWriter<'_, ID> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "Usage: {}", self.0.program_name)?;
// Write option parameter arguments
for option in self.0.options.options.iter()
.filter(|o| matches!(o.r#type, OptType::Value | OptType::Flag)) {
write!(f, " {}", if option.is_required() { '<' } else { '[' })?;
match (option.first_short_name(), option.first_long_name()) {
(Some(short_name), Some(long_name)) => write!(f, "{short_name}|{long_name}")?,
(Some(short_name), None) => f.write_str(short_name)?,
(None, Some(long_name)) => f.write_str(long_name)?,
_ => unreachable!(),
}
if let Some(value_name) = option.value_name {
write!(f, " {value_name}")?;
}
write!(f, "{}", if option.is_required() { '>' } else { ']' })?;
}
// Write positional arguments
for option in self.0.options.options.iter()
.filter(|o| matches!(o.r#type, OptType::Positional)) {
let name = option.first_name();
match option.is_required() {
true => write!(f, " <{name}>")?,
false => write!(f, " [{name}]")?,
}
}
Ok(())
}
}
pub struct StandardFullHelpWriter<'a, ID: 'static>(HelpWriterContext<'a, ID>);
impl<'a, ID: 'static> HelpWriter<'a, ID> for StandardFullHelpWriter<'a, ID> {
fn new(ctx: HelpWriterContext<'a, ID>) -> Self { Self(ctx) }
}
impl<ID> core::fmt::Display for StandardFullHelpWriter<'_, ID> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
use core::fmt::Write;
// Base usage
write!(f, "Usage: {}", self.0.program_name)?;
let short_flag = self.0.options.flag_chars.chars().next().unwrap();
// Write optional short options
let mut first = true;
for option in self.0.options.options {
if let (OptType::Flag | OptType::Value, false) = (option.r#type, option.is_required()) {
if let Some(c) = option.first_short_name_char() {
if first {
write!(f, " [{short_flag}")?;
first = false;
}
f.write_char(c)?;
}
}
}
if !first {
f.write_char(']')?;
}
// Write required short options
first = true;
for option in self.0.options.options {
if let (OptType::Flag | OptType::Value, true) = (option.r#type, option.is_required()) {
if let Some(c) = option.first_short_name_char() {
if first {
write!(f, " <{short_flag}")?;
first = false;
}
f.write_char(c)?;
}
}
}
if !first {
f.write_char('>')?;
}
// Write positional arguments
for option in self.0.options.options.iter()
.filter(|o| matches!(o.r#type, OptType::Positional)) {
let name = option.first_name();
match option.is_required() {
true => write!(f, " <{name}>")?,
false => write!(f, " [{name}]")?,
}
}
writeln!(f)?;
if let Some(description) = self.0.options.description {
writeln!(f)?;
writeln!(f, "{description}")?;
}
fn calculate_left_pad<ID: 'static>(option: &Opt<ID>) -> usize {
(match option.names {
OptIdentifier::Single(name) => name.chars().count(),
OptIdentifier::Multi(names) => (names.len() - 1) * 3 + names.iter()
.fold(0, |accum, name| accum + name.chars().count()),
}) + option.value_name.map_or(0, |v| v.len() + 3)
}
// Determine the alignment width from the longest option parameter
let align_width = 2 + self.0.options.options.iter()
.map(|o| calculate_left_pad(o)).max().unwrap_or(0);
// Write positional argument descriptions
first = true;
for option in self.0.options.options.iter()
.filter(|o| matches!(o.r#type, OptType::Positional)) {
if first {
// Write separator and positional section header
writeln!(f)?;
writeln!(f, "Positional arguments:")?;
first = false;
}
// Write positional argument line
write!(f, " {name}", name = option.first_name())?;
if let Some(help_text) = option.help_string {
write!(f, " {:.<width$} {help_text}", "",
width = align_width - calculate_left_pad(option))?;
}
writeln!(f)?;
}
// Write option parameter argument descriptions
first = true;
for option in self.0.options.options.iter()
.filter(|o| matches!(o.r#type, OptType::Flag | OptType::Value)) {
if first {
// Write separator and options section header
writeln!(f)?;
writeln!(f, "Options:")?;
first = false;
}
// Write option flag name(s)
match option.names {
OptIdentifier::Single(name) => {
write!(f, " {name}")?;
}
OptIdentifier::Multi(names) => for (i, name) in names.iter().enumerate() {
write!(f, "{prefix}{name}", prefix = if i == 0 { " " } else { " | " })?;
}
}
// Write value argument for value options parameters
if let Some(value_name) = option.value_name {
write!(f, " <{value_name}>")?;
}
// Write padding and help text
if let Some(help_text) = option.help_string {
write!(f, " {:.<width$} {help_text}", "",
width = align_width - calculate_left_pad(option))?;
}
writeln!(f)?;
}
Ok(())
}
}

17
jaarg/src/lib.rs Normal file
View File

@@ -0,0 +1,17 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
#![no_std]
mod const_utf8;
mod ordered_bitset;
include!("option.rs");
include!("options.rs");
include!("argparse.rs");
include!("help.rs");
#[cfg(feature = "std")]
pub mod std;

189
jaarg/src/option.rs Normal file
View File

@@ -0,0 +1,189 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
#[derive(Debug, Copy, Clone)]
enum OptType {
Positional,
Flag,
Value,
}
#[derive(Debug)]
enum OptIdentifier {
Single(&'static str),
Multi(&'static[&'static str]),
}
/// Represents an option argument or positional argument to be parsed.
#[derive(Debug)]
pub struct Opt<ID> {
id: ID,
names: OptIdentifier,
value_name: Option<&'static str>,
help_string: Option<&'static str>,
r#type: OptType,
flags: OptFlag,
}
#[derive(Debug)]
struct OptFlag(u8);
impl OptFlag {
pub const REQUIRED: Self = OptFlag(1 << 0);
pub const HELP: Self = OptFlag(1 << 1);
pub const NONE: Self = OptFlag(0);
}
// TODO: Improve this interface by making the name field take AsOptIdentifier when const traits are stabilised
impl<ID> Opt<ID> {
#[inline]
const fn new(id: ID, names: OptIdentifier, value_name: Option<&'static str>, r#type: OptType) -> Self {
assert!(match names {
OptIdentifier::Single(_) => true,
OptIdentifier::Multi(names) => !names.is_empty(),
}, "Option names cannot be an empty slice");
Self { id, names, value_name, help_string: None, r#type, flags: OptFlag::NONE }
}
/// A positional argument that is parsed sequentially without being invoked by an option flag.
pub const fn positional(id: ID, name: &'static str) -> Self {
Self::new(id, OptIdentifier::Single(name), None, OptType::Positional)
}
/// A flag-type option that serves as the interface's help flag.
pub const fn help_flag(id: ID, names: &'static[&'static str]) -> Self {
Self::new(id, OptIdentifier::Multi(names), None, OptType::Flag)
.with_help_flag()
}
/// A flag-type option, takes no value.
pub const fn flag(id: ID, names: &'static[&'static str]) -> Self {
Self::new(id, OptIdentifier::Multi(names), None, OptType::Flag)
}
/// An option argument that takes a value.
pub const fn value(id: ID, names: &'static[&'static str], value_name: &'static str) -> Self {
Self::new(id, OptIdentifier::Multi(names), Some(value_name), OptType::Value)
}
/// This option is required, ie; parsing will fail if it is not specified.
#[inline]
pub const fn required(mut self) -> Self {
assert!(!self.is_help(), "Help flag cannot be made required");
self.flags.0 |= OptFlag::REQUIRED.0;
self
}
/// Sets the help string for an option.
#[inline]
pub const fn help_text(mut self, help_string: &'static str) -> Self {
self.help_string = Some(help_string);
self
}
#[inline]
const fn with_help_flag(mut self) -> Self {
assert!(matches!(self.r#type, OptType::Flag), "Only flags are allowed to be help options");
self.flags.0 |= OptFlag::HELP.0;
self
}
#[inline(always)] const fn is_required(&self) -> bool { (self.flags.0 & OptFlag::REQUIRED.0) != 0 }
#[inline(always)] const fn is_help(&self) -> bool { (self.flags.0 & OptFlag::HELP.0) != 0 }
}
impl<ID: 'static> Opt<ID> {
/// Get the first name of the option.
const fn first_name(&self) -> &str {
match self.names {
OptIdentifier::Single(name) => name,
OptIdentifier::Multi(names) => names.first().unwrap(),
}
}
/// Get the first long option name, if one exists.
const fn first_long_name(&self) -> Option<&'static str> {
match self.names {
OptIdentifier::Single(name) => if name.len() >= 3 { Some(name) } else { None },
// Can be replaced with `find_map` once iterators are const fn
OptIdentifier::Multi(names) => {
let mut i = 0;
while i < names.len() {
if const_utf8::CharIterator::from(names[i]).count() >= 3 {
return Some(names[i]);
}
i += 1;
}
None
}
}
}
/// Get the first short option name, if one exists.
const fn first_short_name(&self) -> Option<&'static str> {
const fn predicate(name: &str) -> bool {
let mut chars = const_utf8::CharIterator::from(name);
if let Some(first) = chars.next() {
if let Some(c) = chars.next() {
if c != first && chars.next().is_none() {
return true
}
}
}
false
}
match self.names {
OptIdentifier::Single(name) => if predicate(&name) { Some(name) } else { None },
// Can be replaced with `find_map` once iterators are const fn
OptIdentifier::Multi(names) => {
let mut i = 0;
while i < names.len() {
if predicate(names[i]) {
return Some(names[i]);
}
i += 1;
}
None
}
}
}
/// Get the first applicable short option's flag character, if one exists.
const fn first_short_name_char(&self) -> Option<char> {
const fn predicate(name: &str) -> Option<char> {
let mut chars = const_utf8::CharIterator::from(name);
if let Some(first) = chars.next() {
if let Some(c) = chars.next() {
if c != first && chars.next().is_none() {
return Some(c)
}
}
}
None
}
match self.names {
OptIdentifier::Single(name) => predicate(&name),
// Can be replaced with `find_map` once iterators are const fn.
OptIdentifier::Multi(names) => {
let mut i = 0;
while i < names.len() {
if let Some(c) = predicate(names[i]) {
return Some(c);
}
i += 1;
}
None
}
}
}
/// Search for a matching name in the option, offset allows to skip the first `n = offset` characters in the comparison.
fn match_name(&self, string: &str, offset: usize) -> Option<&'static str> {
match self.names {
OptIdentifier::Single(name) =>
if name[offset..] == string[offset..] { Some(name) } else { None },
OptIdentifier::Multi(names) =>
names.iter().find(|name| name[offset..] == string[offset..]).map(|v| &**v),
}
}
}

54
jaarg/src/options.rs Normal file
View File

@@ -0,0 +1,54 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
/// Static structure that contains instructions for parsing command-line arguments.
pub struct Opts<ID: 'static> {
/// List of options
options: &'static[Opt<ID>],
/// String containing single characters that match option prefixes
flag_chars: &'static str,
/// A description of what the program does
description: Option<&'static str>,
}
type RequiredParamsBitSet = ordered_bitset::OrderedBitSet<u32, 4>;
/// The maximum amount of allowed required non-positional options.
pub const MAX_REQUIRED_OPTIONS: usize = RequiredParamsBitSet::CAPACITY;
impl<ID: 'static> Opts<ID> {
/// Build argument parser options with the default flag character of '-'.
pub const fn new(options: &'static[Opt<ID>]) -> Self {
// Validate passed options
let mut opt_idx = 0;
let mut num_required_parameters = 0;
while opt_idx < options.len() {
if matches!(options[opt_idx].r#type, OptType::Flag | OptType::Value) && options[opt_idx].is_required() {
num_required_parameters += 1;
}
opt_idx += 1;
}
assert!(num_required_parameters <= RequiredParamsBitSet::CAPACITY,
"More than 128 non-positional required option entries is not supported at this time");
Self {
options,
flag_chars: "-",
description: None,
}
}
/// Set the recognised flag/option characters.
pub const fn with_flag_chars(mut self, flag_chars: &'static str) -> Self {
self.flag_chars = flag_chars;
self
}
/// Set the description of the program, available to help writers.
pub const fn with_description(mut self, description: &'static str) -> Self {
self.description = Some(description);
self
}
}

127
jaarg/src/ordered_bitset.rs Normal file
View File

@@ -0,0 +1,127 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
#![allow(private_bounds)]
use core::ops::{BitAnd, BitAndAssign, BitOrAssign, Not, Shl};
pub(crate) struct OrderedBitSet<T: OrderedBitSetStorage, const S: usize>([T; S]);
impl<T: OrderedBitSetStorage, const S: usize> Default for OrderedBitSet<T, S> {
fn default() -> Self { Self::new() }
}
// TODO: Obvious target for improvement when const traits land
impl<T: OrderedBitSetStorage, const S: usize> OrderedBitSet<T, S> {
/// Number of slots in the bit set.
pub(crate) const CAPACITY: usize = T::BITS as usize * S;
/// Creates a new, empty bit set.
pub(crate) const fn new() -> Self { Self([T::ZERO; S]) }
/// Sets the slot at `index` to a binary value.
pub(crate) fn insert(&mut self, index: usize, value: bool) {
let (array_idx, bit_idx) = self.internal_index(index);
let bit_mask = T::from_usize(0b1) << T::from_usize(bit_idx);
if value {
self.0[array_idx] |= bit_mask;
} else {
self.0[array_idx] &= !bit_mask;
}
}
/// Gets the binary value at slot `index`.
pub(crate) fn get(&self, index: usize) -> bool {
let (array_idx, bit_idx) = self.internal_index(index);
let bit_mask = T::from_usize(0b1) << T::from_usize(bit_idx);
(self.0[array_idx] & bit_mask) != T::from_usize(0)
}
#[inline]
const fn internal_index(&self, index: usize) -> (usize, usize) {
debug_assert!(index < Self::CAPACITY, "Index out of range");
let array_idx = index >> T::SHIFT;
let bit_idx = index & T::MASK;
(array_idx, bit_idx)
}
}
trait OrderedBitSetStorage: core::fmt::Debug
+ Default + Copy + Clone + Eq + PartialEq
+ BitAnd<Output = Self> + Shl<Output = Self> + Not<Output = Self>
+ BitAndAssign + BitOrAssign {
const ZERO: Self;
const SHIFT: u32;
const MASK: usize;
const BITS: u32;
fn from_usize(value: usize) -> Self;
}
macro_rules! impl_bitset_storage {
($t:ty, $b:expr) => {
impl OrderedBitSetStorage for $t {
const ZERO: $t = 0;
const SHIFT: u32 = $b.ilog2();
const MASK: usize = $b as usize - 1;
const BITS: u32 = $b;
#[inline(always)]
fn from_usize(value: usize) -> $t { value as $t }
}
};
}
impl_bitset_storage!(u8, u8::BITS);
impl_bitset_storage!(u16, u16::BITS);
impl_bitset_storage!(u32, u32::BITS);
impl_bitset_storage!(u64, u64::BITS);
impl_bitset_storage!(u128, u128::BITS);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bitset_storage() {
fn harness<T: OrderedBitSetStorage + core::fmt::Debug>(bits_expect: u32, shift_expect: u32) {
assert_eq!(T::ZERO, T::from_usize(0));
assert_eq!(T::SHIFT, shift_expect);
assert_eq!(T::MASK, bits_expect as usize - 1);
assert_eq!(T::BITS, bits_expect);
}
harness::<u8>(8, 3);
harness::<u16>(16, 4);
harness::<u32>(32, 5);
harness::<u64>(64, 6);
harness::<u128>(128, 7);
}
#[test]
fn test_ordered_bitset() {
fn harness<T: OrderedBitSetStorage, const S: usize>(indices: &[usize]) {
assert_eq!(OrderedBitSet::<T, S>::CAPACITY, 128);
let mut bitset = OrderedBitSet::<T, S>::new();
for &index in indices {
bitset.insert(index, true);
}
for slot in 0..OrderedBitSet::<u32, 4>::CAPACITY {
assert_eq!(bitset.get(slot), indices.contains(&slot));
}
for &index in indices {
bitset.insert(index, false);
assert!(!bitset.get(index));
}
}
let indices = [1, 32, 33, 127, 44, 47, 49];
harness::<u8, 16>(&indices);
harness::<u16, 8>(&indices);
harness::<u32, 4>(&indices);
harness::<u64, 2>(&indices);
harness::<u128, 1>(&indices);
}
}

105
jaarg/src/std.rs Normal file
View File

@@ -0,0 +1,105 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT
*/
extern crate std;
use crate::{HandlerResult, HelpWriter, HelpWriterContext, Opt, Opts, ParseControl, ParseError, ParseResult, StandardFullHelpWriter, StandardShortUsageWriter};
use std::collections::BTreeMap;
use std::path::Path;
use std::rc::Rc;
use std::string::String;
use std::{env, eprintln, println};
impl<ID: 'static> Opts<ID> {
/// Wrapper around [Opts::parse] that gathers arguments from the command line and prints errors to stderr.
/// The errors are formatted in a standard user-friendly format.
///
/// Requires `features = [std]`.
pub fn parse_easy<'a>(&self, handler: impl FnMut(&str, &ID, &Opt<ID>, &str, &str) -> HandlerResult<'a, ParseControl>
) -> ParseResult {
let (program_name, argv) = Self::easy_args();
self.parse(&program_name, argv, handler, |name, e| self.easy_error(name, e))
}
/// Prints full help text for the options using the standard full.
///
/// Requires `features = [std]`.
pub fn print_full_help(&self, program_name: &str) {
self.print_help::<StandardFullHelpWriter<'_, ID>>(program_name);
}
/// Print help text to stdout using the provided help writer.
///
/// Requires `features = [std]`.
pub fn print_help<'a, W: HelpWriter<'a, ID>>(&'a self, program_name: &'a str) {
let ctx = HelpWriterContext { options: self, program_name };
println!("{}", W::new(ctx));
}
/// Print help text to stderr using the provided help writer.
///
/// Requires `features = [std]`.
pub fn eprint_help<'a, W: HelpWriter<'a, ID>>(&'a self, program_name: &'a str) {
let ctx = HelpWriterContext { options: self, program_name };
eprintln!("{}", W::new(ctx));
}
fn easy_args<'a>() -> (Rc<str>, env::Args) {
let mut argv = env::args();
let argv0 = argv.next().unwrap();
let program_name = Path::new(&argv0).file_name().unwrap().to_string_lossy();
(program_name.into(), argv)
}
fn easy_error(&self, program_name: &str, err: ParseError) {
eprintln!("{program_name}: {err}");
self.eprint_help::<StandardShortUsageWriter<'_, ID>>(program_name);
if let Some(help_option) = self.options.iter().find(|o| o.is_help()) {
eprintln!("Run '{program_name} {help}' to view all available options.",
help = help_option.first_long_name().unwrap_or(help_option.first_name()));
}
}
}
/// The result of parsing commands with [Opts::parse_map].
pub enum ParseMapResult {
Map(BTreeMap<&'static str, String>),
Exit(std::process::ExitCode),
}
impl Opts<&'static str> {
/// Parse an iterator of strings as arguments and return the results in a [BTreeMap].
///
/// Requires `features = [std]`.
pub fn parse_map<'a, S: AsRef<str> + 'a, I: Iterator<Item = S>>(&self, program_name: &str, args: I,
help: impl Fn(&str), error: impl FnOnce(&str, ParseError)
) -> ParseMapResult {
let mut out: BTreeMap<&'static str, String> = BTreeMap::new();
match self.parse(&program_name, args, |_program_name, id, opt, _name, arg| {
if opt.is_help() {
help(program_name);
Ok(ParseControl::Quit)
} else {
out.insert(id, arg.into());
Ok(ParseControl::Continue)
}
}, error) {
ParseResult::ContinueSuccess => ParseMapResult::Map(out),
ParseResult::ExitSuccess => ParseMapResult::Exit(std::process::ExitCode::SUCCESS),
ParseResult::ExitError => ParseMapResult::Exit(std::process::ExitCode::FAILURE),
}
}
/// Parse arguments from the command line and return the results in a [BTreeMap].
/// Help and errors are formatted in a standard user-friendly format.
///
/// Requires `features = [std]`.
pub fn parse_map_easy(&self) -> ParseMapResult {
let (program_name, argv) = Self::easy_args();
self.parse_map(&program_name, argv,
|name| self.print_full_help(name),
|name, e| self.easy_error(name, e))
}
}