Skip to content

feat(lexarg): Decouple compatibility surface, allowing for helpers #82

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Mar 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/lexarg-error/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pre-release-replacements = [
default = []

[dependencies]
lexarg = { "version" = "0.1.0", path = "../lexarg" }
lexarg-parser = { "version" = "0.1.0", path = "../lexarg-parser" }

[dev-dependencies]

Expand Down
43 changes: 27 additions & 16 deletions crates/lexarg-error/examples/hello-error.rs
Original file line number Diff line number Diff line change
@@ -1,49 +1,57 @@
use lexarg_error::ErrorContext;
use lexarg_error::Result;

struct Args {
thing: String,
number: u32,
shout: bool,
}

fn parse_args() -> Result<Args> {
fn parse_args() -> Result<Args, String> {
#![allow(clippy::enum_glob_use)]
use lexarg::Arg::*;
use lexarg_parser::Arg::*;

let mut thing = None;
let mut number = 1;
let mut shout = false;
let raw = std::env::args_os().collect::<Vec<_>>();
let mut parser = lexarg::Parser::new(&raw);
let mut parser = lexarg_parser::Parser::new(&raw);
let bin_name = parser
.next_raw()
.expect("nothing parsed yet so no attached lingering")
.expect("always at least one");
while let Some(arg) = parser.next_arg() {
match arg {
Short("n") | Long("number") => {
let value = parser
.next_flag_value()
.ok_or_else(|| ErrorContext::msg("missing required value").within(arg))?;
let value = parser.next_flag_value().ok_or_else(|| {
ErrorContext::msg("missing required value")
.within(arg)
.to_string()
})?;
number = value
.to_str()
.ok_or_else(|| {
ErrorContext::msg("invalid number")
.unexpected(Value(value))
.within(arg)
.to_string()
})?
.parse()
.map_err(|e| ErrorContext::msg(e).unexpected(Value(value)).within(arg))?;
.map_err(|e| {
ErrorContext::msg(e)
.unexpected(Value(value))
.within(arg)
.to_string()
})?;
}
Long("shout") => {
shout = true;
}
Value(val) if thing.is_none() => {
thing = Some(
val.to_str()
.ok_or_else(|| ErrorContext::msg("invalid string").unexpected(arg))?,
);
thing = Some(val.to_str().ok_or_else(|| {
ErrorContext::msg("invalid string")
.unexpected(arg)
.to_string()
})?);
}
Short("h") | Long("help") => {
println!("Usage: hello [-n|--number=NUM] [--shout] THING");
Expand All @@ -52,22 +60,25 @@ fn parse_args() -> Result<Args> {
_ => {
return Err(ErrorContext::msg("unexpected argument")
.unexpected(arg)
.within(Value(bin_name))
.into());
.to_string());
}
}
}

Ok(Args {
thing: thing
.ok_or_else(|| ErrorContext::msg("missing argument THING").within(Value(bin_name)))?
.ok_or_else(|| {
ErrorContext::msg("missing argument THING")
.within(Value(bin_name))
.to_string()
})?
.to_owned(),
number,
shout,
})
}

fn main() -> Result<()> {
fn main() -> Result<(), String> {
let args = parse_args()?;
let mut message = format!("Hello {}", args.thing);
if args.shout {
Expand Down
70 changes: 15 additions & 55 deletions crates/lexarg-error/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,52 +19,12 @@
#[cfg(doctest)]
pub struct ReadmeDoctests;

/// `Result` that defaults to [`Error`]
pub type Result<T, E = Error> = std::result::Result<T, E>;

/// Argument error type for use with lexarg
pub struct Error {
msg: String,
}

impl Error {
/// Create a new error object from a printable error message.
#[cold]
pub fn msg<M>(message: M) -> Self
where
M: std::fmt::Display,
{
Self {
msg: message.to_string(),
}
}
}

impl From<ErrorContext<'_>> for Error {
#[cold]
fn from(error: ErrorContext<'_>) -> Self {
Self::msg(error.to_string())
}
}

impl std::fmt::Debug for Error {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.msg.fmt(formatter)
}
}

impl std::fmt::Display for Error {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.msg.fmt(formatter)
}
}

/// Collect context for creating an [`Error`]
/// Collect context for creating an error
#[derive(Debug)]
pub struct ErrorContext<'a> {
msg: String,
within: Option<lexarg::Arg<'a>>,
unexpected: Option<lexarg::Arg<'a>>,
within: Option<lexarg_parser::Arg<'a>>,
unexpected: Option<lexarg_parser::Arg<'a>>,
}

impl<'a> ErrorContext<'a> {
Expand All @@ -81,16 +41,16 @@ impl<'a> ErrorContext<'a> {
}
}

/// [`Arg`][lexarg::Arg] the error occurred within
/// [`Arg`][lexarg_parser::Arg] the error occurred within
#[cold]
pub fn within(mut self, within: lexarg::Arg<'a>) -> Self {
pub fn within(mut self, within: lexarg_parser::Arg<'a>) -> Self {
self.within = Some(within);
self
}

/// The failing [`Arg`][lexarg::Arg]
/// The failing [`Arg`][lexarg_parser::Arg]
#[cold]
pub fn unexpected(mut self, unexpected: lexarg::Arg<'a>) -> Self {
pub fn unexpected(mut self, unexpected: lexarg_parser::Arg<'a>) -> Self {
self.unexpected = Some(unexpected);
self
}
Expand All @@ -112,10 +72,10 @@ impl std::fmt::Display for ErrorContext<'_> {
if let Some(unexpected) = &self.unexpected {
write!(formatter, ", found `")?;
match unexpected {
lexarg::Arg::Short(short) => write!(formatter, "-{short}")?,
lexarg::Arg::Long(long) => write!(formatter, "--{long}")?,
lexarg::Arg::Escape(value) => write!(formatter, "{value}")?,
lexarg::Arg::Value(value) | lexarg::Arg::Unexpected(value) => {
lexarg_parser::Arg::Short(short) => write!(formatter, "-{short}")?,
lexarg_parser::Arg::Long(long) => write!(formatter, "--{long}")?,
lexarg_parser::Arg::Escape(value) => write!(formatter, "{value}")?,
lexarg_parser::Arg::Value(value) | lexarg_parser::Arg::Unexpected(value) => {
write!(formatter, "{}", value.to_string_lossy())?;
}
}
Expand All @@ -124,10 +84,10 @@ impl std::fmt::Display for ErrorContext<'_> {
if let Some(within) = &self.within {
write!(formatter, " when parsing `")?;
match within {
lexarg::Arg::Short(short) => write!(formatter, "-{short}")?,
lexarg::Arg::Long(long) => write!(formatter, "--{long}")?,
lexarg::Arg::Escape(value) => write!(formatter, "{value}")?,
lexarg::Arg::Value(value) | lexarg::Arg::Unexpected(value) => {
lexarg_parser::Arg::Short(short) => write!(formatter, "-{short}")?,
lexarg_parser::Arg::Long(long) => write!(formatter, "--{long}")?,
lexarg_parser::Arg::Escape(value) => write!(formatter, "{value}")?,
lexarg_parser::Arg::Value(value) | lexarg_parser::Arg::Unexpected(value) => {
write!(formatter, "{}", value.to_string_lossy())?;
}
}
Expand Down
11 changes: 11 additions & 0 deletions crates/lexarg-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Change Log
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).

<!-- next-header -->
## [Unreleased] - ReleaseDate

<!-- next-url -->
[Unreleased]: https://github.com/rust-cli/argfile/compare/716170eaa853ddf3032baa9b107eb3e44d6a4124...HEAD
34 changes: 34 additions & 0 deletions crates/lexarg-parser/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
[package]
name = "lexarg-parser"
version = "0.1.0"
description = "Minimal, API stable CLI parser"
categories = ["command-line-interface"]
keywords = ["args", "arguments", "cli", "parser", "getopt"]
repository.workspace = true
license.workspace = true
edition.workspace = true
rust-version.workspace = true
include.workspace = true

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs", "--generate-link-to-definition"]

[package.metadata.release]
pre-release-replacements = [
{file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1},
{file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1},
{file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1},
{file="CHANGELOG.md", search="<!-- next-header -->", replace="<!-- next-header -->\n## [Unreleased] - ReleaseDate\n", exactly=1},
{file="CHANGELOG.md", search="<!-- next-url -->", replace="<!-- next-url -->\n[Unreleased]: https://github.com/epage/pytest-rs/compare/{{tag_name}}...HEAD", exactly=1},
]

[features]
default = []

[dependencies]

[dev-dependencies]

[lints]
workspace = true
Loading
Loading