mirror of
https://github.com/gay-pizza/jaarg.git
synced 2025-12-19 23:30:16 +00:00
I've moved to workspaces+ added homepage & authors
This commit is contained in:
12
jaarg/Cargo.toml
Normal file
12
jaarg/Cargo.toml
Normal 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
48
jaarg/examples/basic.rs
Normal 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
239
jaarg/examples/bin2h.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
24
jaarg/examples/btreemap.rs
Normal file
24
jaarg/examples/btreemap.rs
Normal 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
240
jaarg/src/argparse.rs
Normal 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
141
jaarg/src/const_utf8.rs
Normal 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
186
jaarg/src/help.rs
Normal 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
17
jaarg/src/lib.rs
Normal 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
189
jaarg/src/option.rs
Normal 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
54
jaarg/src/options.rs
Normal 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
127
jaarg/src/ordered_bitset.rs
Normal 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
105
jaarg/src/std.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user