mirror of
https://github.com/gay-pizza/jaarg.git
synced 2025-12-19 15:30:16 +00:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b1a464c79c | |||
| 967422b727 | |||
| fb3625c0b8 | |||
| 46c060f0a7 | |||
| cc4b2f28b5 | |||
| 8c30e5c526 | |||
| 741dfd4d7e | |||
| 934f08a4c2 | |||
| ec0f3f0739 | |||
| 304e12bd8e | |||
| e26f4c933b | |||
| 6158ae31d2 | |||
| 67dc191443 | |||
| 9d8960e772 | |||
| dc833a24ed | |||
| 0098df1252 | |||
| b0072855bc | |||
| b11c55a1ee | |||
| b613cb315f | |||
| 3953ac06c8 | |||
| a013e86067 | |||
| b3091583ed | |||
| ae7c12ad62 | |||
| 274fbbf097 | |||
| 6d42221332 | |||
| 6b26188990 |
31
.github/workflows/test.yaml
vendored
Normal file
31
.github/workflows/test.yaml
vendored
Normal 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}}
|
||||
18
Cargo.toml
18
Cargo.toml
@@ -1,13 +1,15 @@
|
||||
[package]
|
||||
name = "jaarg"
|
||||
version = "0.1.0"
|
||||
license = "MIT"
|
||||
[workspace]
|
||||
default-members = ["jaarg"]
|
||||
members = ["jaarg-nostd"]
|
||||
resolver = "3"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.1"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
description = "It can parse your arguments you should use it it's called jaarg"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = []
|
||||
repository = "https://github.com/gay-pizza/jaarg"
|
||||
authors = ["a dinosaur", "Gay Pizza Specifications"]
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
|
||||
201
LICENSE.Apache-2.0
Normal file
201
LICENSE.Apache-2.0
Normal 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.
|
||||
98
README.md
98
README.md
@@ -1 +1,99 @@
|
||||
# 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 ###
|
||||
|
||||
<!-- main: -->
|
||||
|
||||
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) ###
|
||||
<!-- soon... * [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.
|
||||
|
||||
12
jaarg-nostd/.cargo/config.toml
Normal file
12
jaarg-nostd/.cargo/config.toml
Normal 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
10
jaarg-nostd/Cargo.toml
Normal 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
4
jaarg-nostd/README.md
Normal 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.
|
||||
68
jaarg-nostd/examples/basic_nostd.rs
Normal file
68
jaarg-nostd/examples/basic_nostd.rs
Normal file
@@ -0,0 +1,68 @@
|
||||
/* 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),
|
||||
|program_name, id, _opt, _name, arg| {
|
||||
match id {
|
||||
Arg::Help => {
|
||||
let ctx = HelpWriterContext { options: &OPTIONS, program_name };
|
||||
print!("{}", StandardFullHelpWriter::<'_, Arg>::new(ctx));
|
||||
return Ok(ParseControl::Quit);
|
||||
}
|
||||
Arg::Number => { number = str::parse(arg)?; }
|
||||
Arg::File => { file = arg.into(); }
|
||||
Arg::Out => { out = Some(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::ExitError => { return ExitCode::FAILURE; }
|
||||
}
|
||||
|
||||
// Print the result variables
|
||||
println!("{file} -> {out} (number: {number})",
|
||||
out = out.unwrap_or(file.with_extension("out")));
|
||||
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
46
jaarg-nostd/examples/btreemap_nostd.rs
Normal file
46
jaarg-nostd/examples/btreemap_nostd.rs
Normal 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
210
jaarg-nostd/src/harness.rs
Normal 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
6
jaarg-nostd/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
#![no_std]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub mod harness;
|
||||
pub mod simplepathbuf;
|
||||
81
jaarg-nostd/src/simplepathbuf.rs
Normal file
81
jaarg-nostd/src/simplepathbuf.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
13
jaarg/Cargo.toml
Normal file
13
jaarg/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "jaarg"
|
||||
version.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
repository.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
alloc = []
|
||||
std = ["alloc"]
|
||||
49
jaarg/examples/basic.rs
Normal file
49
jaarg/examples/basic.rs
Normal 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(|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")));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/* bin2c - jaarg example application
|
||||
* 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};
|
||||
@@ -1,9 +1,9 @@
|
||||
/* btreemap - jaarg example program using BTreeMap
|
||||
* 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;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
@@ -16,7 +16,8 @@ fn main() -> ExitCode {
|
||||
|
||||
let map = match OPTIONS.parse_map_easy() {
|
||||
ParseMapResult::Map(map) => map,
|
||||
ParseMapResult::Exit(code) => { return code; }
|
||||
ParseMapResult::ExitSuccess => { return ExitCode::SUCCESS; }
|
||||
ParseMapResult::ExitFailure => { return ExitCode::FAILURE; }
|
||||
};
|
||||
|
||||
println!("{:?}", map);
|
||||
40
jaarg/src/alloc.rs
Normal file
40
jaarg/src/alloc.rs
Normal 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, |_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::ExitSuccess,
|
||||
ParseResult::ExitError => ParseMapResult::ExitFailure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The result of parsing commands with [Opts::parse_map].
|
||||
pub enum ParseMapResult {
|
||||
Map(BTreeMap<&'static str, String>),
|
||||
ExitSuccess, ExitFailure
|
||||
}
|
||||
@@ -1,20 +1,20 @@
|
||||
/* jaarg - Argument parser
|
||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||
* SPDX-License-Identifier: MIT
|
||||
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
*/
|
||||
|
||||
/// Enum describing the result of parsing arguments, and how the program should behave.
|
||||
#[derive(Debug)]
|
||||
pub enum ParseResult {
|
||||
/// Parsing succeeded and program execution should continue
|
||||
/// Parsing succeeded and program execution should continue.
|
||||
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,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
/// Execution control for the parser handler
|
||||
/// Execution control for parser handlers.
|
||||
pub enum ParseControl {
|
||||
/// Continue parsing arguments
|
||||
Continue,
|
||||
@@ -24,7 +24,7 @@ pub enum ParseControl {
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// Result type used by the handler passed to the parser
|
||||
/// Result type used by the handler passed to the parser.
|
||||
type HandlerResult<'a, T> = core::result::Result<T, ParseError<'a>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -69,7 +69,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<'_> {
|
||||
fn from(err: core::num::ParseIntError) -> Self {
|
||||
use core::num::IntErrorKind;
|
||||
@@ -83,7 +83,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<'_> {
|
||||
fn from(_err: core::num::ParseFloatError) -> Self {
|
||||
// HACK: The empty option & argument fields will be fixed up by the parser
|
||||
@@ -94,7 +94,7 @@ impl From<core::num::ParseFloatError> for ParseError<'_> {
|
||||
|
||||
impl core::error::Error for ParseError<'_> {}
|
||||
|
||||
/// Internal state tracked by the parser
|
||||
/// Internal state tracked by the parser.
|
||||
struct ParserState<ID: 'static> {
|
||||
positional_index: usize,
|
||||
expects_arg: Option<(&'static str, &'static Opt<ID>)>,
|
||||
@@ -112,7 +112,7 @@ impl<ID> Default for ParserState<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,
|
||||
mut handler: impl FnMut(&str, &ID, &Opt<ID>, &str, &str) -> HandlerResult<'a, ParseControl>,
|
||||
error: impl FnOnce(&str, ParseError),
|
||||
@@ -140,7 +140,7 @@ impl<ID: 'static> Opts<ID> {
|
||||
|
||||
// Ensure that all required arguments have been provided
|
||||
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 {
|
||||
OptType::Positional => if i >= state.positional_index && option.is_required() {
|
||||
error(program_name, ParseError::RequiredPositional(option.first_name()));
|
||||
@@ -169,7 +169,7 @@ impl<ID: 'static> Opts<ID> {
|
||||
// HACK: Ensure the string fields are set properly, because coerced
|
||||
// ParseIntError/ParseFloatError will have the string fields blanked.
|
||||
Err(ParseError::ArgumentError("", "", kind))
|
||||
=> Err(ParseError::ArgumentError(name, token, kind)),
|
||||
=> Err(ParseError::ArgumentError(name, value, kind)),
|
||||
Err(err) => Err(err),
|
||||
Ok(ctl) => Ok(ctl),
|
||||
}
|
||||
@@ -192,7 +192,7 @@ impl<ID: 'static> Opts<ID> {
|
||||
let mut required_idx = 0;
|
||||
|
||||
// Match a suitable option by name (ignoring the first flag character & skipping positional arguments)
|
||||
let (name, option) = self.options.iter()
|
||||
let (name, option) = self.iter()
|
||||
.filter(|opt| matches!(opt.r#type, OptType::Flag | OptType::Value)).find_map(|opt| {
|
||||
if let Some(name) = opt.match_name(option_str, 1) {
|
||||
Some((name, opt))
|
||||
@@ -1,6 +1,6 @@
|
||||
/* jaarg - Argument parser
|
||||
* 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.
|
||||
@@ -24,7 +24,7 @@ impl<'a> CharIterator<'a> {
|
||||
|
||||
impl CharIterator<'_> {
|
||||
/// 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 mut count = 0;
|
||||
let mut i = 0;
|
||||
@@ -123,3 +123,19 @@ impl CharIterator<'_> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
223
jaarg/src/help.rs
Normal file
223
jaarg/src/help.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
/* jaarg - Argument parser
|
||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
*/
|
||||
|
||||
/// 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(())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/* jaarg - Argument parser
|
||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||
* SPDX-License-Identifier: MIT
|
||||
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
*/
|
||||
|
||||
#![no_std]
|
||||
@@ -13,5 +13,7 @@ include!("options.rs");
|
||||
include!("argparse.rs");
|
||||
include!("help.rs");
|
||||
|
||||
#[cfg(feature = "alloc")]
|
||||
pub mod alloc;
|
||||
#[cfg(feature = "std")]
|
||||
pub mod std;
|
||||
379
jaarg/src/option.rs
Normal file
379
jaarg/src/option.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
/* jaarg - Argument parser
|
||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
*/
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
enum OptType {
|
||||
Positional,
|
||||
Flag,
|
||||
Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
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> {
|
||||
id: ID,
|
||||
names: OptIdentifier,
|
||||
value_name: Option<&'static str>,
|
||||
help_string: Option<&'static str>,
|
||||
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)]
|
||||
const fn is_short_visible(&self) -> bool {
|
||||
(self.flags.0 & OptFlag::VISIBLE_SHORT.0) != 0
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
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.
|
||||
const fn first_short_name(&self) -> Option<&'static str> {
|
||||
const fn predicate(name: &str) -> bool {
|
||||
let mut chars = const_utf8::CharIterator::from(name);
|
||||
if let Some(first) = chars.next() {
|
||||
if let Some(c) = chars.next() {
|
||||
if c != first && chars.next().is_none() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
match self.names {
|
||||
OptIdentifier::Single(name) => if predicate(&name) { Some(name) } else { None },
|
||||
// Can be replaced with `find_map` once iterators are const fn
|
||||
OptIdentifier::Multi(names) => {
|
||||
let mut i = 0;
|
||||
while i < names.len() {
|
||||
if predicate(names[i]) {
|
||||
return Some(names[i]);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the first applicable short option's flag character, if one exists.
|
||||
const fn first_short_name_char(&self) -> Option<char> {
|
||||
const fn predicate(name: &str) -> Option<char> {
|
||||
let mut chars = const_utf8::CharIterator::from(name);
|
||||
if let Some(first) = chars.next() {
|
||||
if let Some(c) = chars.next() {
|
||||
if c != first && chars.next().is_none() {
|
||||
return Some(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
match self.names {
|
||||
OptIdentifier::Single(name) => predicate(&name),
|
||||
// Can be replaced with `find_map` once iterators are const fn.
|
||||
OptIdentifier::Multi(names) => {
|
||||
let mut i = 0;
|
||||
while i < names.len() {
|
||||
if let Some(c) = predicate(names[i]) {
|
||||
return Some(c);
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Search for a matching name in the option, offset allows to skip the first `n = offset` characters in the comparison.
|
||||
fn match_name(&self, string: &str, offset: usize) -> Option<&'static str> {
|
||||
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 opt_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);
|
||||
}
|
||||
}
|
||||
123
jaarg/src/options.rs
Normal file
123
jaarg/src/options.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
/* jaarg - Argument parser
|
||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
*/
|
||||
|
||||
/// Static structure that contains instructions for parsing command-line arguments.
|
||||
#[derive(Debug, PartialEq)]
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 opts_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);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
/* jaarg - Argument parser
|
||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||
* SPDX-License-Identifier: MIT
|
||||
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||
*/
|
||||
|
||||
#![allow(private_bounds)]
|
||||
78
jaarg/src/std.rs
Normal file
78
jaarg/src/std.rs
Normal 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::{
|
||||
ErrorUsageWriter, ErrorUsageWriterContext, HandlerResult, HelpWriter, HelpWriterContext, Opt, Opts,
|
||||
ParseControl, ParseError, ParseResult, StandardErrorUsageWriter, StandardFullHelpWriter, alloc::ParseMapResult
|
||||
};
|
||||
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(&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.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))
|
||||
}
|
||||
}
|
||||
186
src/help.rs
186
src/help.rs
@@ -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(())
|
||||
}
|
||||
}
|
||||
189
src/option.rs
189
src/option.rs
@@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
105
src/std.rs
105
src/std.rs
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user