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}