42 Commits
v0.1.0 ... main

Author SHA1 Message Date
44b2addecb Merge branch '0.2.x'
# Conflicts:
#	jaarg/src/argparse.rs
2025-11-26 15:40:14 +11:00
75fc27bc58 Release 0.2.2 2025-11-26 15:30:43 +11:00
8d9e8aea89 Cargo stinky aaaa 2025-11-26 15:26:07 +11:00
20f5a0bf10 Backport positional argument handler fix. 2025-11-26 15:12:49 +11:00
7165bb9841 Rename test modules for previously included files.
(cherry picked from commit 75e2bde5fb)
2025-11-26 15:03:55 +11:00
8f6f1827ce Add simple argparse test for values
(cherry picked from commit 14028ed2c8)
2025-11-26 15:03:28 +11:00
33af658e93 Add homepage
(cherry picked from commit 23b6402db6)
2025-11-26 15:00:54 +11:00
75e2bde5fb Rename test modules for previously included files. 2025-11-17 21:31:15 +11:00
14028ed2c8 Add simple argparse test for values 2025-11-17 21:29:15 +11:00
23b6402db6 Add homepage 2025-11-17 21:17:32 +11:00
4f1a01f81c Collect parse handler args into a struct with proper names. 2025-11-17 18:11:32 +11:00
a6abeff9f2 Normalise naming between ParseResult and ParseMapResult 2025-11-17 16:35:07 +11:00
87e3e5f4e0 Update README.md 2025-11-16 13:06:15 +11:00
a dinosaur
82ef9cf8d5 Merge pull request #1 from gay-pizza/include-to-mod
Swap include macros to `using mod` with `pub use` & clippy fixes
2025-11-16 12:55:00 +11:00
03e1953aae swap include to using mod with pub use 2025-11-15 17:51:23 -08:00
148a649273 Remove python section from editorconfig 2025-11-15 20:30:14 +11:00
b1a464c79c Fix licence field in Cargo.toml 2025-11-15 19:46:39 +11:00
967422b727 Release 0.2.0 2025-11-15 19:27:26 +11:00
fb3625c0b8 Dual licence under MIT and (more accurately or) Apache 2.0
totally not just cus it's easier for sprout
2025-11-15 19:12:17 +11:00
46c060f0a7 Partially use standard formatter alignment for aligning option help text 2025-11-15 18:52:10 +11:00
cc4b2f28b5 Docs gen doesn't like referencing certain parts of std so just provide more generic examples 2025-11-15 16:58:05 +11:00
8c30e5c526 Add with chain for excluding options from showing up in help 2025-11-15 16:50:30 +11:00
741dfd4d7e Replace non-general shorts-only contractive Usage line in the full standard help writer with the short standard help writer 2025-11-15 16:13:41 +11:00
934f08a4c2 Tweak btreemap_nostd demo callback argument names 2025-11-15 16:00:44 +11:00
ec0f3f0739 Create no_std examples 2025-11-09 14:22:25 +11:00
304e12bd8e Restrict scope of automatic usage hint in standard error writer, so the default behaviour is less spammy 2025-11-06 02:29:06 +11:00
e26f4c933b Add CI test workflow 2025-11-05 21:27:36 +11:00
6158ae31d2 Update README.md 2025-11-05 21:00:11 +11:00
67dc191443 Generalise error & usage writer 2025-11-05 07:48:15 +11:00
9d8960e772 Fix always appending extra newline in writer convenience functions in std-only crate 2025-11-05 07:43:56 +11:00
dc833a24ed Move base BTreeMap API to alloc-only 2025-11-05 07:32:32 +11:00
0098df1252 Tests for Opts 2025-11-05 07:21:34 +11:00
b0072855bc Pull help querying and getting an iterator over options into a public methods 2025-11-05 05:06:59 +11:00
b11c55a1ee Make Opt getters public, write tests for internal & public Opt methods, correct match_name behaviour for edge case 2025-11-05 04:55:52 +11:00
b613cb315f Change homepage to repository 2025-11-04 21:16:20 +11:00
3953ac06c8 I've moved to workspaces+ added homepage & authors 2025-11-04 19:25:33 +11:00
a013e86067 Release 0.1.1 2025-11-04 01:27:41 +11:00
b3091583ed Quick 'n dirty test for const_utf8, also fix count signature 2025-11-03 00:51:28 +11:00
ae7c12ad62 Add example from README.md as basic.rs 2025-11-02 22:55:00 +11:00
274fbbf097 Fix glaring errors in docstrings 2025-11-02 22:22:42 +11:00
6d42221332 Fix incorrect error message format for coerced parsing errors 2025-11-02 22:10:17 +11:00
6b26188990 Legally mandated fancy README.md 2025-11-02 21:53:37 +11:00
35 changed files with 1840 additions and 602 deletions

View File

@@ -10,8 +10,3 @@ tab_width = 4
indent_style = space indent_style = space
indent_size = 2 indent_size = 2
trim_trailing_whitespace = true trim_trailing_whitespace = true
[*.py]
indent_style = tab
indent_size = tab
trim_trailing_whitespace = true

31
.github/workflows/test.yaml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Build & test
on:
push:
paths: [ ".github/workflows/**", "**/*.rs", "**/Cargo.toml", "**/.cargo/config.toml" ]
pull_request:
paths: [ "**/*.rs", "**/Cargo.toml", "**/.cargo/config.toml" ]
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
CARGO_PROFILE: release-debuginfo
jobs:
build:
strategy:
matrix:
os:
- macos-latest
- ubuntu-latest
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose --profile ${{env.CARGO_PROFILE}}
- name: Run tests
run: cargo test --verbose --profile ${{env.CARGO_PROFILE}}

View File

@@ -1,13 +1,16 @@
[package] [workspace]
name = "jaarg" default-members = ["jaarg"]
version = "0.1.0" members = ["jaarg-nostd"]
license = "MIT" resolver = "3"
[workspace.package]
version = "0.2.2"
license = "MIT OR Apache-2.0"
edition = "2021" edition = "2021"
description = "It can parse your arguments you should use it it's called jaarg" description = "It can parse your arguments you should use it it's called jaarg"
repository = "https://github.com/gay-pizza/jaarg"
[features] homepage = "https://gay.pizza/"
default = ["std"] authors = ["a dinosaur", "Gay Pizza Specifications"]
std = []
[profile.release] [profile.release]
lto = "thin" lto = "thin"

201
LICENSE.Apache-2.0 Normal file
View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Gay Pizza Specifications
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

101
README.md
View File

@@ -1 +1,102 @@
# jaarg argument parser library # # jaarg argument parser library #
Dependency-free, const (mostly), no magic macros, `no_std` & no alloc (though nicer with those).
Some say it can parse your arguments.
### Obligatory fancy banners ###
<div>
<a href="https://crates.io/crates/jaarg">
<img src="https://img.shields.io/crates/v/jaarg.svg?logo=rust&style=for-the-badge" alt="Crates version" />
</a>
<a href="#licensing">
<img src="https://img.shields.io/badge/license-MIT%20%7C%20Apache--2.0-green.svg?style=for-the-badge" alt="MIT OR Apache-2.0 License" />
</a>
</div>
### Example usage ###
```rust
// 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")));
```
### Changelog ###
v0.2.2:
* Fixed coerced `ArgumentError` not being rewritten for positional arguments.
* Moved top level includes to `pub use`.
* Hopefully work around licence & read me texts not being included in crate.
v0.2.1:
* Fixed licence field in `Cargo.toml`.
v0.2.0:
* Change licence from `MIT` to `MIT OR Apache-2.0`.
* Moved `Opts::parse_map` into newly introduced `alloc` crate, making it accessible for `no_std` users.
* More generic & flexible help API: removed forced newline, moved error writer to `StandardErrorUsageWriter`,
generalised "Usage" line in standard full writer, enough public constructs to roll a custom help writer.
* Added the ability to exclude options from short usage, full help, or both.
* More tests for validating internal behaviour & enabled CI on GitHub.
* Added new `no_std` examples.
v0.1.1:
* Fixed incorrect error message format for coerced parsing errors.
* Cleaned up docstring formatting.
* Added basic example.
v0.1.0:
* Initial release.
### Roadmap ###
Near future:
* More control over parsing behaviour (getopt style, no special casing shorts for Windows style flags, etc.)
* More practical examples.
Long term:
* Strategy for handling exclusive argument groups.
* Make use of const traits when they land to improve table setup.
### Projects using jaarg (very cool) ###
* [Sprout bootloader](https://github.com/edera-dev/sprout)
* [lbminfo](https://github.com/ScrelliCopter/colourcyclinginthehousetonight/tree/main/lbminfo)
### Licensing ###
jaarg is dual-licensed under either the [MIT](LICENSE.MIT) or [Apache 2.0](LICENSE.Apache-2.0) licences.
Pick whichever works best for your project.

View File

@@ -0,0 +1,12 @@
[target.'cfg(target_os = "macos")']
rustflags = ["-Cpanic=abort", "-C", "link-args=-lSystem"]
[target.'cfg(target_family = "windows")']
rustflags = ["-Cpanic=abort", "-C", "target-feature=+crt-static"]
[target.'cfg(target_os = "linux")']
rustflags = ["-Cpanic=abort", "-C", "link-args=-lc"]
[profile.dev]
panic = "abort"
[profile.release]
panic = "abort"

10
jaarg-nostd/Cargo.toml Normal file
View File

@@ -0,0 +1,10 @@
[package]
publish = false
name = "jaarg-nostd"
description = "nostd examples for jaarg"
edition.workspace = true
[dependencies.jaarg]
path = "../jaarg"
default-features = false
features = ["alloc"]

4
jaarg-nostd/README.md Normal file
View File

@@ -0,0 +1,4 @@
# ⚠ ATTENTION ⚠
Due to cargo limitations, these examples will fail to build & link unless
`cargo build` is ran from this directory. See `.cargo/config.toml` for
requisite build configuration.

View File

@@ -0,0 +1,67 @@
/* basic_nostd - jaarg example program using parse in `no_std`
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
#![no_std]
#![no_main]
extern crate alloc;
use jaarg::{
ErrorUsageWriter, ErrorUsageWriterContext, HelpWriter, HelpWriterContext, Opt, Opts,
ParseControl, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter
};
use jaarg_nostd::{print, println, harness::ExitCode, simplepathbuf::SimplePathBuf};
#[no_mangle]
#[allow(improper_ctypes_definitions)]
extern "C" fn safe_main(args: &[&str]) -> ExitCode {
// Variables for arguments to fill
let mut file = SimplePathBuf::default();
let mut out: Option<SimplePathBuf> = 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 argv
match OPTIONS.parse(
SimplePathBuf::from(*args.first().unwrap()).basename(),
args.iter().skip(1), |ctx| {
match ctx.id {
Arg::Help => {
let ctx = HelpWriterContext { options: &OPTIONS, program_name: ctx.program_name };
print!("{}", StandardFullHelpWriter::<'_, Arg>::new(ctx));
return Ok(ParseControl::Quit);
}
Arg::Number => { number = str::parse(ctx.arg)?; }
Arg::File => { file = ctx.arg.into(); }
Arg::Out => { out = Some(ctx.arg.into()); }
}
Ok(ParseControl::Continue)
}, |program_name, error| {
let ctx = ErrorUsageWriterContext { options: &OPTIONS, program_name, error };
print!("{}", StandardErrorUsageWriter::<'_, Arg>::new(ctx));
}
) {
ParseResult::ContinueSuccess => (),
ParseResult::ExitSuccess => { return ExitCode::SUCCESS; }
ParseResult::ExitFailure => { return ExitCode::FAILURE; }
}
// Print the result variables
println!("{file} -> {out} (number: {number})",
out = out.unwrap_or(file.with_extension("out")));
ExitCode::SUCCESS
}

View File

@@ -0,0 +1,46 @@
/* btreemap_nostd - jaarg example program using BTreeMap in `no_std`
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
#![no_std]
#![no_main]
extern crate alloc;
use jaarg::{
alloc::ParseMapResult, ErrorUsageWriter, ErrorUsageWriterContext, HelpWriter, HelpWriterContext,
Opt, Opts, StandardErrorUsageWriter, StandardFullHelpWriter
};
use jaarg_nostd::{eprint, print, println, harness::ExitCode, simplepathbuf::SimplePathBuf};
#[no_mangle]
#[allow(improper_ctypes_definitions)]
extern "C" fn safe_main(args: &[&str]) -> 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(
SimplePathBuf::from(*args.first().unwrap()).basename(),
args.iter().skip(1),
|program_name| {
let ctx = HelpWriterContext { options: &OPTIONS, program_name };
print!("{}", StandardFullHelpWriter::new(ctx));
},
|program_name, error| {
let ctx = ErrorUsageWriterContext { options: &OPTIONS, program_name, error };
eprint!("{}", StandardErrorUsageWriter::new(ctx));
}
) {
ParseMapResult::Map(map) => map,
ParseMapResult::ExitSuccess => { return ExitCode::SUCCESS; }
ParseMapResult::ExitFailure => { return ExitCode::FAILURE; }
};
println!("{:?}", map);
ExitCode::SUCCESS
}

210
jaarg-nostd/src/harness.rs Normal file
View File

@@ -0,0 +1,210 @@
/* jaarg-nostd - Minimal harness to run examples in no_std on desktop
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
//!! [Okay... ready for the pain?](https://media.tenor.com/cJRcMyUAiMcAAAAC/tenor.gif)
use core::alloc::{GlobalAlloc, Layout};
use core::fmt::Write;
#[allow(unused_imports)]
use core::panic::PanicInfo;
/// Unix file descriptor
pub struct FileDescriptor(core::ffi::c_int);
#[allow(unused)]
impl FileDescriptor {
/// Standard input file descriptor
const STDIN: Self = Self(0);
/// Standard output file descriptor
const STDOUT: Self = Self(1);
/// Standard error file descriptor
const STDERR: Self = Self(2);
}
pub struct StandardOutWriter;
impl Write for StandardOutWriter {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
unsafe {
c::write(FileDescriptor::STDOUT.0, s.as_ptr() as *const core::ffi::c_void, s.len());
}
Ok(())
}
}
pub fn print(args: core::fmt::Arguments) {
StandardOutWriter{}.write_fmt(args).unwrap();
}
pub struct StandardErrorWriter;
impl Write for StandardErrorWriter {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
unsafe {
c::write(FileDescriptor::STDERR.0, s.as_ptr() as *const core::ffi::c_void, s.len());
}
Ok(())
}
}
pub fn eprint(args: core::fmt::Arguments) {
StandardErrorWriter{}.write_fmt(args).unwrap();
}
#[macro_export]
macro_rules! print {
($($arg:tt)*) => {{ $crate::harness::print(format_args!($($arg)*)); }};
}
#[macro_export]
macro_rules! eprint {
($($arg:tt)*) => {{ $crate::harness::eprint(format_args!($($arg)*)); }};
}
#[macro_export]
macro_rules! println {
() => {{ $crate::print!("\n"); }};
($($arg:tt)*) => {{ $crate::print!("{}\n", format_args!($($arg)*)); }};
}
#[macro_export]
macro_rules! eprintln {
() => {{ $crate::eprint!("\n"); }};
($($arg:tt)*) => {{ $crate::eprint!("{}\n", format_args!($($arg)*)); }};
}
/// Calls system abort
pub fn exit(status: i32) -> ! {
unsafe { c::exit(status as core::ffi::c_int) }
}
/// Bare minimum malloc-based global allocator
#[derive(Default)]
pub struct MallocAlloc;
impl MallocAlloc {
// Fundamental alignment table:
// | Target | 32-bit | 64-bit | Note |
// |---------|--------|--------|-----------------------------|
// | macOS | 16 | 16 | Always 16-byte aligned |
// | GNU | 8 | 16 | |
// | Windows | 8 | 16 | nonstd aligned_alloc & free |
// | OpenBSD | 16 | 16 | FIXME: Unsourced |
// [Darwin source](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/ManagingMemory/Articles/MemoryAlloc.html)
// [GNU glibc source](https://sourceware.org/glibc/manual/2.42/html_node/Malloc-Examples.html)
// [Windows source](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/malloc?view=msvc-170)
// if ptr == 32 && !(os == "macos" || os == "openbsd")
#[cfg(all(target_pointer_width = "32", not(any(target_os = "macos", target_os = "openbsd"))))]
const ALIGNMENT: Option<usize> = Some(8);
// if ptr == 64 || (ptr == 32 && (os == "macos" || os == "openbsd"))
#[cfg(any(target_pointer_width = "64", all(target_pointer_width = "32", any(target_os = "macos", target_os = "openbsd"))))]
const ALIGNMENT: Option<usize> = Some(16);
// if !(ptr == 32 || ptr == 64)
#[cfg(not(any(target_pointer_width = "32", target_pointer_width = "64")))]
const ALIGNMENT: Option<usize> = None;
/// If target alignment % requested_align == 0 then malloc is good enough.
#[inline(always)]
const fn layout_can_use_malloc(requested_layout: &Layout) -> bool {
let align = requested_layout.align();
align != 0 && matches!(Self::ALIGNMENT,
Some(sys_align) if (sys_align & (align - 1)) == 0)
}
}
unsafe impl GlobalAlloc for MallocAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
if Self::layout_can_use_malloc(&layout) {
c::malloc(layout.size()).cast::<u8>()
} else {
#[cfg(target_family = "windows")]
return c::aligned_alloc(layout.size(), layout.align()).cast::<u8>();
#[cfg(not(target_family = "windows"))]
return c::aligned_alloc(layout.align(), layout.size()).cast::<u8>();
}
}
#[allow(unused_variables)]
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
#[cfg(target_family = "windows")]
if !Self::layout_can_use_malloc(&layout) {
c::aligned_free(ptr as *mut core::ffi::c_void);
return;
}
c::free(ptr as *mut core::ffi::c_void);
}
}
#[global_allocator]
static GLOBAL_ALLOCATOR: MallocAlloc = MallocAlloc;
/// Hurt me plenty (intellidumb will think this a lang duplicate pls ignore)
#[cfg(not(test))]
#[panic_handler]
unsafe fn panic(info: &PanicInfo) -> ! {
eprintln!("panic abort: {}", info.message());
c::abort()
}
/// Ultra-Violence
#[cfg(not(test))]
#[no_mangle]
pub extern "C" fn rust_eh_personality() {}
/// Nightmare!
#[cfg(not(test))]
#[allow(non_snake_case)]
#[no_mangle]
extern "C" fn _Unwind_Resume() {}
extern "C" {
#[allow(improper_ctypes)]
pub fn safe_main(args: &[&str]) -> ExitCode;
}
/// Exit code to be passed to entry point wrapper.
#[allow(non_camel_case_types)]
#[repr(i32)]
pub enum ExitCode {
SUCCESS = 0,
FAILURE = 1,
}
/// C main entry point, collects argc/argv and calls `safe_main`.
#[cfg(not(test))]
#[no_mangle]
pub unsafe extern "C" fn main(argc: core::ffi::c_int, argv: *const *const core::ffi::c_char) -> core::ffi::c_int {
let mut args = alloc::vec::Vec::<&str>::with_capacity(argc as usize);
for i in 0..argc as usize {
args.push(core::ffi::CStr::from_ptr(*argv.wrapping_add(i)).to_str().unwrap());
}
safe_main(&args) as core::ffi::c_int
}
mod c {
use core::ffi::{c_int, c_void};
/// Until size_t is stabilised
#[allow(non_camel_case_types)]
type c_size_t = usize;
#[allow(dead_code)]
extern "C" {
pub(crate) fn atexit(function: extern "C" fn()) -> c_int;
pub(crate) fn abort() -> !;
pub(crate) fn exit(status: c_int) -> !;
#[cfg(not(target_family = "windows"))]
pub(crate) fn write(fd: c_int, buf: *const c_void, bytes: c_size_t) -> c_int;
#[cfg(target_family = "windows")]
#[link_name = "_write"]
pub(crate) fn write(fd: c_int, buf: *const c_void, bytes: c_size_t) -> c_int;
pub(crate) fn malloc(size: c_size_t) -> *mut c_void;
pub(crate) fn calloc(count: c_size_t, size: c_size_t) -> *mut c_void;
#[cfg(not(target_family = "windows"))]
pub(crate) fn aligned_alloc(alignment: c_size_t, size: c_size_t) -> *mut c_void;
#[cfg(target_family = "windows")]
#[link_name = "_aligned_malloc"]
pub(crate) fn aligned_alloc(size: c_size_t, alignment: c_size_t) -> *mut c_void;
#[cfg(target_family = "windows")]
#[link_name = "_aligned_free"]
pub(crate) fn aligned_free(memblock: *mut c_void);
pub(crate) fn free(ptr: *mut c_void);
}
}

6
jaarg-nostd/src/lib.rs Normal file
View File

@@ -0,0 +1,6 @@
#![no_std]
extern crate alloc;
pub mod harness;
pub mod simplepathbuf;

View File

@@ -0,0 +1,81 @@
/* jaarg-nostd - Minimal harness to run examples in no_std on desktop
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
use alloc::format;
use alloc::string::String;
use core::fmt::{Display, Formatter};
/// Dirty and simple path buffer that's good enough for the `no_std` examples, not for production use.
#[derive(Default, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct SimplePathBuf(String);
impl<S: AsRef<str>> From<S> for SimplePathBuf where String: From<S> {
fn from(value: S) -> Self {
Self(value.into())
}
}
impl Display for SimplePathBuf {
fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
impl SimplePathBuf {
#[inline(always)]
fn path_predicate(c: char) -> bool {
#[cfg(target_family = "windows")]
if c == '\\' { return true; }
c == '/'
}
pub fn with_extension(&self, ext: &str) -> Self {
let dir_sep = self.0.rfind(Self::path_predicate)
.map_or(0, |n| n + 1);
let without_ext: &str = self.0[dir_sep..].rfind('.')
.map_or(&self.0, |ext_sep_rel| {
if ext_sep_rel == 0 { return &self.0; }
let ext_sep = dir_sep + ext_sep_rel;
&self.0[..ext_sep]
});
Self(format!("{without_ext}.{ext}"))
}
pub fn basename(&self) -> &str {
self.0.trim_end_matches(|c| Self::path_predicate(c) || c == '.')
.rsplit_once(Self::path_predicate)
.map_or(&self.0, |(_, base)| base)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_with_extension() {
assert_eq!(
SimplePathBuf::from("name.ext").with_extension("new"),
SimplePathBuf::from("name.new"));
assert_eq!(
SimplePathBuf::from("/path/name.ext").with_extension("new"),
SimplePathBuf::from("/path/name.new"));
assert_eq!(
SimplePathBuf::from("/path.ext/name").with_extension("new"),
SimplePathBuf::from("/path.ext/name.new"));
assert_eq!(
SimplePathBuf::from("/path.ext/.name").with_extension("new"),
SimplePathBuf::from("/path.ext/.name.new"));
}
#[test]
fn test_basename() {
assert_eq!(SimplePathBuf::from("name.ext").basename(), "name.ext");
assert_eq!(SimplePathBuf::from("/path/name.ext").basename(), "name.ext");
assert_eq!(SimplePathBuf::from("/path/name/").basename(), "name");
assert_eq!(SimplePathBuf::from("/path/name/.").basename(), "name");
assert_eq!(SimplePathBuf::from("/path/name/.//").basename(), "name");
}
}

14
jaarg/Cargo.toml Normal file
View File

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

1
jaarg/LICENSE.Apache-2.0 Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE.Apache-2.0

1
jaarg/LICENSE.MIT Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE.MIT

1
jaarg/README.md Symbolic link
View File

@@ -0,0 +1 @@
../README.md

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

@@ -0,0 +1,49 @@
/* basic - jaarg example program using parse_easy
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
use jaarg::{Opt, OptHide, 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"]).hide_usage(OptHide::Short)
.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(|ctx| {
match ctx.id {
Arg::Help => {
OPTIONS.print_full_help(ctx.program_name);
return Ok(ParseControl::Quit);
}
Arg::Number => { number = str::parse(ctx.arg)?; }
Arg::File => { file = ctx.arg.into(); }
Arg::Out => { out = Some(ctx.arg.into()); }
}
Ok(ParseControl::Continue)
}) {
ParseResult::ContinueSuccess => (),
ParseResult::ExitSuccess => std::process::exit(0),
ParseResult::ExitFailure => std::process::exit(1),
}
// Print the result variables
println!("{file:?} -> {out:?} (number: {number:?})",
out = out.unwrap_or(file.with_extension("out")));
}

View File

@@ -1,6 +1,6 @@
/* bin2c - jaarg example application /* bin2c - jaarg example application
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications * SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT OR Apache-2.0
*/ */
use jaarg::{Opt, Opts, ParseControl, ParseResult}; use jaarg::{Opt, Opts, ParseControl, ParseResult};
@@ -210,14 +210,14 @@ pub fn main() -> ExitCode {
Opt::value(Arg::Whitespace, &["--whitespace"], "\" \"").help_text("Emitted indentation (Default: \"\\t\")"), 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\ ]).with_description("Convert one or more binary and text file(s) to a C header file,\n\
as arrays and C strings respectively."); as arrays and C strings respectively.");
match OPTIONS.parse_easy(|program_name, id, _opt, _name, arg| { match OPTIONS.parse_easy(|ctx| {
match id { match ctx.id {
Arg::Out => { arguments.out = arg.into(); } Arg::Out => { arguments.out = ctx.arg.into(); }
Arg::Bin => { jobs.push(Job { job_type: JobType::Binary, path: arg.into() }); } Arg::Bin => { jobs.push(Job { job_type: JobType::Binary, path: ctx.arg.into() }); }
Arg::Txt => { jobs.push(Job { job_type: JobType::Text, path: arg.into() }); } Arg::Txt => { jobs.push(Job { job_type: JobType::Text, path: ctx.arg.into() }); }
Arg::Whitespace => { arguments.whitespace = arg.into(); } Arg::Whitespace => { arguments.whitespace = ctx.arg.into(); }
Arg::Help => { Arg::Help => {
OPTIONS.print_full_help(program_name); OPTIONS.print_full_help(ctx.program_name);
return Ok(ParseControl::Quit); return Ok(ParseControl::Quit);
} }
} }
@@ -234,6 +234,6 @@ pub fn main() -> ExitCode {
} }
}, },
ParseResult::ExitSuccess => { ExitCode::SUCCESS } ParseResult::ExitSuccess => { ExitCode::SUCCESS }
ParseResult::ExitError => { ExitCode::FAILURE } ParseResult::ExitFailure => { ExitCode::FAILURE }
} }
} }

View File

@@ -1,9 +1,9 @@
/* btreemap - jaarg example program using BTreeMap /* btreemap - jaarg example program using BTreeMap
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications * SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT OR Apache-2.0
*/ */
use jaarg::{std::ParseMapResult, Opt, Opts}; use jaarg::{alloc::ParseMapResult, Opt, Opts};
use std::process::ExitCode; use std::process::ExitCode;
fn main() -> ExitCode { fn main() -> ExitCode {
@@ -16,7 +16,8 @@ fn main() -> ExitCode {
let map = match OPTIONS.parse_map_easy() { let map = match OPTIONS.parse_map_easy() {
ParseMapResult::Map(map) => map, ParseMapResult::Map(map) => map,
ParseMapResult::Exit(code) => { return code; } ParseMapResult::ExitSuccess => { return ExitCode::SUCCESS; }
ParseMapResult::ExitFailure => { return ExitCode::FAILURE; }
}; };
println!("{:?}", map); println!("{:?}", map);

40
jaarg/src/alloc.rs Normal file
View File

@@ -0,0 +1,40 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::string::String;
use crate::{Opts, ParseControl, ParseError, ParseResult};
impl Opts<&'static str> {
/// Parse an iterator of strings as arguments and return the results in a [`BTreeMap`].
///
/// Requires `features = ["alloc"]`.
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, |ctx| {
if ctx.option.is_help() {
help(program_name);
Ok(ParseControl::Quit)
} else {
out.insert(ctx.id, ctx.arg.into());
Ok(ParseControl::Continue)
}
}, error) {
ParseResult::ContinueSuccess => ParseMapResult::Map(out),
ParseResult::ExitSuccess => ParseMapResult::ExitSuccess,
ParseResult::ExitFailure => ParseMapResult::ExitFailure,
}
}
}
/// The result of parsing commands with [Opts::parse_map].
pub enum ParseMapResult {
Map(BTreeMap<&'static str, String>),
ExitSuccess, ExitFailure
}

View File

@@ -1,20 +1,24 @@
/* jaarg - Argument parser /* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications * SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT OR Apache-2.0
*/ */
use crate::{Opt, Opts};
use crate::option::OptType;
use crate::options::RequiredParamsBitSet;
/// Enum describing the result of parsing arguments, and how the program should behave. /// Enum describing the result of parsing arguments, and how the program should behave.
#[derive(Debug)] #[derive(Debug)]
pub enum ParseResult { pub enum ParseResult {
/// Parsing succeeded and program execution should continue /// Parsing succeeded and program execution should continue.
ContinueSuccess, ContinueSuccess,
/// Parsing succeeded and program should exit with success (eg; std::process::ExitCode::SUCCESS) /// Parsing succeeded and program should exit with success (eg; `exit(0)`).
ExitSuccess, ExitSuccess,
/// There was an error while parsing and program should exit with failure (eg; std::process::ExitCode::FAILURE) /// There was an error while parsing and program should exit with failure (eg; `exit(1)`).
ExitError, ExitFailure,
} }
/// Execution control for the parser handler /// Execution control for parser handlers.
pub enum ParseControl { pub enum ParseControl {
/// Continue parsing arguments /// Continue parsing arguments
Continue, Continue,
@@ -24,8 +28,23 @@ pub enum ParseControl {
Quit, Quit,
} }
/// Result type used by the handler passed to the parser #[derive(Debug)]
type HandlerResult<'a, T> = core::result::Result<T, ParseError<'a>>; pub struct ParseHandlerContext<'a, ID: 'static> {
/// Name of the program, for printing statuses to the user.
pub program_name: &'a str,
/// The generic argument ID that was matched.
pub id: &'a ID,
/// The option that was matched by the parser.
pub option: &'a Opt<ID>,
/// The name of the argument parameter that was matched,
/// for option parameters this is the token supplied by the user.
pub name: &'a str,
/// The argument provided to positional arguments and value options, else "".
pub arg: &'a str,
}
/// Result type used by the handler passed to the parser.
pub(crate) type HandlerResult<'a, T> = core::result::Result<T, ParseError<'a>>;
#[derive(Debug)] #[derive(Debug)]
pub enum ParseError<'a> { pub enum ParseError<'a> {
@@ -69,7 +88,7 @@ impl core::fmt::Display for ParseError<'_> {
} }
} }
/// Convenience coercion for dealing with integer parsing errors /// Convenience coercion for dealing with integer parsing errors.
impl From<core::num::ParseIntError> for ParseError<'_> { impl From<core::num::ParseIntError> for ParseError<'_> {
fn from(err: core::num::ParseIntError) -> Self { fn from(err: core::num::ParseIntError) -> Self {
use core::num::IntErrorKind; use core::num::IntErrorKind;
@@ -83,7 +102,7 @@ impl From<core::num::ParseIntError> for ParseError<'_> {
} }
} }
/// Convenience coercion for dealing with floating-point parsing errors /// Convenience coercion for dealing with floating-point parsing errors.
impl From<core::num::ParseFloatError> for ParseError<'_> { impl From<core::num::ParseFloatError> for ParseError<'_> {
fn from(_err: core::num::ParseFloatError) -> Self { fn from(_err: core::num::ParseFloatError) -> Self {
// HACK: The empty option & argument fields will be fixed up by the parser // HACK: The empty option & argument fields will be fixed up by the parser
@@ -94,7 +113,7 @@ impl From<core::num::ParseFloatError> for ParseError<'_> {
impl core::error::Error for ParseError<'_> {} impl core::error::Error for ParseError<'_> {}
/// Internal state tracked by the parser /// Internal state tracked by the parser.
struct ParserState<ID: 'static> { struct ParserState<ID: 'static> {
positional_index: usize, positional_index: usize,
expects_arg: Option<(&'static str, &'static Opt<ID>)>, expects_arg: Option<(&'static str, &'static Opt<ID>)>,
@@ -112,9 +131,9 @@ impl<ID> Default for ParserState<ID> {
} }
impl<ID: 'static> Opts<ID> { impl<ID: 'static> Opts<ID> {
/// Parse an iterator of strings as arguments /// 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, 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>, mut handler: impl FnMut(ParseHandlerContext<ID>) -> HandlerResult<'a, ParseControl>,
error: impl FnOnce(&str, ParseError), error: impl FnOnce(&str, ParseError),
) -> ParseResult { ) -> ParseResult {
let mut state = ParserState::default(); let mut state = ParserState::default();
@@ -127,7 +146,7 @@ impl<ID: 'static> Opts<ID> {
Err(err) => { Err(err) => {
// Call the error handler // Call the error handler
error(program_name, err); error(program_name, err);
return ParseResult::ExitError; return ParseResult::ExitFailure;
} }
} }
} }
@@ -135,21 +154,21 @@ impl<ID: 'static> Opts<ID> {
// Ensure that value options are provided a value // Ensure that value options are provided a value
if let Some((name, _)) = state.expects_arg.take() { if let Some((name, _)) = state.expects_arg.take() {
error(program_name, ParseError::ExpectArgument(name)); error(program_name, ParseError::ExpectArgument(name));
return ParseResult::ExitError; return ParseResult::ExitFailure;
} }
// Ensure that all required arguments have been provided // Ensure that all required arguments have been provided
let mut required_flag_idx = 0; let mut required_flag_idx = 0;
for (i, option) in self.options.iter().enumerate() { for (i, option) in self.iter().enumerate() {
match option.r#type { match option.r#type {
OptType::Positional => if i >= state.positional_index && option.is_required() { OptType::Positional => if i >= state.positional_index && option.is_required() {
error(program_name, ParseError::RequiredPositional(option.first_name())); error(program_name, ParseError::RequiredPositional(option.first_name()));
return ParseResult::ExitError; return ParseResult::ExitFailure;
} }
OptType::Flag | OptType::Value => if option.is_required() { OptType::Flag | OptType::Value => if option.is_required() {
if !state.required_param_presences.get(required_flag_idx) { if !state.required_param_presences.get(required_flag_idx) {
error(program_name, ParseError::RequiredParameter(option.first_name())); error(program_name, ParseError::RequiredParameter(option.first_name()));
return ParseResult::ExitError; return ParseResult::ExitFailure;
} }
required_flag_idx += 1; required_flag_idx += 1;
} }
@@ -162,14 +181,14 @@ impl<ID: 'static> Opts<ID> {
/// Parse the next token in the argument stream /// Parse the next token in the argument stream
fn next<'a, 'b>(&self, state: &mut ParserState<ID>, token: &'b str, program_name: &str, 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> handler: &mut impl FnMut(ParseHandlerContext<ID>) -> HandlerResult<'a, ParseControl>
) -> HandlerResult<'b, ParseControl> where 'a: 'b { ) -> HandlerResult<'b, ParseControl> where 'a: 'b {
let mut call_handler = |option: &Opt<ID>, name, value| { let mut call_handler = |option: &Opt<ID>, name, value| {
match handler(program_name, &option.id, option, name, value) { match handler(ParseHandlerContext{ program_name, id: &option.id, option, name, arg: value }) {
// HACK: Ensure the string fields are set properly, because coerced // HACK: Ensure the string fields are set properly, because coerced
// ParseIntError/ParseFloatError will have the string fields blanked. // ParseIntError/ParseFloatError will have the string fields blanked.
Err(ParseError::ArgumentError("", "", kind)) Err(ParseError::ArgumentError("", "", kind))
=> Err(ParseError::ArgumentError(name, token, kind)), => Err(ParseError::ArgumentError(name, value, kind)),
Err(err) => Err(err), Err(err) => Err(err),
Ok(ctl) => Ok(ctl), Ok(ctl) => Ok(ctl),
} }
@@ -192,7 +211,7 @@ impl<ID: 'static> Opts<ID> {
let mut required_idx = 0; let mut required_idx = 0;
// Match a suitable option by name (ignoring the first flag character & skipping positional arguments) // Match a suitable option by name (ignoring the first flag character & skipping positional arguments)
let (name, option) = self.options.iter() let (name, option) = self.iter()
.filter(|opt| matches!(opt.r#type, OptType::Flag | OptType::Value)).find_map(|opt| { .filter(|opt| matches!(opt.r#type, OptType::Flag | OptType::Value)).find_map(|opt| {
if let Some(name) = opt.match_name(option_str, 1) { if let Some(name) = opt.match_name(option_str, 1) {
Some((name, opt)) Some((name, opt))
@@ -228,7 +247,7 @@ impl<ID: 'static> Opts<ID> {
// Find the next positional argument // Find the next positional argument
for (i, option) in self.options[state.positional_index..].iter().enumerate() { for (i, option) in self.options[state.positional_index..].iter().enumerate() {
if matches!(option.r#type, OptType::Positional) { if matches!(option.r#type, OptType::Positional) {
handler(program_name, &option.id, option, option.first_name(), token)?; call_handler(option, option.first_name(), token)?;
state.positional_index += i + 1; state.positional_index += i + 1;
return Ok(ParseControl::Continue); return Ok(ParseControl::Continue);
} }
@@ -238,3 +257,48 @@ impl<ID: 'static> Opts<ID> {
} }
} }
} }
#[cfg(test)]
mod tests {
extern crate alloc;
use alloc::string::String;
use super::*;
#[test]
fn test() {
enum ArgID { One, Two, Three, Four, Five }
const OPTIONS: Opts<ArgID> = Opts::new(&[
Opt::positional(ArgID::One, "one"),
Opt::flag(ArgID::Two, &["--two"]),
Opt::value(ArgID::Three, &["--three"], "value"),
Opt::value(ArgID::Four, &["--four"], "value"),
Opt::value(ArgID::Five, &["--five"], "value"),
]);
const ARGUMENTS: &[&str] = &["one", "--two", "--three=three", "--five=", "--four", "four"];
//TODO: currently needs alloc to deal with arguments not being able to escape handler
let mut one: Option<String> = None;
let mut two = false;
let mut three: Option<String> = None;
let mut four: Option<String> = None;
let mut five: Option<String> = None;
assert!(matches!(OPTIONS.parse("", ARGUMENTS.iter(), |ctx| {
match ctx.id {
ArgID::One => { one = Some(ctx.arg.into()); }
ArgID::Two => { two = true; }
ArgID::Three => { three = Some(ctx.arg.into()); }
ArgID::Four => { four = Some(ctx.arg.into()); }
ArgID::Five => { five = Some(ctx.arg.into()); }
}
Ok(ParseControl::Continue)
}, |_, error| {
panic!("unreachable: {error:?}");
}), ParseResult::ContinueSuccess));
assert_eq!(one, Some("one".into()));
assert!(two);
assert_eq!(three, Some("three".into()));
assert_eq!(four, Some("four".into()));
assert_eq!(five, Some("".into()));
}
}

View File

@@ -1,6 +1,6 @@
/* jaarg - Argument parser /* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications * SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT OR Apache-2.0
*/ */
/// Fully const fn nostd UTF-8 character iterator. /// Fully const fn nostd UTF-8 character iterator.
@@ -24,7 +24,7 @@ impl<'a> CharIterator<'a> {
impl CharIterator<'_> { impl CharIterator<'_> {
/// Gets a count of the number of Unicode characters (not graphemes) in the string. /// Gets a count of the number of Unicode characters (not graphemes) in the string.
pub(crate) const fn count(self) -> usize { pub(crate) const fn count(&self) -> usize {
let len = self.bytes.len(); let len = self.bytes.len();
let mut count = 0; let mut count = 0;
let mut i = 0; let mut i = 0;
@@ -123,3 +123,19 @@ impl CharIterator<'_> {
Some(result) 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);
}
}
}

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

@@ -0,0 +1,226 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
use crate::{Opt, Opts, ParseError};
use crate::option::{OptIdentifier, OptType};
/// Enough context to show full help text.
pub struct HelpWriterContext<'a, ID: 'static> {
pub options: &'a Opts<ID>,
pub program_name: &'a str,
}
impl<ID: 'static> Clone for HelpWriterContext<'_, ID> {
fn clone(&self) -> Self {
Self { options: self.options, program_name: self.program_name }
}
}
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.iter()
.filter(|o| matches!((o.r#type, o.is_short_visible()), (OptType::Value | OptType::Flag, true))) {
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.iter()
.filter(|o| matches!((o.r#type, o.is_short_visible()), (OptType::Positional, true))) {
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 {
// Base short usage
writeln!(f, "{}", StandardShortUsageWriter::new(self.0.clone()))?;
if let Some(description) = self.0.options.description {
writeln!(f)?;
writeln!(f, "{description}")?;
}
// Determine the alignment width from the longest option parameter
fn calculate_option_line_length<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)
}
let align_width = 3 + self.0.options.iter()
.map(|o| calculate_option_line_length(o)).max().unwrap_or(0);
// Write positional argument descriptions
let mut first = true;
for option in self.0.options.iter()
.filter(|o| matches!((o.r#type, o.is_full_visible()), (OptType::Positional, true))) {
if first {
// Write separator and positional section header
writeln!(f)?;
writeln!(f, "Positional arguments:")?;
first = false;
}
// Write positional argument line (name + optional aligned help text)
let name = option.first_name();
write!(f, " {name}")?;
if let Some(help_text) = option.help_string {
write!(f, " {:.<width$} {help_text}", "",
width = align_width - name.chars().count() - 1)?;
}
writeln!(f)?;
}
/// Formatter for option usage lines.
struct OptionUsageLine<'a, ID>(&'a Opt<ID>);
impl<ID> core::fmt::Display for OptionUsageLine<'_, ID> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
use core::fmt::Write;
let mut length = 0;
// Write option flag name(s)
match self.0.names {
OptIdentifier::Single(name) => {
write!(f, "{name}")?;
length = name.chars().count();
}
OptIdentifier::Multi(names) => {
for (i, name) in names.iter().enumerate() {
if i == 0 {
write!(f, "{name}")?;
length += name.chars().count();
} else {
write!(f, " | {name}")?;
length += 3 + name.chars().count();
}
}
}
}
// Write value argument for value options parameters
if let Some(value_name) = self.0.value_name {
write!(f, " <{value_name}>")?;
length += 2 + value_name.chars().count() + 1;
}
// Write padding if requested
match (f.align(), f.width().unwrap_or(0).checked_sub(length)) {
(Some(core::fmt::Alignment::Left), Some(width)) if width > 0 => {
let fill = f.fill();
// First padding char is *always* a space
f.write_char(' ')?;
for _ in 1..width {
f.write_char(fill)?;
}
Ok(())
}
_ => Ok(()),
}
}
}
// Write option parameter argument descriptions
first = true;
for option in self.0.options.iter()
.filter(|o| matches!((o.r#type, o.is_full_visible()), (OptType::Flag | OptType::Value, true))) {
if first {
// Write separator and options section header
writeln!(f)?;
writeln!(f, "Options:")?;
first = false;
}
// Write line for option, with aligned help text if needed
let line = OptionUsageLine(option);
if let Some(help_text) = option.help_string {
write!(f, " {line:.<align_width$} {help_text}")?;
} else {
write!(f, " {line}")?;
}
writeln!(f)?;
}
Ok(())
}
}
// Enough context to show usage and error information.
pub struct ErrorUsageWriterContext<'a, ID: 'static> {
pub options: &'a Opts<ID>,
pub program_name: &'a str,
pub error: ParseError<'a>
}
pub trait ErrorUsageWriter<'a, ID: 'static>: core::fmt::Display {
fn new(ctx: ErrorUsageWriterContext<'a, ID>) -> Self;
}
pub struct StandardErrorUsageWriter<'a, ID: 'static>(ErrorUsageWriterContext<'a, ID>);
impl<'a, ID: 'static> ErrorUsageWriter<'a, ID> for StandardErrorUsageWriter<'a, ID> {
fn new(ctx: ErrorUsageWriterContext<'a, ID>) -> Self { Self(ctx) }
}
impl<ID> core::fmt::Display for StandardErrorUsageWriter<'_, ID> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
// Write error
writeln!(f, "{name}: {error}", name=self.0.program_name, error = self.0.error)?;
// Provide usage hint for missing required arguments
if matches!(self.0.error, ParseError::RequiredPositional(_) | ParseError::RequiredParameter(_)) {
// Write short usage
writeln!(f, "{}", StandardShortUsageWriter::new(HelpWriterContext {
options: self.0.options,
program_name: self.0.program_name
}))?;
// Write full help instruction if available
if let Some(help_option) = self.0.options.help_option() {
writeln!(f, "Run '{name} {help}' to view all available options.",
name = self.0.program_name,
// Prefer long name, but otherwise any name is fine
help = help_option.first_long_name().unwrap_or(help_option.first_name()))?;
}
}
Ok(())
}
}

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

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

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

@@ -0,0 +1,381 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
use crate::const_utf8;
#[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) enum OptType {
Positional,
Flag,
Value,
}
#[derive(Debug, PartialEq)]
pub(crate) enum OptIdentifier {
Single(&'static str),
Multi(&'static[&'static str]),
}
/// Represents an option argument or positional argument to be parsed.
#[derive(Debug, PartialEq)]
pub struct Opt<ID> {
pub(crate) id: ID,
pub(crate) names: OptIdentifier,
pub(crate) value_name: Option<&'static str>,
pub(crate) help_string: Option<&'static str>,
pub(crate) r#type: OptType,
flags: OptFlag,
}
pub enum OptHide {
Short,
Full,
All,
}
#[derive(Debug, PartialEq)]
struct OptFlag(u8);
impl OptFlag {
#[allow(dead_code)]
pub const NONE: Self = Self(0);
pub const REQUIRED: Self = OptFlag(1 << 0);
pub const HELP: Self = OptFlag(1 << 1);
pub const VISIBLE_SHORT: Self = OptFlag(1 << 2);
pub const VISIBLE_FULL: Self = OptFlag(1 << 3);
pub const DEFAULT: Self = Self(Self::VISIBLE_SHORT.0 | Self::VISIBLE_FULL.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::DEFAULT }
}
/// 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
}
/// Marks the option to exclude it from appearing in short usage text, full help text, or both.
#[inline]
pub const fn hide_usage(mut self, from: OptHide) -> Self {
self.flags.0 &= !match from {
OptHide::Short => OptFlag::VISIBLE_SHORT.0,
OptHide::Full => OptFlag::VISIBLE_FULL.0,
OptHide::All => OptFlag::VISIBLE_SHORT.0 | OptFlag::VISIBLE_FULL.0,
};
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
}
/// Returns true if this is a required positional argument, or required option argument.
#[inline(always)]
pub const fn is_required(&self) -> bool {
(self.flags.0 & OptFlag::REQUIRED.0) != 0
}
/// Returns true if this is the help option.
#[inline(always)]
pub const fn is_help(&self) -> bool {
(self.flags.0 & OptFlag::HELP.0) != 0
}
#[inline(always)]
pub(crate) const fn is_short_visible(&self) -> bool {
(self.flags.0 & OptFlag::VISIBLE_SHORT.0) != 0
}
#[inline(always)]
pub(crate) const fn is_full_visible(&self) -> bool {
(self.flags.0 & OptFlag::VISIBLE_FULL.0) != 0
}
}
#[allow(dead_code)]
impl<ID: 'static> Opt<ID> {
/// Get the first name of the option.
pub 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.
pub 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.
pub(crate) 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.
pub(crate) 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.
pub(crate) fn match_name(&self, string: &str, offset: usize) -> Option<&'static str> {
let rhs = &string[offset..];
if rhs.is_empty() {
return None;
}
match self.names {
OptIdentifier::Single(name) =>
if &name[offset..] == rhs { Some(name) } else { None },
OptIdentifier::Multi(names) =>
names.iter().find(|name| &name[offset..] == rhs).map(|v| &**v),
}
}
}
impl core::ops::BitOr for OptFlag {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output { Self(self.0 | rhs.0) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[should_panic(expected = "Option names cannot be an empty slice")]
fn test_new_empty_names_disallowed() {
Opt::new((), OptIdentifier::Multi(&[]), None, OptType::Positional);
}
#[test]
fn test_public_initialisers() {
assert_eq!(Opt::positional((), "name"), Opt { id: (),
names: OptIdentifier::Single("name"), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::DEFAULT,
});
assert_eq!(Opt::help_flag((), &["name"]), Opt { id: (),
names: OptIdentifier::Multi(&["name"]), value_name: None, help_string: None,
r#type: OptType::Flag, flags: OptFlag::DEFAULT | OptFlag::HELP,
});
assert_eq!(Opt::flag((), &["name"]), Opt { id: (),
names: OptIdentifier::Multi(&["name"]), value_name: None, help_string: None,
r#type: OptType::Flag, flags: OptFlag::DEFAULT,
});
assert_eq!(Opt::value((), &["name"], "value"), Opt { id: (),
names: OptIdentifier::Multi(&["name"]), value_name: Some("value"), help_string: None,
r#type: OptType::Value, flags: OptFlag::DEFAULT,
});
}
#[test]
fn test_valid_with_chains() {
assert_eq!(Opt::positional((), "").required(), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::DEFAULT | OptFlag::REQUIRED,
});
assert_eq!(Opt::positional((), "").required().help_text("help string"), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: Some("help string"),
r#type: OptType::Positional, flags: OptFlag::DEFAULT | OptFlag::REQUIRED,
});
assert_eq!(Opt::positional((), "").help_text("help string"), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: Some("help string"),
r#type: OptType::Positional, flags: OptFlag::DEFAULT,
});
assert_eq!(Opt::positional((), "").hide_usage(OptHide::Short), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::VISIBLE_FULL,
});
assert_eq!(Opt::positional((), "").hide_usage(OptHide::Full), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::VISIBLE_SHORT,
});
assert_eq!(Opt::positional((), "").hide_usage(OptHide::All), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::NONE,
});
assert_eq!(Opt::positional((), "").required().hide_usage(OptHide::All), Opt { id: (),
names: OptIdentifier::Single(""), value_name: None, help_string: None,
r#type: OptType::Positional, flags: OptFlag::REQUIRED,
});
}
#[test]
#[should_panic(expected = "Help flag cannot be made required")]
fn test_required_help_disallowed() {
Opt::help_flag((), &["-h", "--help"]).required();
}
#[test]
#[should_panic(expected = "Only flags are allowed to be help options")]
fn test_positional_with_help_flag_disallowed() {
Opt::positional((), "").with_help_flag();
}
#[test]
#[should_panic(expected = "Only flags are allowed to be help options")]
fn test_value_with_help_flag_disallowed() {
Opt::value((), &[""], "").with_help_flag();
}
#[test]
fn test_flag_getters() {
const HELP: Opt<()> = Opt::help_flag((), &[""]);
const REQUIRED: Opt<()> = Opt::positional((), "").required();
assert!(HELP.is_help());
assert!(!HELP.is_required());
assert!(REQUIRED.is_required());
assert!(!REQUIRED.is_help());
}
#[test]
fn test_first_name() {
assert_eq!(Opt::positional((), "first").first_name(), "first");
assert_eq!(Opt::flag((), &["first", "second"]).first_name(), "first");
}
#[test]
fn test_first_long_name() {
assert_eq!(Opt::positional((), "--long").first_long_name(), Some("--long"));
assert_eq!(Opt::positional((), "-long").first_long_name(), Some("-long"));
assert_eq!(Opt::positional((), "--l").first_long_name(), Some("--l"));
assert_eq!(Opt::positional((), "-s").first_long_name(), None);
assert_eq!(Opt::flag((), &["-s", "--long"]).first_long_name(), Some("--long"));
}
#[test]
fn test_first_short_name() {
assert_eq!(Opt::positional((), "-s").first_short_name(), Some("-s"));
assert_eq!(Opt::positional((), "--long").first_short_name(), None);
assert_eq!(Opt::positional((), "--").first_short_name(), None);
assert_eq!(Opt::positional((), "-lo").first_short_name(), None);
assert_eq!(Opt::positional((), "--l").first_short_name(), None);
assert_eq!(Opt::flag((), &["--long", "-s"]).first_short_name(), Some("-s"));
}
#[test]
fn test_first_short_name_char() {
assert_eq!(Opt::positional((), "-s").first_short_name_char(), Some('s'));
assert_eq!(Opt::positional((), "--long").first_short_name_char(), None);
assert_eq!(Opt::positional((), "--").first_short_name_char(), None);
assert_eq!(Opt::positional((), "-lo").first_short_name_char(), None);
assert_eq!(Opt::positional((), "--l").first_short_name_char(), None);
assert_eq!(Opt::flag((), &["--long", "-s"]).first_short_name_char(), Some('s'));
}
#[test]
fn test_match_name() {
assert_eq!(Opt::flag((), &["--one", "--two", "--threee", "--three"])
.match_name("--three", 0), Some("--three"));
assert_eq!(Opt::flag((), &["--one", "--two", "--threee"])
.match_name("--three", 0), None);
assert_eq!(Opt::flag((), &["/one", "/two", "/three", "/four"])
.match_name("-three", 1), Some("/three"));
assert_eq!(Opt::positional((), "-s").match_name("-s", 1), Some("-s"));
assert_eq!(Opt::flag((), &["-x", "-s"]).match_name("-s", 2), None);
assert_eq!(Opt::positional((), "-x").match_name("-s", 2), None);
}
}

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

@@ -0,0 +1,126 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
use crate::{ordered_bitset, Opt};
use crate::option::OptType;
/// Static structure that contains instructions for parsing command-line arguments.
#[derive(Debug, PartialEq)]
pub struct Opts<ID: 'static> {
/// List of options
pub(crate) options: &'static[Opt<ID>],
/// String containing single characters that match option prefixes
pub(crate) flag_chars: &'static str,
/// A description of what the program does
pub(crate) description: Option<&'static str>,
}
pub(crate) 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,
}
}
/// Sets the recognised flag/option characters.
#[inline]
pub const fn with_flag_chars(mut self, flag_chars: &'static str) -> Self {
self.flag_chars = flag_chars;
self
}
/// Sets the description of the program, available to help writers.
#[inline]
pub const fn with_description(mut self, description: &'static str) -> Self {
self.description = Some(description);
self
}
/// Gets the first available help option if one exists.
pub const fn help_option(&self) -> Option<&'static Opt<ID>> {
let mut i = 0;
while i < self.options.len() {
if self.options[i].is_help() {
return Some(&self.options[i]);
}
i += 1;
}
None
}
/// Gets an iterator over the parser's options.
#[inline]
pub fn iter(&self) -> core::slice::Iter<'static, Opt<ID>> {
self.options.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(unused)]
fn test_required_opt_limit() {
const NUM_OPTS: usize = MAX_REQUIRED_OPTIONS + 2;
const OPT_LIST: [Opt<()>; NUM_OPTS] = {
const REQUIRED: Opt<()> = Opt::flag((), &[""]).required();
let mut array: [Opt<()>; NUM_OPTS] = [REQUIRED; NUM_OPTS];
array[0] = Opt::help_flag((), &[""]);
array[NUM_OPTS - 1] = Opt::positional((), "");
array
};
const OPTIONS: Opts<()> = Opts::new(&OPT_LIST);
}
#[test]
fn test_with_chains() {
assert_eq!(Opts::<()>::new(&[]).with_flag_chars("-/"),
Opts { options: &[], flag_chars: "-/", description: None });
assert_eq!(Opts::<()>::new(&[]).with_description("test description"),
Opts { options: &[], flag_chars: "-", description: Some("test description") });
}
#[test]
fn test_help_option() {
const OPTS1: Opts<()> = Opts::new(&[
Opt::flag((), &[""]),
Opt::flag((), &[""]),
Opt::positional((), ""),
Opt::positional((), ""),
Opt::help_flag((), &["--help"]),
Opt::value((), &[""], ""),
Opt::help_flag((), &[""]),
]);
const OPTS2: Opts<()> = Opts::new(&[
Opt::flag((), &[""]),
Opt::positional((), ""),
Opt::value((), &[""], ""),
]);
assert_eq!(OPTS1.help_option(), Some(&Opt::help_flag((), &["--help"])));
assert_eq!(OPTS2.help_option(), None);
}
}

View File

@@ -1,6 +1,6 @@
/* jaarg - Argument parser /* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications * SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT OR Apache-2.0
*/ */
#![allow(private_bounds)] #![allow(private_bounds)]

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

@@ -0,0 +1,78 @@
/* jaarg - Argument parser
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
* SPDX-License-Identifier: MIT OR Apache-2.0
*/
extern crate std;
use crate::{
alloc::ParseMapResult, ErrorUsageWriter, ErrorUsageWriterContext, HandlerResult, HelpWriter, HelpWriterContext,
Opts, ParseControl, ParseError, ParseHandlerContext, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter
};
use std::path::Path;
use std::rc::Rc;
use std::{env, eprint, print};
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(ParseHandlerContext<ID>) -> HandlerResult<'a, ParseControl>
) -> ParseResult {
let (program_name, argv) = Self::easy_args();
self.parse(&program_name, argv, handler,
|name, e| self.eprint_usage::<StandardErrorUsageWriter<'_, ID>>(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 };
print!("{}", 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 };
eprint!("{}", W::new(ctx));
}
/// Print error & usage text to stderr using the provided error & usage writer.
///
/// Requires `features = ["std"]`.
pub fn eprint_usage<'a, W: ErrorUsageWriter<'a, ID>>(&'a self, program_name: &'a str, error: ParseError<'a>) {
let ctx = ErrorUsageWriterContext { options: self, program_name, error };
eprint!("{}", W::new(ctx));
}
fn easy_args() -> (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)
}
}
impl Opts<&'static str> {
/// Parse arguments from the command line and return the results in a [`alloc::collections::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.eprint_usage::<StandardErrorUsageWriter<'_, &'static str>>(name, e))
}
}

View File

@@ -1,186 +0,0 @@
/* 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(())
}
}

View File

@@ -1,17 +0,0 @@
/* 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;

View File

@@ -1,189 +0,0 @@
/* 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 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),
}
}
}

View File

@@ -1,54 +0,0 @@
/* 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
}
}

View File

@@ -1,105 +0,0 @@
/* 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 `jaarg::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 using `jaarg::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))
}
}