miniarg/
split_args.rs

1//! Splits a cmdline into multiple args.
2//!
3//! # Usage
4//!
5//! ```
6//! # use miniarg::split_args::SplitArgs;
7//! let mut args = SplitArgs::new("executable param1 \"param2, but with spaces\" param3");
8//! assert_eq!(args.next(), Some("executable"));
9//! assert_eq!(args.next(), Some("param1"));
10//! assert_eq!(args.next(), Some("param2, but with spaces"));
11//! assert_eq!(args.next(), Some("param3"));
12//! assert_eq!(args.next(), None);
13//! ```
14//!
15//! It never panics or errors.
16
17use core::iter::FusedIterator;
18
19use crate::parse::{Char, Quote, StrChars, StrIndex, StrRange};
20
21/// Splits a cmdline into multiple args.
22///
23/// See the [module documentation] for more details.
24///
25/// [module documentation]: index.html
26pub struct SplitArgs<'a> {
27    iter: StrChars<'a>,
28}
29
30impl<'a> SplitArgs<'a> {
31    /// Creates from a cmdline.
32    ///
33    /// See the [module documentation] for more details.
34    ///
35    /// [module documentation]: index.html
36    #[must_use]
37    pub const fn new(cmdline: &'a str) -> Self {
38        Self {
39            iter: StrChars::new(cmdline),
40        }
41    }
42
43    /// Get the substring `start..end`.
44    ///
45    /// # Panics
46    ///
47    /// If `start..end` is not valid.
48    fn get_range(&self, start: StrIndex, end: StrIndex) -> &'a str {
49        let range = StrRange { start, end };
50        range.get(self.iter.get()).expect("range should be valid")
51    }
52}
53
54impl<'a> Iterator for SplitArgs<'a> {
55    type Item = &'a str;
56
57    fn next(&mut self) -> Option<Self::Item> {
58        loop {
59            let c = self.iter.peek()?;
60
61            match c {
62                Char::Whitespace => {
63                    self.iter.advance();
64                }
65
66                Char::Letter(_) => {
67                    let start = self.iter.pos();
68                    self.iter.advance();
69
70                    while let Some(c) = self.iter.peek() {
71                        match c {
72                            Char::Letter(_) | Char::Quote(_) => {
73                                self.iter.advance();
74                            }
75
76                            Char::Whitespace => {
77                                let end = self.iter.pos();
78                                self.iter.advance();
79
80                                // SAFETY: `start` and `end` are obtained via
81                                //         the iterator, so they must be valid.
82                                return Some(self.get_range(start, end));
83                            }
84                        }
85                    }
86
87                    // SAFETY: `start` was obtained via the iterator, so this
88                    //         range must be valid.
89                    return Some(self.get_range(start, self.iter.pos()));
90                }
91
92                Char::Quote(Quote::Single) => {
93                    self.iter.advance();
94                    let start = self.iter.pos();
95
96                    while let Some(c) = self.iter.peek() {
97                        match c {
98                            Char::Letter(_) | Char::Whitespace | Char::Quote(Quote::Double) => {
99                                self.iter.advance();
100                            }
101
102                            Char::Quote(Quote::Single) => {
103                                let end = self.iter.pos();
104                                self.iter.advance();
105
106                                // SAFETY: `start` and `end` are obtained via
107                                //         the iterator, so they must be valid.
108                                return Some(self.get_range(start, end));
109                            }
110                        }
111                    }
112
113                    // SAFETY: `start` was obtained via the iterator, so this
114                    //         range must be valid.
115                    return Some(self.get_range(start, self.iter.pos()));
116                }
117
118                Char::Quote(Quote::Double) => {
119                    self.iter.advance();
120                    let start = self.iter.pos();
121
122                    while let Some(c) = self.iter.peek() {
123                        match c {
124                            Char::Letter(_) | Char::Whitespace | Char::Quote(Quote::Single) => {
125                                self.iter.advance();
126                            }
127
128                            Char::Quote(Quote::Double) => {
129                                let end = self.iter.pos();
130                                self.iter.advance();
131
132                                // SAFETY: `start` and `end` are obtained via
133                                //         the iterator, so they must be valid.
134                                return Some(self.get_range(start, end));
135                            }
136                        }
137                    }
138
139                    // SAFETY: `start` was obtained via the iterator, so this
140                    //         range must be valid.
141                    return Some(self.get_range(start, self.iter.pos()));
142                }
143            }
144        }
145    }
146}
147
148impl FusedIterator for SplitArgs<'_> {}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    macro_rules! test {
155        ($test:ident: $cmdline:expr => [ $($arg:expr),* ]) => {
156            #[test]
157            fn $test() {
158                let mut parsed = SplitArgs::new($cmdline);
159                $(
160                    assert_eq!(parsed.next(), Some($arg));
161                )*
162                assert_eq!(parsed.next(), None);
163            }
164        };
165    }
166
167    test!(basic: "string" => ["string"]);
168    test!(two: "string1 string2" => ["string1", "string2"]);
169    test!(single_quotes: "string1 'string2 string3' string4" => ["string1", "string2 string3", "string4"]);
170    test!(double_quotes: "string1 \"string2 string3\" string4" => ["string1", "string2 string3", "string4"]);
171    test!(single_quotes_two: "'1 2' '3 4'" => ["1 2", "3 4"]);
172    test!(double_quotes_two: "\"1 2\" \"3 4\"" => ["1 2", "3 4"]);
173    test!(unterminated_single_quotes: "1 \"2 3 4" => ["1", "2 3 4"]);
174    test!(unterminated_double_quotes: "1 '2 3 4" => ["1", "2 3 4"]);
175    test!(other_whitespace: "1\t2\n3 4\r5" => ["1", "2", "3", "4", "5"]);
176    test!(non_ascii: "string1 rusty🦀 party🎉time string2" => ["string1", "rusty🦀", "party🎉time", "string2"]);
177    test!(non_ascii_basic: "strÄng" => ["strÄng"]);
178    test!(non_ascii_two: "sträng1 sträng2" => ["sträng1", "sträng2"]);
179    test!(non_acsii_quotes: "\"sträng1 sträng2\"" => ["sträng1 sträng2"]);
180}