embedded_cli/
arguments.rs

1use crate::token::{Tokens, TokensIter};
2
3#[derive(Debug, Eq, PartialEq)]
4pub enum Arg<'a> {
5    /// Used to represent `--`.
6    /// After double dash all other args
7    /// will always be `ArgToken::Value`
8    DoubleDash,
9
10    /// Long option. Only name is stored (without `--`)
11    ///
12    /// In `get --config normal -f file -vs`
13    /// `--config` will be a long option with name `config`
14    LongOption(&'a str),
15
16    /// Short option. Only single ASCII char is stored (without `-`).
17    /// UTF-8 here is not supported.
18    ///
19    /// In `get --config normal -f file -vs`
20    /// `-f` and `-vs` will be short options.
21    /// `v` and `s` are treated as written separately (as '-v -s`)
22    ShortOption(char),
23
24    /// Value of an option or an argument.
25    ///
26    /// In `get --config normal -v file`
27    /// `normal` and `file` will be a value
28    Value(&'a str),
29}
30
31#[derive(Clone, Debug, Eq)]
32pub struct ArgList<'a> {
33    tokens: Tokens<'a>,
34}
35
36impl<'a> ArgList<'a> {
37    /// Create new arg list from given tokens
38    pub fn new(tokens: Tokens<'a>) -> Self {
39        Self { tokens }
40    }
41
42    pub fn args(&self) -> ArgsIter<'a> {
43        ArgsIter::new(self.tokens.iter())
44    }
45}
46
47impl<'a> PartialEq for ArgList<'a> {
48    fn eq(&self, other: &Self) -> bool {
49        self.args().eq(other.args())
50    }
51}
52
53#[derive(Clone, Debug, Eq, PartialEq)]
54pub enum ArgError {
55    NonAsciiShortOption,
56}
57
58#[derive(Debug)]
59pub struct ArgsIter<'a> {
60    values_only: bool,
61
62    /// Short options (ASCII chars) that
63    /// are left from previous iteration
64    leftover: &'a [u8],
65
66    tokens: TokensIter<'a>,
67}
68
69impl<'a> ArgsIter<'a> {
70    fn new(tokens: TokensIter<'a>) -> Self {
71        Self {
72            values_only: false,
73            leftover: &[],
74            tokens,
75        }
76    }
77
78    /// Converts whats left in this iterator back to `ArgList`
79    ///
80    /// If iterator was in the middle of iterating of collapsed
81    /// short options (like `-vhs`), non iterated options are discarded
82    pub fn into_args(self) -> ArgList<'a> {
83        ArgList::new(self.tokens.into_tokens())
84    }
85}
86
87impl<'a> Iterator for ArgsIter<'a> {
88    type Item = Result<Arg<'a>, ArgError>;
89
90    fn next(&mut self) -> Option<Self::Item> {
91        fn process_leftover<'a>(byte: u8) -> Result<Arg<'a>, ArgError> {
92            if byte.is_ascii_alphabetic() {
93                // SAFETY: we checked that this is alphabetic ASCII
94                Ok(Arg::ShortOption(unsafe {
95                    char::from_u32_unchecked(byte as u32)
96                }))
97            } else {
98                Err(ArgError::NonAsciiShortOption)
99            }
100        }
101
102        if !self.leftover.is_empty() {
103            let byte = self.leftover[0];
104            self.leftover = &self.leftover[1..];
105            return Some(process_leftover(byte));
106        }
107
108        let raw = self.tokens.next()?;
109        let bytes = raw.as_bytes();
110
111        if self.values_only {
112            return Some(Ok(Arg::Value(raw)));
113        }
114
115        let token = if bytes.len() > 1 && bytes[0] == b'-' {
116            if bytes[1] == b'-' {
117                if bytes.len() == 2 {
118                    self.values_only = true;
119                    Arg::DoubleDash
120                } else {
121                    Arg::LongOption(unsafe { raw.get_unchecked(2..) })
122                }
123            } else {
124                self.leftover = &bytes[2..];
125                return Some(process_leftover(bytes[1]));
126            }
127        } else {
128            Arg::Value(raw)
129        };
130
131        Some(Ok(token))
132    }
133}
134
135#[derive(Debug)]
136pub struct FromArgumentError<'a> {
137    pub value: &'a str,
138    pub expected: &'static str,
139}
140
141pub trait FromArgument<'a> {
142    fn from_arg(arg: &'a str) -> Result<Self, FromArgumentError<'_>>
143    where
144        Self: Sized;
145}
146
147impl<'a> FromArgument<'a> for &'a str {
148    fn from_arg(arg: &'a str) -> Result<Self, FromArgumentError<'_>> {
149        Ok(arg)
150    }
151}
152
153macro_rules! impl_arg_fromstr {
154    ($id:ident) => (
155        impl<'a> FromArgument<'a> for $id {
156            fn from_arg(arg: &'a str) -> Result<Self, FromArgumentError<'_>> {
157                arg.parse().map_err(|_| FromArgumentError {
158                    value: arg,
159                    expected: stringify!($id),
160                })
161            }
162        }
163    );
164
165    ($id:ident, $($ids:ident),+) => (
166        impl_arg_fromstr!{$id}
167        impl_arg_fromstr!{$($ids),+}
168    )
169}
170
171impl_arg_fromstr! {char, bool, u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize, f32, f64}
172
173#[cfg(test)]
174mod tests {
175    use rstest::rstest;
176
177    use crate::{arguments::ArgList, token::Tokens};
178
179    use super::{Arg, ArgError};
180
181    #[rstest]
182    #[case("arg1 --option1 val1 -f val2 -vs", &[
183        Ok(Arg::Value("arg1")), 
184        Ok(Arg::LongOption("option1")),
185        Ok(Arg::Value("val1")),
186        Ok(Arg::ShortOption('f')),
187        Ok(Arg::Value("val2")),
188        Ok(Arg::ShortOption('v')),
189        Ok(Arg::ShortOption('s')),
190    ])]
191    #[case("arg1 --option1 -- val1 -f val2 -vs", &[
192        Ok(Arg::Value("arg1")),
193        Ok(Arg::LongOption("option1")),
194        Ok(Arg::DoubleDash),
195        Ok(Arg::Value("val1")),
196        Ok(Arg::Value("-f")),
197        Ok(Arg::Value("val2")),
198        Ok(Arg::Value("-vs")),
199    ])]
200    #[case("arg1 -бjв", &[
201        Ok(Arg::Value("arg1")),
202        Err(ArgError::NonAsciiShortOption),
203        Err(ArgError::NonAsciiShortOption),
204        Ok(Arg::ShortOption('j')),
205        Err(ArgError::NonAsciiShortOption),
206        Err(ArgError::NonAsciiShortOption),
207    ])]
208    fn arg_tokens(#[case] input: &str, #[case] expected: &[Result<Arg<'_>, ArgError>]) {
209        let mut input = input.as_bytes().to_vec();
210        let input = core::str::from_utf8_mut(&mut input).unwrap();
211        let tokens = Tokens::new(input);
212        let args = ArgList::new(tokens);
213        let mut iter = args.args();
214
215        for arg in expected {
216            let actual = iter.next().unwrap();
217            assert_eq!(&actual, arg);
218        }
219        assert_eq!(iter.next(), None);
220        assert_eq!(iter.next(), None);
221    }
222
223    #[test]
224    fn test_eq() {
225        let mut input = b"arg1 arg2".to_vec();
226        let input = core::str::from_utf8_mut(&mut input).unwrap();
227        let tokens = Tokens::new(input);
228        let args1 = ArgList::new(tokens);
229
230        let mut input = b"   arg1    arg2  ".to_vec();
231        let input = core::str::from_utf8_mut(&mut input).unwrap();
232        let tokens = Tokens::new(input);
233        let args2 = ArgList::new(tokens);
234
235        assert_eq!(args1, args2)
236    }
237}