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}