arg_kit/lib.rs
1#![doc(html_logo_url = "https://64-tesseract.ftp.sh/tesseract.gif", html_favicon_url = "https://64-tesseract.ftp.sh/tesseract.gif")]
2
3//! A featherweight toolkit to help iterate over long and short commandline
4//! arguments concisely. It's a comfy middleground between bloated libraries
5//! like `clap` and painstakingly hacking something together yourself for each
6//! new project.
7//!
8//! It doesn't handle taking positional arguments, but you can manually consume
9//! later values in order of appearance. Do you really need bloated proc macros
10//! when collecting arguments can be simplified to a `.next()`? You have zero
11//! indication of what's going on under the hood, so you can't implement your
12//! own behaviour.
13//!
14//! All the functionality boils down to `(&str).as_arg()`, which checks if the
15//! string begins with `-` or `--`, and returns an appropriate iterator for
16//! either a single long argument or each grouped short argument.
17//! Putting it in practice, the most basic code looks like this:
18//!
19//! ```rust,ignore
20//! let mut argv = std::env::args();
21//! // Skip executable
22//! argv.next();
23//! // For each separate argument
24//! while let Some(args) = argv.next() {
25//! // For the single long argument or each combined short argument
26//! for arg in args.as_arg() {
27//! match arg {
28//! Argument::Long("help") | Argument::Short("h") => {
29//! eprintln!("{HELP_TEXT}");
30//! },
31//! Argument::Long("value") | Argument::Short("v") => {
32//! let value: isize = argv.next()?.parse()?;
33//! do_something(value);
34//! },
35//! unknown => {
36//! eprintln!("Unknown argument {unknown}");
37//! }
38//! }
39//! }
40//! }
41//! ```
42//!
43//! Note that in this case it is best to use `while let Some(var) = iter.next()`
44//! over `for var in iter`. This is because `for` actually takes ownership of
45//! the iterator, so you can't go to the next argument in the match body to grab
46//! a positional value with a simple `iter.next().unwrap()`.
47//!
48//! ---
49//!
50//! That's still a lot of boilerplate. To make it a bit narrower, [`Argument`]s
51//! can be constructed with the [`arg`] macro:
52//!
53//! ```rust,ignore
54//! assert_eq!(Argument::Long("long"), arg!(--long));
55//! assert_eq!(Argument::Short("s"), arg!(-s));
56//! assert_eq!(Argument::Bare("other"), arg!("other"));
57//!
58//! match arg {
59//! arg!(--help) | arg!(-h) => {
60//! ...
61//! },
62//! arg!(--a-long-argument) => {
63//! ...
64//! },
65//! _ => {},
66//! }
67//! ```
68//!
69//! Usually you match both one long and short argument at once, so you can also
70//! combine exactly one of each in `arg!(--help | -h)` or `arg!(-h | --help)`.
71//!
72//! ---
73//!
74//! There's still a few layers of indentation that can be cut down on though,
75//! since they would rarely change. You can replace the `for` and `match` with a
76//! single [`match_arg`]:
77//!
78//! ```rust,ignore
79//! let mut argv = std::env::args();
80//! // Skip executable
81//! argv.next();
82//! while let Some(args) = argv.next() {
83//! match_arg!(args; {
84//! arg!(-h | --help) => {
85//! ...
86//! },
87//! _ => {},
88//! });
89//! }
90//! ```
91//!
92//! Or if you don't need any control between the `while` and `for`, you can cut
93//! right to the meat of the logic and opt for [`for_args`]:
94//!
95//! ```rust,ignore
96//! let mut argv = std::env::args();
97//! // Skip executable
98//! argv.next();
99//! for_args!(argv; {
100//! arg!(-h | --help) => {
101//! ...
102//! },
103//! _ => {},
104//! });
105//! ```
106//!
107//! Note that if you need to match (secret?) arguments with spaces, funky
108//! characters Rust doesn't recognize as part of an identifier (excluding dashes
109//! which there's an edge case for), or if you want to match arbitrary long,
110//! short, or bare arguments separately, you'll need to manually construct
111//! `Argument::Long(var)`, `Argument::Short(var)`, or `Argument::Bare(var)`.
112//!
113//! If you're wondering why the `Short` variant carries a `&str` rather than a
114//! `char`, it's to make it possible to bind them to the same `match` variable,
115//! and also because macros in [`arg`] can't convert `ident`s into a `char`.
116//! If you accidentally try matching `arg!(-abc)`, _it will silently fail,_ and
117//! there can't be any checks for it. Blame Rust I guess.
118
119
120use std::{ iter, fmt, };
121
122
123/// Quick way to contruct [`Argument`]s. Supports long and short arguments by
124/// prepending one or two dashes, or "bare" arguments that aren't classified as
125/// either long or short.
126///
127/// Long arguments support anything Rust considers an "identifier" (variable
128/// name), separated by at most one dash. For example `arg!(--long)` or
129/// `arg!(--long-arg)` are valid, but `arg!(--long--arg)` isn't.
130///
131/// Short arguments must only be one character long (eg. `arg!(-a)`). This
132/// requirement _isn't checked_ due to limitations of `macro_rules`, and I'm not
133/// writing an entire proc macro crate for one shortcut.
134/// If you use more than one character, it won't ever be matched.
135///
136/// In a match block, you can "or" exactly one long and short argument like
137/// `arg!(-h | --help)`, which is equivalent to `arg!(-h) | arg!(--help)`.
138///
139/// Bare arguments (anything not valid as a long or short) must be quoted, for
140/// example `arg!("bare-argument")`. Putting one or two dashes in front of the
141/// argument will never match as it'd be recognized as a valid argument, but
142/// three would be parsed as a bare (eg. `arg!("---lol")`).
143///
144/// The `arg!(+...)` syntax is used internally to combine Rust identifiers
145/// separated by dashes.
146#[macro_export]
147macro_rules! arg {
148 // Long and short arguments together
149 (-- $($long:ident)-+ | - $short:ident) => {
150 arg!(-- $($long)-+) | arg!(- $short)
151 };
152 (- $short:ident | -- $($long:ident)-+) => {
153 arg!(- $short) | arg!(-- $($long)-+)
154 };
155
156 // Long argument
157 (-- $($a:ident)-+) => {
158 $crate::Argument::Long(arg!(+ $($a)-+))
159 };
160
161 // Short argument
162 (- $a:ident) => {
163 $crate::Argument::Short(stringify!($a))
164 };
165
166 // Anything else to match to
167 ($a:literal) => {
168 $crate::Argument::Bare($a)
169 };
170
171 // If long argument contains dashes, combine them using `arg!(+...)`
172 (+ $first:ident) => {
173 stringify!($first)
174 };
175 (+ $first:ident - $($next:ident)-+) => {
176 concat!(stringify!($first), "-", arg!(+ $($next)-+))
177 };
178}
179
180/// Matches each part of a single argument - once if it's a long argument (eg.
181/// `--help`), or for each character of combined short arguments (eg. `-abc`).
182///
183/// Expands to:
184///
185/// ```rust,ignore
186/// for arg in $str.as_arg() {
187/// match arg { $match }
188/// }
189/// ```
190#[macro_export]
191macro_rules! match_arg {
192 ($str:expr; $match:tt) => {
193 for __ak_arg in $str.as_arg() {
194 match __ak_arg $match
195 }
196 }
197}
198
199/// Matches each part of each argument in a `&str` iterator (like `env::args`),
200/// using the [`match_arg`] macro.
201///
202/// Expands to:
203///
204/// ```rust,ignore
205/// while let Some(args) in $iter.next() {
206/// for arg in args.as_arg() {
207/// match arg { $match }
208/// }
209/// }
210/// ```
211#[macro_export]
212macro_rules! for_args {
213 ($iter:expr; $match:tt) => {
214 while let Some(__ak_args) = $iter.next() {
215 $crate::match_arg!(__ak_args; $match)
216 }
217 }
218}
219
220
221/// A single argument that can be matched from an [`ArgumentIterator`].
222#[derive(Clone, Eq, PartialEq, Debug)]
223pub enum Argument<'a> {
224 /// A long argument without the leading dashes.
225 /// `Argument::Long("help") == arg!(--help)`
226 Long(&'a str),
227 /// A short argument without the leading dash.
228 /// `Argument::Short("h") == arg!(-h)`
229 ///
230 /// Important to note that it must be only 1 character long or it
231 /// will never match, but for ergonimic reasons it is actually a `&str`.
232 Short(&'a str),
233 /// Raw string of anything else passed as an argument, whether it has zero
234 /// or three dashes. Manually constructing an invalid bare argument with leading
235 /// dashes won't be matched properly.
236 /// `Argument::Bare("---yeet") == arg!("---yeet")`
237 ///
238 /// Exists as an alternative to putting parsed arguments in a `Result`.
239 Bare(&'a str),
240}
241
242impl fmt::Display for Argument<'_> {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 match self {
245 Self::Long(long) => write!(f, "--{long}"),
246 Self::Short(short) => write!(f, "-{short}"),
247 Self::Bare(bare) => write!(f, "{bare}"),
248 }
249 }
250}
251
252impl Argument<'_> {
253 pub fn is_long(&self) -> bool {
254 matches!(self, Self::Long(_))
255 }
256
257 pub fn is_short(&self) -> bool {
258 matches!(self, Self::Short(_))
259 }
260
261 pub fn is_bare(&self) -> bool {
262 matches!(self, Self::Bare(_))
263 }
264}
265
266
267/// An iterator over a string's arguments - equivalent to an `iter::once(&str)`
268/// if the argument is long or bare, or `str::chars()` for each combined short
269/// argument (except returning `&str`s for ergonomic reasons).
270///
271/// Constructed with `(&str).as_arg()` (see [`AsArgumentIterator`]).
272#[derive(Clone)]
273pub struct ArgumentIterator<'a>(ArgumentIteratorState<'a>);
274
275/// Private state of an iterator. Implementation is horrible, please don't look
276/// at it.
277#[derive(Clone)]
278enum ArgumentIteratorState<'a> {
279 Long(Option<&'a str>),
280 Short(&'a str),
281 Bare(Option<&'a str>),
282}
283
284impl<'a> iter::Iterator for ArgumentIterator<'a> {
285 type Item = Argument<'a>;
286
287 fn next(&mut self) -> Option<Self::Item> {
288 match self.0 {
289 ArgumentIteratorState::Long(ref mut long) => Some(Argument::Long(long.take()?)),
290 ArgumentIteratorState::Short(ref mut short) => {
291 let (one, rest) = short.split_at_checked(1)?;
292 *short = rest;
293 Some(Argument::Short(one))
294 },
295 ArgumentIteratorState::Bare(ref mut bare) => Some(Argument::Bare(bare.take()?)),
296 }
297 }
298}
299
300
301pub trait AsArgumentIterator {
302 fn as_arg<'a>(&'a self) -> ArgumentIterator<'a>;
303}
304
305impl AsArgumentIterator for str {
306 fn as_arg<'a>(&'a self) -> ArgumentIterator<'a> {
307 if self.starts_with("---") {
308 return ArgumentIterator(ArgumentIteratorState::Bare(Some(self)));
309 }
310
311 if let Some(long) = self.strip_prefix("--") {
312 if long.is_empty() {
313 ArgumentIterator(ArgumentIteratorState::Bare(Some(self)))
314 } else {
315 ArgumentIterator(ArgumentIteratorState::Long(Some(long)))
316 }
317 } else if let Some(short) = self.strip_prefix("-") {
318 if short.is_empty() {
319 ArgumentIterator(ArgumentIteratorState::Bare(Some(self)))
320 } else {
321 ArgumentIterator(ArgumentIteratorState::Short(short))
322 }
323 } else {
324 ArgumentIterator(ArgumentIteratorState::Bare(Some(self)))
325 }
326 }
327}
328
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333
334 #[test]
335 fn long() {
336 let long_arg = "--help";
337 let mut long_iter = long_arg.as_arg();
338 assert_eq!(long_iter.next(), Some(arg!(--help)));
339 assert_eq!(long_iter.next(), None);
340 }
341
342 #[test]
343 fn short() {
344 let short_arg = "-abc";
345 let mut short_iter = short_arg.as_arg();
346 assert_eq!(short_iter.next(), Some(arg!(-a)));
347 assert_eq!(short_iter.next(), Some(arg!(-b)));
348 assert_eq!(short_iter.next(), Some(arg!(-c)));
349 assert_eq!(short_iter.next(), None);
350 }
351
352 #[test]
353 fn long_no_ident() {
354 let long_arg = "--";
355 let mut long_iter = long_arg.as_arg();
356 assert_eq!(long_iter.next(), Some(arg!("--")));
357 assert_eq!(long_iter.next(), None);
358 }
359
360 #[test]
361 fn short_no_ident() {
362 let short_arg = "-";
363 let mut short_iter = short_arg.as_arg();
364 assert_eq!(short_iter.next(), Some(arg!("-")));
365 assert_eq!(short_iter.next(), None);
366 }
367
368 #[test]
369 fn long_with_dash() {
370 let long_arg = "--abc-def";
371 let mut long_iter = long_arg.as_arg();
372 assert_eq!(long_iter.next(), Some(arg!(--abc-def)));
373 assert_eq!(long_iter.next(), None);
374 }
375
376 #[test]
377 fn no_dashes() {
378 let other_arg = "yeet";
379 let mut other_iter = other_arg.as_arg();
380 assert_eq!(other_iter.next(), Some(arg!("yeet")));
381 assert_eq!(other_iter.next(), None);
382 }
383
384 #[test]
385 fn three_dashes() {
386 let other_arg = "---yeet";
387 let mut other_iter = other_arg.as_arg();
388 assert_eq!(other_iter.next(), Some(arg!("---yeet")));
389 assert_eq!(other_iter.next(), None);
390 }
391
392 #[test]
393 fn match_long() {
394 let mut state = false;
395 match_arg!("--hello"; {
396 arg!(--hello) => {
397 assert_eq!(state, false);
398 state = true;
399 },
400 _ => panic!(),
401 });
402 }
403
404 #[test]
405 fn match_short() {
406 let mut state = 0;
407 match_arg!("-abc"; {
408 arg!(-a) => {
409 assert_eq!(state, 0);
410 state = 1;
411 },
412 arg!(-b) => {
413 assert_eq!(state, 1);
414 state = 2;
415 },
416 arg!(-c) => {
417 assert_eq!(state, 2);
418 state = 3;
419 },
420 _ => panic!(),
421 });
422 }
423
424 #[test]
425 fn for_long_short_bare() {
426 let mut args = ["-a", "not_an_arg", "--blah", "-cd"].into_iter();
427 let mut state = 0;
428 for_args!(args; {
429 arg!(-a | --blah) => {
430 state = match state {
431 0 => 1,
432 2 => 3,
433 _ => panic!(),
434 }
435 },
436 arg!("not_an_arg") | arg!(-d) => {
437 state = match state {
438 1 => 2,
439 4 => 5,
440 _ => panic!(),
441 }
442 },
443 arg!(-c) => {
444 assert_eq!(state, 3);
445 state = 4;
446 },
447 _ => panic!(),
448 });
449 }
450}