mirror of
https://github.com/gay-pizza/jaarg.git
synced 2025-12-19 15:30:16 +00:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75fc27bc58 | |||
| 8d9e8aea89 | |||
| 20f5a0bf10 | |||
| 7165bb9841 | |||
| 8f6f1827ce | |||
| 33af658e93 | |||
| 87e3e5f4e0 | |||
|
|
82ef9cf8d5 | ||
|
03e1953aae
|
|||
| 148a649273 | |||
| 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 |
@@ -10,8 +10,3 @@ tab_width = 4
|
|||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
|
|
||||||
[*.py]
|
|
||||||
indent_style = tab
|
|
||||||
indent_size = tab
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|||||||
31
.github/workflows/test.yaml
vendored
Normal file
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}}
|
||||||
19
Cargo.toml
19
Cargo.toml
@@ -1,13 +1,16 @@
|
|||||||
[package]
|
[workspace]
|
||||||
name = "jaarg"
|
default-members = ["jaarg"]
|
||||||
version = "0.1.0"
|
members = ["jaarg-nostd"]
|
||||||
license = "MIT"
|
resolver = "3"
|
||||||
|
|
||||||
|
[workspace.package]
|
||||||
|
version = "0.2.2"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "It can parse your arguments you should use it it's called jaarg"
|
description = "It can parse your arguments you should use it it's called jaarg"
|
||||||
|
repository = "https://github.com/gay-pizza/jaarg"
|
||||||
[features]
|
homepage = "https://gay.pizza/"
|
||||||
default = ["std"]
|
authors = ["a dinosaur", "Gay Pizza Specifications"]
|
||||||
std = []
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = "thin"
|
lto = "thin"
|
||||||
|
|||||||
201
LICENSE.Apache-2.0
Normal file
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.
|
||||||
101
README.md
101
README.md
@@ -1 +1,102 @@
|
|||||||
# jaarg argument parser library #
|
# jaarg argument parser library #
|
||||||
|
|
||||||
|
Dependency-free, const (mostly), no magic macros, `no_std` & no alloc (though nicer with those).
|
||||||
|
Some say it can parse your arguments.
|
||||||
|
|
||||||
|
### Obligatory fancy banners ###
|
||||||
|
<div>
|
||||||
|
<a href="https://crates.io/crates/jaarg">
|
||||||
|
<img src="https://img.shields.io/crates/v/jaarg.svg?logo=rust&style=for-the-badge" alt="Crates version" />
|
||||||
|
</a>
|
||||||
|
<a href="#licensing">
|
||||||
|
<img src="https://img.shields.io/badge/license-MIT%20%7C%20Apache--2.0-green.svg?style=for-the-badge" alt="MIT OR Apache-2.0 License" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
### Example usage ###
|
||||||
|
```rust
|
||||||
|
// Variables for arguments to fill
|
||||||
|
let mut file = PathBuf::new();
|
||||||
|
let mut out: Option<PathBuf> = None;
|
||||||
|
let mut number = 0;
|
||||||
|
|
||||||
|
// Set up arguments table
|
||||||
|
enum Arg { Help, Number, File, Out }
|
||||||
|
const OPTIONS: Opts<Arg> = Opts::new(&[
|
||||||
|
Opt::help_flag(Arg::Help, &["-h", "--help"]).help_text("Show this help and exit."),
|
||||||
|
Opt::value(Arg::Number, &["-n", "--number"], "value")
|
||||||
|
.help_text("Optionally specify a number (default: 0)"),
|
||||||
|
Opt::positional(Arg::File, "file").required()
|
||||||
|
.help_text("Input file."),
|
||||||
|
Opt::positional(Arg::Out, "out")
|
||||||
|
.help_text("Output destination (optional).")
|
||||||
|
]).with_description("My simple utility.");
|
||||||
|
|
||||||
|
// Parse command-line arguments from `std::env::args()`
|
||||||
|
match OPTIONS.parse_easy(|program_name, id, _opt, _name, arg| {
|
||||||
|
match id {
|
||||||
|
Arg::Help => {
|
||||||
|
OPTIONS.print_full_help(program_name);
|
||||||
|
return Ok(ParseControl::Quit);
|
||||||
|
}
|
||||||
|
Arg::Number => { number = str::parse(arg)?; }
|
||||||
|
Arg::File => { file = arg.into(); }
|
||||||
|
Arg::Out => { out = Some(arg.into()); }
|
||||||
|
}
|
||||||
|
Ok(ParseControl::Continue)
|
||||||
|
}) {
|
||||||
|
ParseResult::ContinueSuccess => (),
|
||||||
|
ParseResult::ExitSuccess => std::process::exit(0),
|
||||||
|
ParseResult::ExitError => std::process::exit(1),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the result variables
|
||||||
|
println!("{file:?} -> {out:?} (number: {number:?})",
|
||||||
|
out = out.unwrap_or(file.with_extension("out")));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Changelog ###
|
||||||
|
|
||||||
|
v0.2.2:
|
||||||
|
* Fixed coerced `ArgumentError` not being rewritten for positional arguments.
|
||||||
|
* Moved top level includes to `pub use`.
|
||||||
|
* Hopefully work around licence & read me texts not being included in crate.
|
||||||
|
|
||||||
|
v0.2.1:
|
||||||
|
* Fixed licence field in `Cargo.toml`.
|
||||||
|
|
||||||
|
v0.2.0:
|
||||||
|
* Change licence from `MIT` to `MIT OR Apache-2.0`.
|
||||||
|
* Moved `Opts::parse_map` into newly introduced `alloc` crate, making it accessible for `no_std` users.
|
||||||
|
* More generic & flexible help API: removed forced newline, moved error writer to `StandardErrorUsageWriter`,
|
||||||
|
generalised "Usage" line in standard full writer, enough public constructs to roll a custom help writer.
|
||||||
|
* Added the ability to exclude options from short usage, full help, or both.
|
||||||
|
* More tests for validating internal behaviour & enabled CI on GitHub.
|
||||||
|
* Added new `no_std` examples.
|
||||||
|
|
||||||
|
v0.1.1:
|
||||||
|
* Fixed incorrect error message format for coerced parsing errors.
|
||||||
|
* Cleaned up docstring formatting.
|
||||||
|
* Added basic example.
|
||||||
|
|
||||||
|
v0.1.0:
|
||||||
|
* Initial release.
|
||||||
|
|
||||||
|
### Roadmap ###
|
||||||
|
|
||||||
|
Near future:
|
||||||
|
* More control over parsing behaviour (getopt style, no special casing shorts for Windows style flags, etc.)
|
||||||
|
* More practical examples.
|
||||||
|
|
||||||
|
Long term:
|
||||||
|
* Strategy for handling exclusive argument groups.
|
||||||
|
* Make use of const traits when they land to improve table setup.
|
||||||
|
|
||||||
|
### Projects using jaarg (very cool) ###
|
||||||
|
* [Sprout bootloader](https://github.com/edera-dev/sprout)
|
||||||
|
* [lbminfo](https://github.com/ScrelliCopter/colourcyclinginthehousetonight/tree/main/lbminfo)
|
||||||
|
|
||||||
|
### Licensing ###
|
||||||
|
|
||||||
|
jaarg is dual-licensed under either the [MIT](LICENSE.MIT) or [Apache 2.0](LICENSE.Apache-2.0) licences.
|
||||||
|
Pick whichever works best for your project.
|
||||||
|
|||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
14
jaarg/Cargo.toml
Normal file
14
jaarg/Cargo.toml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "jaarg"
|
||||||
|
version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["std"]
|
||||||
|
alloc = []
|
||||||
|
std = ["alloc"]
|
||||||
1
jaarg/LICENSE.Apache-2.0
Symbolic link
1
jaarg/LICENSE.Apache-2.0
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../LICENSE.Apache-2.0
|
||||||
1
jaarg/LICENSE.MIT
Symbolic link
1
jaarg/LICENSE.MIT
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../LICENSE.MIT
|
||||||
1
jaarg/README.md
Symbolic link
1
jaarg/README.md
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../README.md
|
||||||
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
|
/* bin2c - jaarg example application
|
||||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||||
* SPDX-License-Identifier: MIT
|
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use jaarg::{Opt, Opts, ParseControl, ParseResult};
|
use jaarg::{Opt, Opts, ParseControl, ParseResult};
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
/* btreemap - jaarg example program using BTreeMap
|
/* btreemap - jaarg example program using BTreeMap
|
||||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||||
* SPDX-License-Identifier: MIT
|
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use jaarg::{std::ParseMapResult, Opt, Opts};
|
use jaarg::{alloc::ParseMapResult, Opt, Opts};
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
@@ -16,7 +16,8 @@ fn main() -> ExitCode {
|
|||||||
|
|
||||||
let map = match OPTIONS.parse_map_easy() {
|
let map = match OPTIONS.parse_map_easy() {
|
||||||
ParseMapResult::Map(map) => map,
|
ParseMapResult::Map(map) => map,
|
||||||
ParseMapResult::Exit(code) => { return code; }
|
ParseMapResult::ExitSuccess => { return ExitCode::SUCCESS; }
|
||||||
|
ParseMapResult::ExitFailure => { return ExitCode::FAILURE; }
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("{:?}", map);
|
println!("{:?}", map);
|
||||||
40
jaarg/src/alloc.rs
Normal file
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,24 @@
|
|||||||
/* jaarg - Argument parser
|
/* jaarg - Argument parser
|
||||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||||
* SPDX-License-Identifier: MIT
|
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use crate::{Opt, Opts};
|
||||||
|
use crate::option::OptType;
|
||||||
|
use crate::options::RequiredParamsBitSet;
|
||||||
|
|
||||||
/// Enum describing the result of parsing arguments, and how the program should behave.
|
/// Enum describing the result of parsing arguments, and how the program should behave.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ParseResult {
|
pub enum ParseResult {
|
||||||
/// Parsing succeeded and program execution should continue
|
/// Parsing succeeded and program execution should continue.
|
||||||
ContinueSuccess,
|
ContinueSuccess,
|
||||||
/// Parsing succeeded and program should exit with success (eg; std::process::ExitCode::SUCCESS)
|
/// Parsing succeeded and program should exit with success (eg; `exit(0)`).
|
||||||
ExitSuccess,
|
ExitSuccess,
|
||||||
/// There was an error while parsing and program should exit with failure (eg; std::process::ExitCode::FAILURE)
|
/// There was an error while parsing and program should exit with failure (eg; `exit(1)`).
|
||||||
ExitError,
|
ExitError,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execution control for the parser handler
|
/// Execution control for parser handlers.
|
||||||
pub enum ParseControl {
|
pub enum ParseControl {
|
||||||
/// Continue parsing arguments
|
/// Continue parsing arguments
|
||||||
Continue,
|
Continue,
|
||||||
@@ -24,8 +28,8 @@ pub enum ParseControl {
|
|||||||
Quit,
|
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>>;
|
pub(crate) type HandlerResult<'a, T> = core::result::Result<T, ParseError<'a>>;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ParseError<'a> {
|
pub enum ParseError<'a> {
|
||||||
@@ -69,7 +73,7 @@ impl core::fmt::Display for ParseError<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience coercion for dealing with integer parsing errors
|
/// Convenience coercion for dealing with integer parsing errors.
|
||||||
impl From<core::num::ParseIntError> for ParseError<'_> {
|
impl From<core::num::ParseIntError> for ParseError<'_> {
|
||||||
fn from(err: core::num::ParseIntError) -> Self {
|
fn from(err: core::num::ParseIntError) -> Self {
|
||||||
use core::num::IntErrorKind;
|
use core::num::IntErrorKind;
|
||||||
@@ -83,7 +87,7 @@ impl From<core::num::ParseIntError> for ParseError<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience coercion for dealing with floating-point parsing errors
|
/// Convenience coercion for dealing with floating-point parsing errors.
|
||||||
impl From<core::num::ParseFloatError> for ParseError<'_> {
|
impl From<core::num::ParseFloatError> for ParseError<'_> {
|
||||||
fn from(_err: core::num::ParseFloatError) -> Self {
|
fn from(_err: core::num::ParseFloatError) -> Self {
|
||||||
// HACK: The empty option & argument fields will be fixed up by the parser
|
// HACK: The empty option & argument fields will be fixed up by the parser
|
||||||
@@ -94,7 +98,7 @@ impl From<core::num::ParseFloatError> for ParseError<'_> {
|
|||||||
|
|
||||||
impl core::error::Error for ParseError<'_> {}
|
impl core::error::Error for ParseError<'_> {}
|
||||||
|
|
||||||
/// Internal state tracked by the parser
|
/// Internal state tracked by the parser.
|
||||||
struct ParserState<ID: 'static> {
|
struct ParserState<ID: 'static> {
|
||||||
positional_index: usize,
|
positional_index: usize,
|
||||||
expects_arg: Option<(&'static str, &'static Opt<ID>)>,
|
expects_arg: Option<(&'static str, &'static Opt<ID>)>,
|
||||||
@@ -112,7 +116,7 @@ impl<ID> Default for ParserState<ID> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<ID: 'static> Opts<ID> {
|
impl<ID: 'static> Opts<ID> {
|
||||||
/// Parse an iterator of strings as arguments
|
/// Parses an iterator of strings as argument tokens.
|
||||||
pub fn parse<'a, S: AsRef<str> + 'a, I: Iterator<Item = S>>(&self, program_name: &str, args: I,
|
pub fn parse<'a, S: AsRef<str> + 'a, I: Iterator<Item = S>>(&self, program_name: &str, args: I,
|
||||||
mut handler: impl FnMut(&str, &ID, &Opt<ID>, &str, &str) -> HandlerResult<'a, ParseControl>,
|
mut handler: impl FnMut(&str, &ID, &Opt<ID>, &str, &str) -> HandlerResult<'a, ParseControl>,
|
||||||
error: impl FnOnce(&str, ParseError),
|
error: impl FnOnce(&str, ParseError),
|
||||||
@@ -140,7 +144,7 @@ impl<ID: 'static> Opts<ID> {
|
|||||||
|
|
||||||
// Ensure that all required arguments have been provided
|
// Ensure that all required arguments have been provided
|
||||||
let mut required_flag_idx = 0;
|
let mut required_flag_idx = 0;
|
||||||
for (i, option) in self.options.iter().enumerate() {
|
for (i, option) in self.iter().enumerate() {
|
||||||
match option.r#type {
|
match option.r#type {
|
||||||
OptType::Positional => if i >= state.positional_index && option.is_required() {
|
OptType::Positional => if i >= state.positional_index && option.is_required() {
|
||||||
error(program_name, ParseError::RequiredPositional(option.first_name()));
|
error(program_name, ParseError::RequiredPositional(option.first_name()));
|
||||||
@@ -169,7 +173,7 @@ impl<ID: 'static> Opts<ID> {
|
|||||||
// HACK: Ensure the string fields are set properly, because coerced
|
// HACK: Ensure the string fields are set properly, because coerced
|
||||||
// ParseIntError/ParseFloatError will have the string fields blanked.
|
// ParseIntError/ParseFloatError will have the string fields blanked.
|
||||||
Err(ParseError::ArgumentError("", "", kind))
|
Err(ParseError::ArgumentError("", "", kind))
|
||||||
=> Err(ParseError::ArgumentError(name, token, kind)),
|
=> Err(ParseError::ArgumentError(name, value, kind)),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
Ok(ctl) => Ok(ctl),
|
Ok(ctl) => Ok(ctl),
|
||||||
}
|
}
|
||||||
@@ -192,7 +196,7 @@ impl<ID: 'static> Opts<ID> {
|
|||||||
let mut required_idx = 0;
|
let mut required_idx = 0;
|
||||||
|
|
||||||
// Match a suitable option by name (ignoring the first flag character & skipping positional arguments)
|
// Match a suitable option by name (ignoring the first flag character & skipping positional arguments)
|
||||||
let (name, option) = self.options.iter()
|
let (name, option) = self.iter()
|
||||||
.filter(|opt| matches!(opt.r#type, OptType::Flag | OptType::Value)).find_map(|opt| {
|
.filter(|opt| matches!(opt.r#type, OptType::Flag | OptType::Value)).find_map(|opt| {
|
||||||
if let Some(name) = opt.match_name(option_str, 1) {
|
if let Some(name) = opt.match_name(option_str, 1) {
|
||||||
Some((name, opt))
|
Some((name, opt))
|
||||||
@@ -228,7 +232,7 @@ impl<ID: 'static> Opts<ID> {
|
|||||||
// Find the next positional argument
|
// Find the next positional argument
|
||||||
for (i, option) in self.options[state.positional_index..].iter().enumerate() {
|
for (i, option) in self.options[state.positional_index..].iter().enumerate() {
|
||||||
if matches!(option.r#type, OptType::Positional) {
|
if matches!(option.r#type, OptType::Positional) {
|
||||||
handler(program_name, &option.id, option, option.first_name(), token)?;
|
call_handler(option, option.first_name(), token)?;
|
||||||
state.positional_index += i + 1;
|
state.positional_index += i + 1;
|
||||||
return Ok(ParseControl::Continue);
|
return Ok(ParseControl::Continue);
|
||||||
}
|
}
|
||||||
@@ -238,3 +242,49 @@ impl<ID: 'static> Opts<ID> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse() {
|
||||||
|
extern crate alloc;
|
||||||
|
use alloc::string::String;
|
||||||
|
|
||||||
|
enum ArgID { One, Two, Three, Four, Five }
|
||||||
|
const OPTIONS: Opts<ArgID> = Opts::new(&[
|
||||||
|
Opt::positional(ArgID::One, "one"),
|
||||||
|
Opt::flag(ArgID::Two, &["--two"]),
|
||||||
|
Opt::value(ArgID::Three, &["--three"], "value"),
|
||||||
|
Opt::value(ArgID::Four, &["--four"], "value"),
|
||||||
|
Opt::value(ArgID::Five, &["--five"], "value"),
|
||||||
|
]);
|
||||||
|
const ARGUMENTS: &[&str] = &["one", "--two", "--three=three", "--five=", "--four", "four"];
|
||||||
|
|
||||||
|
//TODO: currently needs alloc to deal with arguments not being able to escape handler
|
||||||
|
let mut one: Option<String> = None;
|
||||||
|
let mut two = false;
|
||||||
|
let mut three: Option<String> = None;
|
||||||
|
let mut four: Option<String> = None;
|
||||||
|
let mut five: Option<String> = None;
|
||||||
|
assert!(matches!(OPTIONS.parse("", ARGUMENTS.iter(), |_program_name, id, _opt, _name, arg| {
|
||||||
|
match id {
|
||||||
|
ArgID::One => { one = Some(arg.into()); }
|
||||||
|
ArgID::Two => { two = true; }
|
||||||
|
ArgID::Three => { three = Some(arg.into()); }
|
||||||
|
ArgID::Four => { four = Some(arg.into()); }
|
||||||
|
ArgID::Five => { five = Some(arg.into()); }
|
||||||
|
}
|
||||||
|
Ok(ParseControl::Continue)
|
||||||
|
}, |_, error| {
|
||||||
|
panic!("unreachable: {error:?}");
|
||||||
|
}), ParseResult::ContinueSuccess));
|
||||||
|
|
||||||
|
assert_eq!(one, Some("one".into()));
|
||||||
|
assert!(two);
|
||||||
|
assert_eq!(three, Some("three".into()));
|
||||||
|
assert_eq!(four, Some("four".into()));
|
||||||
|
assert_eq!(five, Some("".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/* jaarg - Argument parser
|
/* jaarg - Argument parser
|
||||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||||
* SPDX-License-Identifier: MIT
|
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/// Fully const fn nostd UTF-8 character iterator.
|
/// Fully const fn nostd UTF-8 character iterator.
|
||||||
@@ -24,7 +24,7 @@ impl<'a> CharIterator<'a> {
|
|||||||
|
|
||||||
impl CharIterator<'_> {
|
impl CharIterator<'_> {
|
||||||
/// Gets a count of the number of Unicode characters (not graphemes) in the string.
|
/// Gets a count of the number of Unicode characters (not graphemes) in the string.
|
||||||
pub(crate) const fn count(self) -> usize {
|
pub(crate) const fn count(&self) -> usize {
|
||||||
let len = self.bytes.len();
|
let len = self.bytes.len();
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
@@ -123,3 +123,19 @@ impl CharIterator<'_> {
|
|||||||
Some(result)
|
Some(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test() {
|
||||||
|
for s in ["pizza", "/ˈpitt͡sə/", "pizzaskjærer", "🍕", "比薩", "ピザ", "Ćevapi", "🏳️⚧️"] {
|
||||||
|
let mut it = CharIterator::from(s);
|
||||||
|
assert_eq!(it.count(), s.chars().count());
|
||||||
|
s.chars().for_each(|c| assert_eq!(it.next(), Some(c)));
|
||||||
|
assert_eq!(it.next(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
226
jaarg/src/help.rs
Normal file
226
jaarg/src/help.rs
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
/* jaarg - Argument parser
|
||||||
|
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||||
|
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::{Opt, Opts, ParseError};
|
||||||
|
use crate::option::{OptIdentifier, OptType};
|
||||||
|
|
||||||
|
/// Enough context to show full help text.
|
||||||
|
pub struct HelpWriterContext<'a, ID: 'static> {
|
||||||
|
pub options: &'a Opts<ID>,
|
||||||
|
pub program_name: &'a str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ID: 'static> Clone for HelpWriterContext<'_, ID> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self { options: self.options, program_name: self.program_name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait HelpWriter<'a, ID: 'static>: core::fmt::Display {
|
||||||
|
fn new(ctx: HelpWriterContext<'a, ID>) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StandardShortUsageWriter<'a, ID: 'static>(HelpWriterContext<'a, ID>);
|
||||||
|
|
||||||
|
impl<'a, ID: 'static> HelpWriter<'a, ID> for StandardShortUsageWriter<'a, ID> {
|
||||||
|
fn new(ctx: HelpWriterContext<'a, ID>) -> Self { Self(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ID: 'static> core::fmt::Display for StandardShortUsageWriter<'_, ID> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
write!(f, "Usage: {}", self.0.program_name)?;
|
||||||
|
|
||||||
|
// Write option parameter arguments
|
||||||
|
for option in self.0.options.iter()
|
||||||
|
.filter(|o| matches!((o.r#type, o.is_short_visible()), (OptType::Value | OptType::Flag, true))) {
|
||||||
|
write!(f, " {}", if option.is_required() { '<' } else { '[' })?;
|
||||||
|
match (option.first_short_name(), option.first_long_name()) {
|
||||||
|
(Some(short_name), Some(long_name)) => write!(f, "{short_name}|{long_name}")?,
|
||||||
|
(Some(short_name), None) => f.write_str(short_name)?,
|
||||||
|
(None, Some(long_name)) => f.write_str(long_name)?,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
if let Some(value_name) = option.value_name {
|
||||||
|
write!(f, " {value_name}")?;
|
||||||
|
}
|
||||||
|
write!(f, "{}", if option.is_required() { '>' } else { ']' })?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write positional arguments
|
||||||
|
for option in self.0.options.iter()
|
||||||
|
.filter(|o| matches!((o.r#type, o.is_short_visible()), (OptType::Positional, true))) {
|
||||||
|
let name = option.first_name();
|
||||||
|
match option.is_required() {
|
||||||
|
true => write!(f, " <{name}>")?,
|
||||||
|
false => write!(f, " [{name}]")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StandardFullHelpWriter<'a, ID: 'static>(HelpWriterContext<'a, ID>);
|
||||||
|
|
||||||
|
impl<'a, ID: 'static> HelpWriter<'a, ID> for StandardFullHelpWriter<'a, ID> {
|
||||||
|
fn new(ctx: HelpWriterContext<'a, ID>) -> Self { Self(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ID> core::fmt::Display for StandardFullHelpWriter<'_, ID> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
// Base short usage
|
||||||
|
writeln!(f, "{}", StandardShortUsageWriter::new(self.0.clone()))?;
|
||||||
|
|
||||||
|
if let Some(description) = self.0.options.description {
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, "{description}")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the alignment width from the longest option parameter
|
||||||
|
fn calculate_option_line_length<ID: 'static>(option: &Opt<ID>) -> usize {
|
||||||
|
(match option.names {
|
||||||
|
OptIdentifier::Single(name) => name.chars().count(),
|
||||||
|
OptIdentifier::Multi(names) => (names.len() - 1) * 3 + names.iter()
|
||||||
|
.fold(0, |accum, name| accum + name.chars().count()),
|
||||||
|
}) + option.value_name.map_or(0, |v| v.len() + 3)
|
||||||
|
}
|
||||||
|
let align_width = 3 + self.0.options.iter()
|
||||||
|
.map(|o| calculate_option_line_length(o)).max().unwrap_or(0);
|
||||||
|
|
||||||
|
// Write positional argument descriptions
|
||||||
|
let mut first = true;
|
||||||
|
for option in self.0.options.iter()
|
||||||
|
.filter(|o| matches!((o.r#type, o.is_full_visible()), (OptType::Positional, true))) {
|
||||||
|
if first {
|
||||||
|
// Write separator and positional section header
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, "Positional arguments:")?;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write positional argument line (name + optional aligned help text)
|
||||||
|
let name = option.first_name();
|
||||||
|
write!(f, " {name}")?;
|
||||||
|
if let Some(help_text) = option.help_string {
|
||||||
|
write!(f, " {:.<width$} {help_text}", "",
|
||||||
|
width = align_width - name.chars().count() - 1)?;
|
||||||
|
}
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formatter for option usage lines.
|
||||||
|
struct OptionUsageLine<'a, ID>(&'a Opt<ID>);
|
||||||
|
impl<ID> core::fmt::Display for OptionUsageLine<'_, ID> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
use core::fmt::Write;
|
||||||
|
let mut length = 0;
|
||||||
|
|
||||||
|
// Write option flag name(s)
|
||||||
|
match self.0.names {
|
||||||
|
OptIdentifier::Single(name) => {
|
||||||
|
write!(f, "{name}")?;
|
||||||
|
length = name.chars().count();
|
||||||
|
}
|
||||||
|
OptIdentifier::Multi(names) => {
|
||||||
|
for (i, name) in names.iter().enumerate() {
|
||||||
|
if i == 0 {
|
||||||
|
write!(f, "{name}")?;
|
||||||
|
length += name.chars().count();
|
||||||
|
} else {
|
||||||
|
write!(f, " | {name}")?;
|
||||||
|
length += 3 + name.chars().count();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write value argument for value options parameters
|
||||||
|
if let Some(value_name) = self.0.value_name {
|
||||||
|
write!(f, " <{value_name}>")?;
|
||||||
|
length += 2 + value_name.chars().count() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write padding if requested
|
||||||
|
match (f.align(), f.width().unwrap_or(0).checked_sub(length)) {
|
||||||
|
(Some(core::fmt::Alignment::Left), Some(width)) if width > 0 => {
|
||||||
|
let fill = f.fill();
|
||||||
|
// First padding char is *always* a space
|
||||||
|
f.write_char(' ')?;
|
||||||
|
for _ in 1..width {
|
||||||
|
f.write_char(fill)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write option parameter argument descriptions
|
||||||
|
first = true;
|
||||||
|
for option in self.0.options.iter()
|
||||||
|
.filter(|o| matches!((o.r#type, o.is_full_visible()), (OptType::Flag | OptType::Value, true))) {
|
||||||
|
if first {
|
||||||
|
// Write separator and options section header
|
||||||
|
writeln!(f)?;
|
||||||
|
writeln!(f, "Options:")?;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write line for option, with aligned help text if needed
|
||||||
|
let line = OptionUsageLine(option);
|
||||||
|
if let Some(help_text) = option.help_string {
|
||||||
|
write!(f, " {line:.<align_width$} {help_text}")?;
|
||||||
|
} else {
|
||||||
|
write!(f, " {line}")?;
|
||||||
|
}
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Enough context to show usage and error information.
|
||||||
|
pub struct ErrorUsageWriterContext<'a, ID: 'static> {
|
||||||
|
pub options: &'a Opts<ID>,
|
||||||
|
pub program_name: &'a str,
|
||||||
|
pub error: ParseError<'a>
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ErrorUsageWriter<'a, ID: 'static>: core::fmt::Display {
|
||||||
|
fn new(ctx: ErrorUsageWriterContext<'a, ID>) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StandardErrorUsageWriter<'a, ID: 'static>(ErrorUsageWriterContext<'a, ID>);
|
||||||
|
|
||||||
|
impl<'a, ID: 'static> ErrorUsageWriter<'a, ID> for StandardErrorUsageWriter<'a, ID> {
|
||||||
|
fn new(ctx: ErrorUsageWriterContext<'a, ID>) -> Self { Self(ctx) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<ID> core::fmt::Display for StandardErrorUsageWriter<'_, ID> {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
// Write error
|
||||||
|
writeln!(f, "{name}: {error}", name=self.0.program_name, error = self.0.error)?;
|
||||||
|
|
||||||
|
// Provide usage hint for missing required arguments
|
||||||
|
if matches!(self.0.error, ParseError::RequiredPositional(_) | ParseError::RequiredParameter(_)) {
|
||||||
|
// Write short usage
|
||||||
|
writeln!(f, "{}", StandardShortUsageWriter::new(HelpWriterContext {
|
||||||
|
options: self.0.options,
|
||||||
|
program_name: self.0.program_name
|
||||||
|
}))?;
|
||||||
|
|
||||||
|
// Write full help instruction if available
|
||||||
|
if let Some(help_option) = self.0.options.help_option() {
|
||||||
|
writeln!(f, "Run '{name} {help}' to view all available options.",
|
||||||
|
name = self.0.program_name,
|
||||||
|
// Prefer long name, but otherwise any name is fine
|
||||||
|
help = help_option.first_long_name().unwrap_or(help_option.first_name()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
24
jaarg/src/lib.rs
Normal file
24
jaarg/src/lib.rs
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* jaarg - Argument parser
|
||||||
|
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||||
|
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#![no_std]
|
||||||
|
|
||||||
|
mod const_utf8;
|
||||||
|
mod ordered_bitset;
|
||||||
|
|
||||||
|
mod option;
|
||||||
|
mod options;
|
||||||
|
mod argparse;
|
||||||
|
mod help;
|
||||||
|
|
||||||
|
pub use option::*;
|
||||||
|
pub use options::*;
|
||||||
|
pub use argparse::*;
|
||||||
|
pub use help::*;
|
||||||
|
|
||||||
|
#[cfg(feature = "alloc")]
|
||||||
|
pub mod alloc;
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
pub mod std;
|
||||||
381
jaarg/src/option.rs
Normal file
381
jaarg/src/option.rs
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
/* jaarg - Argument parser
|
||||||
|
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||||
|
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::const_utf8;
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub(crate) enum OptType {
|
||||||
|
Positional,
|
||||||
|
Flag,
|
||||||
|
Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub(crate) enum OptIdentifier {
|
||||||
|
Single(&'static str),
|
||||||
|
Multi(&'static[&'static str]),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents an option argument or positional argument to be parsed.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct Opt<ID> {
|
||||||
|
pub(crate) id: ID,
|
||||||
|
pub(crate) names: OptIdentifier,
|
||||||
|
pub(crate) value_name: Option<&'static str>,
|
||||||
|
pub(crate) help_string: Option<&'static str>,
|
||||||
|
pub(crate) r#type: OptType,
|
||||||
|
flags: OptFlag,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum OptHide {
|
||||||
|
Short,
|
||||||
|
Full,
|
||||||
|
All,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
struct OptFlag(u8);
|
||||||
|
|
||||||
|
impl OptFlag {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub const NONE: Self = Self(0);
|
||||||
|
pub const REQUIRED: Self = OptFlag(1 << 0);
|
||||||
|
pub const HELP: Self = OptFlag(1 << 1);
|
||||||
|
pub const VISIBLE_SHORT: Self = OptFlag(1 << 2);
|
||||||
|
pub const VISIBLE_FULL: Self = OptFlag(1 << 3);
|
||||||
|
|
||||||
|
pub const DEFAULT: Self = Self(Self::VISIBLE_SHORT.0 | Self::VISIBLE_FULL.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Improve this interface by making the name field take AsOptIdentifier when const traits are stabilised
|
||||||
|
impl<ID> Opt<ID> {
|
||||||
|
#[inline]
|
||||||
|
const fn new(id: ID, names: OptIdentifier, value_name: Option<&'static str>, r#type: OptType) -> Self {
|
||||||
|
assert!(match names {
|
||||||
|
OptIdentifier::Single(_) => true,
|
||||||
|
OptIdentifier::Multi(names) => !names.is_empty(),
|
||||||
|
}, "Option names cannot be an empty slice");
|
||||||
|
Self { id, names, value_name, help_string: None, r#type, flags: OptFlag::DEFAULT }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A positional argument that is parsed sequentially without being invoked by an option flag.
|
||||||
|
pub const fn positional(id: ID, name: &'static str) -> Self {
|
||||||
|
Self::new(id, OptIdentifier::Single(name), None, OptType::Positional)
|
||||||
|
}
|
||||||
|
/// A flag-type option that serves as the interface's help flag.
|
||||||
|
pub const fn help_flag(id: ID, names: &'static[&'static str]) -> Self {
|
||||||
|
Self::new(id, OptIdentifier::Multi(names), None, OptType::Flag)
|
||||||
|
.with_help_flag()
|
||||||
|
}
|
||||||
|
/// A flag-type option, takes no value.
|
||||||
|
pub const fn flag(id: ID, names: &'static[&'static str]) -> Self {
|
||||||
|
Self::new(id, OptIdentifier::Multi(names), None, OptType::Flag)
|
||||||
|
}
|
||||||
|
/// An option argument that takes a value.
|
||||||
|
pub const fn value(id: ID, names: &'static[&'static str], value_name: &'static str) -> Self {
|
||||||
|
Self::new(id, OptIdentifier::Multi(names), Some(value_name), OptType::Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This option is required, ie; parsing will fail if it is not specified.
|
||||||
|
#[inline]
|
||||||
|
pub const fn required(mut self) -> Self {
|
||||||
|
assert!(!self.is_help(), "Help flag cannot be made required");
|
||||||
|
self.flags.0 |= OptFlag::REQUIRED.0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the help string for an option.
|
||||||
|
#[inline]
|
||||||
|
pub const fn help_text(mut self, help_string: &'static str) -> Self {
|
||||||
|
self.help_string = Some(help_string);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marks the option to exclude it from appearing in short usage text, full help text, or both.
|
||||||
|
#[inline]
|
||||||
|
pub const fn hide_usage(mut self, from: OptHide) -> Self {
|
||||||
|
self.flags.0 &= !match from {
|
||||||
|
OptHide::Short => OptFlag::VISIBLE_SHORT.0,
|
||||||
|
OptHide::Full => OptFlag::VISIBLE_FULL.0,
|
||||||
|
OptHide::All => OptFlag::VISIBLE_SHORT.0 | OptFlag::VISIBLE_FULL.0,
|
||||||
|
};
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
const fn with_help_flag(mut self) -> Self {
|
||||||
|
assert!(matches!(self.r#type, OptType::Flag), "Only flags are allowed to be help options");
|
||||||
|
self.flags.0 |= OptFlag::HELP.0;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this is a required positional argument, or required option argument.
|
||||||
|
#[inline(always)]
|
||||||
|
pub const fn is_required(&self) -> bool {
|
||||||
|
(self.flags.0 & OptFlag::REQUIRED.0) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if this is the help option.
|
||||||
|
#[inline(always)]
|
||||||
|
pub const fn is_help(&self) -> bool {
|
||||||
|
(self.flags.0 & OptFlag::HELP.0) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub(crate) const fn is_short_visible(&self) -> bool {
|
||||||
|
(self.flags.0 & OptFlag::VISIBLE_SHORT.0) != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
pub(crate) const fn is_full_visible(&self) -> bool {
|
||||||
|
(self.flags.0 & OptFlag::VISIBLE_FULL.0) != 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
impl<ID: 'static> Opt<ID> {
|
||||||
|
/// Get the first name of the option.
|
||||||
|
pub const fn first_name(&self) -> &str {
|
||||||
|
match self.names {
|
||||||
|
OptIdentifier::Single(name) => name,
|
||||||
|
OptIdentifier::Multi(names) => names.first().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first long option name, if one exists.
|
||||||
|
pub const fn first_long_name(&self) -> Option<&'static str> {
|
||||||
|
match self.names {
|
||||||
|
OptIdentifier::Single(name) => if name.len() >= 3 { Some(name) } else { None },
|
||||||
|
// Can be replaced with `find_map` once iterators are const fn
|
||||||
|
OptIdentifier::Multi(names) => {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < names.len() {
|
||||||
|
if const_utf8::CharIterator::from(names[i]).count() >= 3 {
|
||||||
|
return Some(names[i]);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first short option name, if one exists.
|
||||||
|
pub(crate) const fn first_short_name(&self) -> Option<&'static str> {
|
||||||
|
const fn predicate(name: &str) -> bool {
|
||||||
|
let mut chars = const_utf8::CharIterator::from(name);
|
||||||
|
if let Some(first) = chars.next() {
|
||||||
|
if let Some(c) = chars.next() {
|
||||||
|
if c != first && chars.next().is_none() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
match self.names {
|
||||||
|
OptIdentifier::Single(name) => if predicate(name) { Some(name) } else { None },
|
||||||
|
// Can be replaced with `find_map` once iterators are const fn
|
||||||
|
OptIdentifier::Multi(names) => {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < names.len() {
|
||||||
|
if predicate(names[i]) {
|
||||||
|
return Some(names[i]);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first applicable short option's flag character, if one exists.
|
||||||
|
pub(crate) const fn first_short_name_char(&self) -> Option<char> {
|
||||||
|
const fn predicate(name: &str) -> Option<char> {
|
||||||
|
let mut chars = const_utf8::CharIterator::from(name);
|
||||||
|
if let Some(first) = chars.next() {
|
||||||
|
if let Some(c) = chars.next() {
|
||||||
|
if c != first && chars.next().is_none() {
|
||||||
|
return Some(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
match self.names {
|
||||||
|
OptIdentifier::Single(name) => predicate(name),
|
||||||
|
// Can be replaced with `find_map` once iterators are const fn.
|
||||||
|
OptIdentifier::Multi(names) => {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < names.len() {
|
||||||
|
if let Some(c) = predicate(names[i]) {
|
||||||
|
return Some(c);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Search for a matching name in the option, offset allows to skip the first `n = offset` characters in the comparison.
|
||||||
|
pub(crate) fn match_name(&self, string: &str, offset: usize) -> Option<&'static str> {
|
||||||
|
let rhs = &string[offset..];
|
||||||
|
if rhs.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
match self.names {
|
||||||
|
OptIdentifier::Single(name) =>
|
||||||
|
if &name[offset..] == rhs { Some(name) } else { None },
|
||||||
|
OptIdentifier::Multi(names) =>
|
||||||
|
names.iter().find(|name| &name[offset..] == rhs).map(|v| &**v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::ops::BitOr for OptFlag {
|
||||||
|
type Output = Self;
|
||||||
|
fn bitor(self, rhs: Self) -> Self::Output { Self(self.0 | rhs.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Option names cannot be an empty slice")]
|
||||||
|
fn test_new_empty_names_disallowed() {
|
||||||
|
Opt::new((), OptIdentifier::Multi(&[]), None, OptType::Positional);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_public_initialisers() {
|
||||||
|
assert_eq!(Opt::positional((), "name"), Opt { id: (),
|
||||||
|
names: OptIdentifier::Single("name"), value_name: None, help_string: None,
|
||||||
|
r#type: OptType::Positional, flags: OptFlag::DEFAULT,
|
||||||
|
});
|
||||||
|
assert_eq!(Opt::help_flag((), &["name"]), Opt { id: (),
|
||||||
|
names: OptIdentifier::Multi(&["name"]), value_name: None, help_string: None,
|
||||||
|
r#type: OptType::Flag, flags: OptFlag::DEFAULT | OptFlag::HELP,
|
||||||
|
});
|
||||||
|
assert_eq!(Opt::flag((), &["name"]), Opt { id: (),
|
||||||
|
names: OptIdentifier::Multi(&["name"]), value_name: None, help_string: None,
|
||||||
|
r#type: OptType::Flag, flags: OptFlag::DEFAULT,
|
||||||
|
});
|
||||||
|
assert_eq!(Opt::value((), &["name"], "value"), Opt { id: (),
|
||||||
|
names: OptIdentifier::Multi(&["name"]), value_name: Some("value"), help_string: None,
|
||||||
|
r#type: OptType::Value, flags: OptFlag::DEFAULT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_valid_with_chains() {
|
||||||
|
assert_eq!(Opt::positional((), "").required(), Opt { id: (),
|
||||||
|
names: OptIdentifier::Single(""), value_name: None, help_string: None,
|
||||||
|
r#type: OptType::Positional, flags: OptFlag::DEFAULT | OptFlag::REQUIRED,
|
||||||
|
});
|
||||||
|
assert_eq!(Opt::positional((), "").required().help_text("help string"), Opt { id: (),
|
||||||
|
names: OptIdentifier::Single(""), value_name: None, help_string: Some("help string"),
|
||||||
|
r#type: OptType::Positional, flags: OptFlag::DEFAULT | OptFlag::REQUIRED,
|
||||||
|
});
|
||||||
|
assert_eq!(Opt::positional((), "").help_text("help string"), Opt { id: (),
|
||||||
|
names: OptIdentifier::Single(""), value_name: None, help_string: Some("help string"),
|
||||||
|
r#type: OptType::Positional, flags: OptFlag::DEFAULT,
|
||||||
|
});
|
||||||
|
assert_eq!(Opt::positional((), "").hide_usage(OptHide::Short), Opt { id: (),
|
||||||
|
names: OptIdentifier::Single(""), value_name: None, help_string: None,
|
||||||
|
r#type: OptType::Positional, flags: OptFlag::VISIBLE_FULL,
|
||||||
|
});
|
||||||
|
assert_eq!(Opt::positional((), "").hide_usage(OptHide::Full), Opt { id: (),
|
||||||
|
names: OptIdentifier::Single(""), value_name: None, help_string: None,
|
||||||
|
r#type: OptType::Positional, flags: OptFlag::VISIBLE_SHORT,
|
||||||
|
});
|
||||||
|
assert_eq!(Opt::positional((), "").hide_usage(OptHide::All), Opt { id: (),
|
||||||
|
names: OptIdentifier::Single(""), value_name: None, help_string: None,
|
||||||
|
r#type: OptType::Positional, flags: OptFlag::NONE,
|
||||||
|
});
|
||||||
|
assert_eq!(Opt::positional((), "").required().hide_usage(OptHide::All), Opt { id: (),
|
||||||
|
names: OptIdentifier::Single(""), value_name: None, help_string: None,
|
||||||
|
r#type: OptType::Positional, flags: OptFlag::REQUIRED,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Help flag cannot be made required")]
|
||||||
|
fn test_required_help_disallowed() {
|
||||||
|
Opt::help_flag((), &["-h", "--help"]).required();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Only flags are allowed to be help options")]
|
||||||
|
fn test_positional_with_help_flag_disallowed() {
|
||||||
|
Opt::positional((), "").with_help_flag();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "Only flags are allowed to be help options")]
|
||||||
|
fn test_value_with_help_flag_disallowed() {
|
||||||
|
Opt::value((), &[""], "").with_help_flag();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_flag_getters() {
|
||||||
|
const HELP: Opt<()> = Opt::help_flag((), &[""]);
|
||||||
|
const REQUIRED: Opt<()> = Opt::positional((), "").required();
|
||||||
|
assert!(HELP.is_help());
|
||||||
|
assert!(!HELP.is_required());
|
||||||
|
assert!(REQUIRED.is_required());
|
||||||
|
assert!(!REQUIRED.is_help());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_first_name() {
|
||||||
|
assert_eq!(Opt::positional((), "first").first_name(), "first");
|
||||||
|
assert_eq!(Opt::flag((), &["first", "second"]).first_name(), "first");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_first_long_name() {
|
||||||
|
assert_eq!(Opt::positional((), "--long").first_long_name(), Some("--long"));
|
||||||
|
assert_eq!(Opt::positional((), "-long").first_long_name(), Some("-long"));
|
||||||
|
assert_eq!(Opt::positional((), "--l").first_long_name(), Some("--l"));
|
||||||
|
assert_eq!(Opt::positional((), "-s").first_long_name(), None);
|
||||||
|
assert_eq!(Opt::flag((), &["-s", "--long"]).first_long_name(), Some("--long"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_first_short_name() {
|
||||||
|
assert_eq!(Opt::positional((), "-s").first_short_name(), Some("-s"));
|
||||||
|
assert_eq!(Opt::positional((), "--long").first_short_name(), None);
|
||||||
|
assert_eq!(Opt::positional((), "--").first_short_name(), None);
|
||||||
|
assert_eq!(Opt::positional((), "-lo").first_short_name(), None);
|
||||||
|
assert_eq!(Opt::positional((), "--l").first_short_name(), None);
|
||||||
|
assert_eq!(Opt::flag((), &["--long", "-s"]).first_short_name(), Some("-s"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_first_short_name_char() {
|
||||||
|
assert_eq!(Opt::positional((), "-s").first_short_name_char(), Some('s'));
|
||||||
|
assert_eq!(Opt::positional((), "--long").first_short_name_char(), None);
|
||||||
|
assert_eq!(Opt::positional((), "--").first_short_name_char(), None);
|
||||||
|
assert_eq!(Opt::positional((), "-lo").first_short_name_char(), None);
|
||||||
|
assert_eq!(Opt::positional((), "--l").first_short_name_char(), None);
|
||||||
|
assert_eq!(Opt::flag((), &["--long", "-s"]).first_short_name_char(), Some('s'));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_match_name() {
|
||||||
|
assert_eq!(Opt::flag((), &["--one", "--two", "--threee", "--three"])
|
||||||
|
.match_name("--three", 0), Some("--three"));
|
||||||
|
assert_eq!(Opt::flag((), &["--one", "--two", "--threee"])
|
||||||
|
.match_name("--three", 0), None);
|
||||||
|
assert_eq!(Opt::flag((), &["/one", "/two", "/three", "/four"])
|
||||||
|
.match_name("-three", 1), Some("/three"));
|
||||||
|
assert_eq!(Opt::positional((), "-s").match_name("-s", 1), Some("-s"));
|
||||||
|
|
||||||
|
assert_eq!(Opt::flag((), &["-x", "-s"]).match_name("-s", 2), None);
|
||||||
|
assert_eq!(Opt::positional((), "-x").match_name("-s", 2), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
126
jaarg/src/options.rs
Normal file
126
jaarg/src/options.rs
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/* jaarg - Argument parser
|
||||||
|
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||||
|
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::{ordered_bitset, Opt};
|
||||||
|
use crate::option::OptType;
|
||||||
|
|
||||||
|
/// Static structure that contains instructions for parsing command-line arguments.
|
||||||
|
#[derive(Debug, PartialEq)]
|
||||||
|
pub struct Opts<ID: 'static> {
|
||||||
|
/// List of options
|
||||||
|
pub(crate) options: &'static[Opt<ID>],
|
||||||
|
/// String containing single characters that match option prefixes
|
||||||
|
pub(crate) flag_chars: &'static str,
|
||||||
|
/// A description of what the program does
|
||||||
|
pub(crate) description: Option<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type RequiredParamsBitSet = ordered_bitset::OrderedBitSet<u32, 4>;
|
||||||
|
|
||||||
|
/// The maximum amount of allowed required non-positional options.
|
||||||
|
pub const MAX_REQUIRED_OPTIONS: usize = RequiredParamsBitSet::CAPACITY;
|
||||||
|
|
||||||
|
impl<ID: 'static> Opts<ID> {
|
||||||
|
/// Build argument parser options with the default flag character of '-'.
|
||||||
|
pub const fn new(options: &'static[Opt<ID>]) -> Self {
|
||||||
|
// Validate passed options
|
||||||
|
let mut opt_idx = 0;
|
||||||
|
let mut num_required_parameters = 0;
|
||||||
|
while opt_idx < options.len() {
|
||||||
|
if matches!(options[opt_idx].r#type, OptType::Flag | OptType::Value) && options[opt_idx].is_required() {
|
||||||
|
num_required_parameters += 1;
|
||||||
|
}
|
||||||
|
opt_idx += 1;
|
||||||
|
}
|
||||||
|
assert!(num_required_parameters <= RequiredParamsBitSet::CAPACITY,
|
||||||
|
"More than 128 non-positional required option entries is not supported at this time");
|
||||||
|
|
||||||
|
Self {
|
||||||
|
options,
|
||||||
|
flag_chars: "-",
|
||||||
|
description: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the recognised flag/option characters.
|
||||||
|
#[inline]
|
||||||
|
pub const fn with_flag_chars(mut self, flag_chars: &'static str) -> Self {
|
||||||
|
self.flag_chars = flag_chars;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the description of the program, available to help writers.
|
||||||
|
#[inline]
|
||||||
|
pub const fn with_description(mut self, description: &'static str) -> Self {
|
||||||
|
self.description = Some(description);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the first available help option if one exists.
|
||||||
|
pub const fn help_option(&self) -> Option<&'static Opt<ID>> {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < self.options.len() {
|
||||||
|
if self.options[i].is_help() {
|
||||||
|
return Some(&self.options[i]);
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets an iterator over the parser's options.
|
||||||
|
#[inline]
|
||||||
|
pub fn iter(&self) -> core::slice::Iter<'static, Opt<ID>> {
|
||||||
|
self.options.iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(unused)]
|
||||||
|
fn test_required_opt_limit() {
|
||||||
|
const NUM_OPTS: usize = MAX_REQUIRED_OPTIONS + 2;
|
||||||
|
const OPT_LIST: [Opt<()>; NUM_OPTS] = {
|
||||||
|
const REQUIRED: Opt<()> = Opt::flag((), &[""]).required();
|
||||||
|
let mut array: [Opt<()>; NUM_OPTS] = [REQUIRED; NUM_OPTS];
|
||||||
|
array[0] = Opt::help_flag((), &[""]);
|
||||||
|
array[NUM_OPTS - 1] = Opt::positional((), "");
|
||||||
|
array
|
||||||
|
};
|
||||||
|
const OPTIONS: Opts<()> = Opts::new(&OPT_LIST);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_with_chains() {
|
||||||
|
assert_eq!(Opts::<()>::new(&[]).with_flag_chars("-/"),
|
||||||
|
Opts { options: &[], flag_chars: "-/", description: None });
|
||||||
|
assert_eq!(Opts::<()>::new(&[]).with_description("test description"),
|
||||||
|
Opts { options: &[], flag_chars: "-", description: Some("test description") });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_help_option() {
|
||||||
|
const OPTS1: Opts<()> = Opts::new(&[
|
||||||
|
Opt::flag((), &[""]),
|
||||||
|
Opt::flag((), &[""]),
|
||||||
|
Opt::positional((), ""),
|
||||||
|
Opt::positional((), ""),
|
||||||
|
Opt::help_flag((), &["--help"]),
|
||||||
|
Opt::value((), &[""], ""),
|
||||||
|
Opt::help_flag((), &[""]),
|
||||||
|
]);
|
||||||
|
const OPTS2: Opts<()> = Opts::new(&[
|
||||||
|
Opt::flag((), &[""]),
|
||||||
|
Opt::positional((), ""),
|
||||||
|
Opt::value((), &[""], ""),
|
||||||
|
]);
|
||||||
|
assert_eq!(OPTS1.help_option(), Some(&Opt::help_flag((), &["--help"])));
|
||||||
|
assert_eq!(OPTS2.help_option(), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/* jaarg - Argument parser
|
/* jaarg - Argument parser
|
||||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
||||||
* SPDX-License-Identifier: MIT
|
* SPDX-License-Identifier: MIT OR Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#![allow(private_bounds)]
|
#![allow(private_bounds)]
|
||||||
78
jaarg/src/std.rs
Normal file
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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
17
src/lib.rs
17
src/lib.rs
@@ -1,17 +0,0 @@
|
|||||||
/* jaarg - Argument parser
|
|
||||||
* SPDX-FileCopyrightText: (C) 2025 Gay Pizza Specifications
|
|
||||||
* SPDX-License-Identifier: MIT
|
|
||||||
*/
|
|
||||||
|
|
||||||
#![no_std]
|
|
||||||
|
|
||||||
mod const_utf8;
|
|
||||||
mod ordered_bitset;
|
|
||||||
|
|
||||||
include!("option.rs");
|
|
||||||
include!("options.rs");
|
|
||||||
include!("argparse.rs");
|
|
||||||
include!("help.rs");
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub mod std;
|
|
||||||
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