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