noshell_parser/
parser.rs

1//! A parser for collecting arguments from a token stream.
2
3use core::fmt::Debug;
4use core::str::FromStr;
5
6use heapless::Vec;
7
8use crate::lexer::{Flag, IntoTokens, Token, Values};
9
10/// Defines the possible errors that may occur during parsing of arguments.
11#[derive(Debug, PartialEq, Eq, thiserror::Error)]
12#[cfg_attr(feature = "defmt", derive(defmt::Format))]
13#[non_exhaustive]
14pub enum Error {
15    /// The argument is not defined.
16    #[error("undefined argument")]
17    UndefinedArgument,
18
19    /// The argument value is invalid, meaning that it cannot be converted to the destination
20    /// type. This could mean that there is a missing implementation for [`str::parse`] trait.
21    #[error("invalid argument")]
22    InvalidArgument,
23
24    /// The argument value is missing, which occurs when the flag is not boolean and expect a
25    /// value.
26    #[error("missing argument")]
27    MissingArgument,
28
29    /// Insufficient space for parsing arguments.
30    #[error("out of parser memory space")]
31    OutOfMemory,
32}
33
34/// Defines the result of argument parsing. This is a simple key-value store that offers a look-up
35/// over parsed arguments.
36#[derive(Debug, Default)]
37#[cfg_attr(feature = "defmt", derive(defmt::Format))]
38pub struct ParsedArgs<'a, const ARG_COUNT_MAX: usize = 8> {
39    args: Vec<(&'a str, Values<'a>), ARG_COUNT_MAX>,
40}
41
42impl<'a, const SIZE: usize> ParsedArgs<'a, SIZE> {
43    /// Parse the command line input from a token stream. The result is the set of found arguments.
44    pub fn parse(argv: impl IntoTokens<'a>, ids: &[(Flag<'a>, &'a str)]) -> Self {
45        Self::try_parse(argv, ids).expect("cannot parse arguments")
46    }
47
48    /// Try to parse the input arguments.
49    pub fn try_parse(
50        argv: impl IntoTokens<'a>,
51        ids: &[(Flag<'a>, &'a str)],
52    ) -> Result<Self, Error> {
53        let mut tokens = argv.into_tokens();
54
55        let mut out = Self::default();
56
57        while let Some(token) = tokens.next() {
58            if let Token::Flag(f) = &token {
59                let id = if let Some((_, id)) = ids.iter().find(|x| f == &x.0) {
60                    id
61                } else {
62                    return Err(Error::UndefinedArgument);
63                };
64
65                if out.args.push((id, tokens.values())).is_err() {
66                    return Err(Error::OutOfMemory);
67                }
68            }
69        }
70
71        Ok(out)
72    }
73
74    /// Check if there exists an argument with the given key (i.e. short or long flag).
75    #[inline(always)]
76    pub fn contains(&self, id: &str) -> bool {
77        self.args.iter().any(|x| id == x.0)
78    }
79
80    /// Get one value for the given flag identifier.
81    pub fn get_one<T>(&self, id: &str) -> Option<Option<T>>
82    where
83        T: FromStr,
84    {
85        self.try_get_one::<T>(id).expect("invalid argument")
86    }
87
88    /// Get many values for the given flag identifier.
89    pub fn get_many<B, T>(&self, id: &str) -> Option<B>
90    where
91        B: FromIterator<T>,
92        T: FromStr,
93    {
94        self.try_get_many::<B, T>(id).expect("invalid argument")
95    }
96
97    /// Try to get and parse the argument value if any.
98    pub fn try_get_one<T>(&self, id: &str) -> Result<Option<Option<T>>, Error>
99    where
100        T: FromStr,
101    {
102        if let Some((_, values)) = self.args.iter().find(|x| id == x.0) {
103            let mut iter = values.clone();
104
105            let value = if let Some(value) = iter.next() {
106                value
107            } else {
108                // The argument has no value.
109                return Ok(Some(None));
110            };
111
112            if iter.next().is_some() {
113                // The argument has more than one value.
114                return Err(Error::InvalidArgument);
115            }
116
117            return value
118                .parse::<T>()
119                // The argument is present and has a value (i.e. Some(Some(_))).
120                .map(Some)
121                .map(Some)
122                // The value cannot be parsed to the target type `T`.
123                .map_err(|_| Error::InvalidArgument);
124        }
125
126        // The argument has not been found.
127        Ok(None)
128    }
129
130    /// Try to get and parse the argument value if any. The value can be constructed from
131    /// an iterator.
132    pub fn try_get_many<B, T>(&self, id: &str) -> Result<Option<B>, Error>
133    where
134        B: FromIterator<T>,
135        T: FromStr,
136    {
137        if let Some((_, values)) = self.args.iter().find(|x| x.0 == id) {
138            let iter = values.clone();
139
140            // Collect on Seq<Result<T, _>> can be coerced to Result<Seq<T>, _>.
141            let result: Result<B, _> = iter.map(|x| x.parse::<T>()).collect();
142
143            // The value cannot be parsed to the target type `T`.
144            return result.map(Some).map_err(|_| Error::InvalidArgument);
145        }
146
147        Ok(None)
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use googletest::prelude::*;
154
155    use crate::lexer::Tokens;
156
157    use super::*;
158
159    #[test]
160    fn it_should_parse_missing_arg() {
161        let ids = &[(Flag::Short('v'), "verbose")];
162        let tokens = Tokens::new(&["-v"]);
163
164        let args: ParsedArgs<'_, 1> = ParsedArgs::parse(tokens, ids);
165
166        assert_that!(args.try_get_one::<u32>("verbose"), eq(&Ok(Some(None))));
167    }
168
169    #[test]
170    fn it_should_parse_invalid_arg() {
171        let ids = &[(Flag::Short('v'), "verbose")];
172        let tokens = Tokens::new(&["-v", "-42"]);
173
174        let args: ParsedArgs<'_, 1> = ParsedArgs::parse(tokens, ids);
175
176        assert_that!(
177            args.try_get_one::<u32>("verbose"),
178            eq(&Err(Error::InvalidArgument))
179        );
180    }
181
182    #[test]
183    fn it_should_parse_valid_value() {
184        let ids = &[(Flag::Short('v'), "verbose")];
185        let tokens = Tokens::new(&["-v", "42"]);
186
187        let args: ParsedArgs<'_, 1> = ParsedArgs::parse(tokens, ids);
188
189        assert_that!(
190            args.try_get_one::<u32>("verbose"),
191            matches_pattern!(&Ok(Some(Some(42))))
192        );
193    }
194}