no_pico_args/
lib.rs

1/*!
2An ultra simple CLI arguments parser.
3
4If you think that this library doesn't support some feature, it's probably intentional.
5
6- No help generation
7- Only flags, options, free arguments and subcommands are supported
8- Options can be separated by a space, `=` or nothing. See build features
9- Arguments can be in any order
10- Non UTF-8 arguments are supported
11
12## Build features
13
14- `eq-separator`
15
16  Allows parsing arguments separated by `=`<br/>
17  This feature adds about 1KiB to the resulting binary
18
19- `short-space-opt`
20
21  Makes the space between short keys and their values optional (e.g. `-w10`)<br/>
22  If `eq-separator` is enabled, then it takes precedence and the '=' is not included.<br/>
23  If `eq-separator` is disabled, then `-K=value` gives an error instead of returning `"=value"`.<br/>
24  The optional space is only applicable for short keys because `--keyvalue` would be ambiguous
25
26- `combined-flags`
27
28  Allows combination of flags, e.g. `-abc` instead of `-a -b -c`<br/>
29  If `short-space-opt` or `eq-separator` are enabled, you must parse flags after values,
30  to prevent ambiguities
31*/
32
33#![forbid(unsafe_code)]
34#![warn(missing_docs)]
35#![no_std]
36
37#[cfg(feature = "std")]
38extern crate std;
39
40extern crate alloc;
41
42use alloc::string::String;
43use alloc::string::ToString;
44use alloc::vec::Vec;
45use core::fmt;
46use core::fmt::Display;
47use core::str::FromStr;
48
49/// A list of possible errors.
50#[derive(Clone, Debug)]
51pub enum Error {
52    /// A missing free-standing argument.
53    MissingArgument,
54
55    /// A missing option.
56    MissingOption(Keys),
57
58    /// An option without a value.
59    OptionWithoutAValue(&'static str),
60
61    /// Failed to parse a UTF-8 free-standing argument.
62    #[allow(missing_docs)]
63    Utf8ArgumentParsingFailed { value: String, cause: String },
64
65    /// Failed to parse a raw free-standing argument.
66    #[allow(missing_docs)]
67    ArgumentParsingFailed { cause: String },
68}
69
70impl Display for Error {
71    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
72        match self {
73            Error::MissingArgument => {
74                write!(f, "free-standing argument is missing")
75            }
76            Error::MissingOption(key) => {
77                if key.second().is_empty() {
78                    write!(f, "the '{}' option must be set", key.first())
79                } else {
80                    write!(
81                        f,
82                        "the '{}/{}' option must be set",
83                        key.first(),
84                        key.second()
85                    )
86                }
87            }
88            Error::OptionWithoutAValue(key) => {
89                write!(f, "the '{}' option doesn't have an associated value", key)
90            }
91            Error::Utf8ArgumentParsingFailed { value, cause } => {
92                write!(f, "failed to parse '{}': {}", value, cause)
93            }
94            Error::ArgumentParsingFailed { cause } => {
95                write!(f, "failed to parse a binary argument: {}", cause)
96            }
97        }
98    }
99}
100
101impl core::error::Error for Error {}
102
103#[derive(Clone, Copy, PartialEq)]
104enum PairKind {
105    #[cfg(any(feature = "eq-separator", feature = "short-space-opt"))]
106    SingleArgument,
107    TwoArguments,
108}
109
110/// An arguments parser.
111#[derive(Clone, Debug)]
112pub struct Arguments(Vec<String>);
113
114impl Arguments {
115    /// Creates a parser from a vector of arguments.
116    ///
117    /// The executable path **must** be removed.
118    ///
119    /// This can be used for supporting `--` arguments to forward to another program.
120    /// See `examples/dash_dash.rs` for an example.
121    pub fn from_vec(args: Vec<String>) -> Self {
122        Arguments(args)
123    }
124
125    /// Creates a parser from a single string.
126    ///
127    /// This simply equals `Arguments::from_vec(string.split(' ').map(str::to_string).collect())`.
128    pub fn from_string(string: String) -> Self {
129        Self::from_vec(string.split(' ').map(str::to_string).collect())
130    }
131
132    /// Creates a parser from [`env::args_os`].
133    ///
134    /// The executable path will be removed.
135    ///
136    /// [`env::args_os`]: https://doc.rust-lang.org/stable/std/env/fn.args_os.html
137    #[cfg(feature = "std")]
138    pub fn from_env() -> Self {
139        let mut args: Vec<_> = std::env::args_os()
140            .map(|s| s.into_string().expect("Non-UTF-8 argument"))
141            .collect();
142        args.remove(0);
143        Arguments(args)
144    }
145
146    /// Parses the name of the subcommand, that is, the first positional argument.
147    ///
148    /// Returns `None` when subcommand starts with `-` or when there are no arguments left.
149    ///
150    /// # Errors
151    ///
152    /// - When arguments is not a UTF-8 string.
153    pub fn subcommand(&mut self) -> Option<String> {
154        if self.0.is_empty() {
155            return None;
156        }
157
158        let s = self.0[0].as_str();
159        if s.starts_with('-') {
160            return None;
161        }
162
163        Some(self.0.remove(0))
164    }
165
166    /// Checks that arguments contain a specified flag.
167    ///
168    /// Searches through all arguments, not only the first/next one.
169    ///
170    /// Calling this method "consumes" the flag: if a flag is present `n`
171    /// times then the first `n` calls to `contains` for that flag will
172    /// return `true`, and subsequent calls will return `false`.
173    ///
174    /// When the "combined-flags" feature is used, repeated letters count
175    /// as repeated flags: `-vvv` is treated the same as `-v -v -v`.
176    pub fn contains<A: Into<Keys>>(&mut self, keys: A) -> bool {
177        self.contains_impl(keys.into())
178    }
179
180    #[inline(never)]
181    fn contains_impl(&mut self, keys: Keys) -> bool {
182        if let Some((idx, _)) = self.index_of(keys) {
183            self.0.remove(idx);
184            true
185        } else {
186            #[cfg(feature = "combined-flags")]
187            // Combined flags only work if the short flag is a single character
188            {
189                if keys.first().len() == 2 {
190                    let short_flag = &keys.first()[1..2];
191                    for (n, item) in self.0.iter().enumerate() {
192                        let s = item.as_str();
193
194                        if s.starts_with('-') && !s.starts_with("--") && s.contains(short_flag) {
195                            if s.len() == 2 {
196                                // last flag
197                                self.0.remove(n);
198                            } else {
199                                self.0[n] = s.replacen(short_flag, "", 1);
200                            }
201                            return true;
202                        }
203                    }
204                }
205            }
206            false
207        }
208    }
209
210    /// Parses a key-value pair using `FromStr` trait.
211    ///
212    /// This is a shorthand for `value_from_fn("--key", FromStr::from_str)`
213    pub fn value_from_str<A, T>(&mut self, keys: A) -> Result<T, Error>
214    where
215        A: Into<Keys>,
216        T: FromStr,
217        <T as FromStr>::Err: Display,
218    {
219        self.value_from_fn(keys, FromStr::from_str)
220    }
221
222    /// Parses a key-value pair using a specified function.
223    ///
224    /// Searches through all argument, not only the first/next one.
225    ///
226    /// When a key-value pair is separated by a space, the algorithm
227    /// will treat the next argument after the key as a value,
228    /// even if it has a `-/--` prefix.
229    /// So a key-value pair like `--key --value` is not an error.
230    ///
231    /// Must be used only once for each option.
232    ///
233    /// # Errors
234    ///
235    /// - When option is not present.
236    /// - When value parsing failed.
237    /// - When key-value pair is separated not by space or `=`.
238    ///
239    /// [`value_from_os_str`]: struct.Arguments.html#method.value_from_os_str
240    pub fn value_from_fn<A: Into<Keys>, T, E: Display>(
241        &mut self,
242        keys: A,
243        f: fn(&str) -> Result<T, E>,
244    ) -> Result<T, Error> {
245        let keys = keys.into();
246        match self.opt_value_from_fn(keys, f) {
247            Ok(Some(v)) => Ok(v),
248            Ok(None) => Err(Error::MissingOption(keys)),
249            Err(e) => Err(e),
250        }
251    }
252
253    /// Parses an optional key-value pair using `FromStr` trait.
254    ///
255    /// This is a shorthand for `opt_value_from_fn("--key", FromStr::from_str)`
256    pub fn opt_value_from_str<A, T>(&mut self, keys: A) -> Result<Option<T>, Error>
257    where
258        A: Into<Keys>,
259        T: FromStr,
260        <T as FromStr>::Err: Display,
261    {
262        self.opt_value_from_fn(keys, FromStr::from_str)
263    }
264
265    /// Parses an optional key-value pair using a specified function.
266    ///
267    /// The same as [`value_from_fn`], but returns `Ok(None)` when option is not present.
268    ///
269    /// [`value_from_fn`]: struct.Arguments.html#method.value_from_fn
270    pub fn opt_value_from_fn<A: Into<Keys>, T, E: Display>(
271        &mut self,
272        keys: A,
273        f: fn(&str) -> Result<T, E>,
274    ) -> Result<Option<T>, Error> {
275        self.opt_value_from_fn_impl(keys.into(), f)
276    }
277
278    #[inline(never)]
279    fn opt_value_from_fn_impl<T, E: Display>(
280        &mut self,
281        keys: Keys,
282        f: fn(&str) -> Result<T, E>,
283    ) -> Result<Option<T>, Error> {
284        match self.find_value(keys)? {
285            Some((value, kind, idx)) => {
286                match f(value) {
287                    Ok(value) => {
288                        // Remove only when all checks are passed.
289                        self.0.remove(idx);
290                        if kind == PairKind::TwoArguments {
291                            self.0.remove(idx);
292                        }
293
294                        Ok(Some(value))
295                    }
296                    Err(e) => Err(Error::Utf8ArgumentParsingFailed {
297                        value: value.to_string(),
298                        cause: error_to_string(e),
299                    }),
300                }
301            }
302            None => Ok(None),
303        }
304    }
305
306    // The whole logic must be type-independent to prevent monomorphization.
307    #[cfg(any(feature = "eq-separator", feature = "short-space-opt"))]
308    #[inline(never)]
309    fn find_value(&mut self, keys: Keys) -> Result<Option<(&str, PairKind, usize)>, Error> {
310        if let Some((idx, key)) = self.index_of(keys) {
311            // Parse a `--key value` pair.
312
313            let value = match self.0.get(idx + 1) {
314                Some(v) => v,
315                None => return Err(Error::OptionWithoutAValue(key)),
316            };
317
318            Ok(Some((value, PairKind::TwoArguments, idx)))
319        } else if let Some((idx, key)) = self.index_of2(keys) {
320            // Parse a `--key=value` or `-Kvalue` pair.
321
322            let value = &self.0[idx];
323
324            // Only UTF-8 strings are supported in this method.
325            let value = value.as_str();
326
327            let mut value_range = key.len()..value.len();
328
329            if value.as_bytes().get(value_range.start) == Some(&b'=') {
330                #[cfg(feature = "eq-separator")]
331                {
332                    value_range.start += 1;
333                }
334                #[cfg(not(feature = "eq-separator"))]
335                return Err(Error::OptionWithoutAValue(key));
336            } else {
337                // Key must be followed by `=` if not `short-space-opt`
338                #[cfg(not(feature = "short-space-opt"))]
339                return Err(Error::OptionWithoutAValue(key));
340            }
341
342            // Check for quoted value.
343            if let Some(c) = value.as_bytes().get(value_range.start).cloned() {
344                if c == b'"' || c == b'\'' {
345                    value_range.start += 1;
346
347                    // A closing quote must be the same as an opening one.
348                    if ends_with(&value[value_range.start..], c) {
349                        value_range.end -= 1;
350                    } else {
351                        return Err(Error::OptionWithoutAValue(key));
352                    }
353                }
354            }
355
356            // Check length, otherwise String::drain will panic.
357            if value_range.end - value_range.start == 0 {
358                return Err(Error::OptionWithoutAValue(key));
359            }
360
361            // Extract `value` from `--key="value"`.
362            let value = &value[value_range];
363
364            if value.is_empty() {
365                return Err(Error::OptionWithoutAValue(key));
366            }
367
368            Ok(Some((value, PairKind::SingleArgument, idx)))
369        } else {
370            Ok(None)
371        }
372    }
373
374    // The whole logic must be type-independent to prevent monomorphization.
375    #[cfg(not(any(feature = "eq-separator", feature = "short-space-opt")))]
376    #[inline(never)]
377    fn find_value(&mut self, keys: Keys) -> Result<Option<(&str, PairKind, usize)>, Error> {
378        if let Some((idx, key)) = self.index_of(keys) {
379            // Parse a `--key value` pair.
380
381            let value = match self.0.get(idx + 1) {
382                Some(v) => v,
383                None => return Err(Error::OptionWithoutAValue(key)),
384            };
385            Ok(Some((value, PairKind::TwoArguments, idx)))
386        } else {
387            Ok(None)
388        }
389    }
390
391    /// Parses multiple key-value pairs into the `Vec` using `FromStr` trait.
392    ///
393    /// This is a shorthand for `values_from_fn("--key", FromStr::from_str)`
394    pub fn values_from_str<A, T>(&mut self, keys: A) -> Result<Vec<T>, Error>
395    where
396        A: Into<Keys>,
397        T: FromStr,
398        <T as FromStr>::Err: Display,
399    {
400        self.values_from_fn(keys, FromStr::from_str)
401    }
402
403    /// Parses multiple key-value pairs into the `Vec` using a specified function.
404    ///
405    /// This functions can be used to parse arguments like:<br>
406    /// `--file /path1 --file /path2 --file /path3`<br>
407    /// But not `--file /path1 /path2 /path3`.
408    ///
409    /// Arguments can also be separated: `--file /path1 --some-flag --file /path2`
410    ///
411    /// This method simply executes [Arguments::opt_value_from_fn] multiple times.
412    ///
413    /// An empty `Vec` is not an error.
414    pub fn values_from_fn<A: Into<Keys>, T, E: Display>(
415        &mut self,
416        keys: A,
417        f: fn(&str) -> Result<T, E>,
418    ) -> Result<Vec<T>, Error> {
419        let keys = keys.into();
420
421        let mut values = Vec::new();
422        loop {
423            match self.opt_value_from_fn(keys, f) {
424                Ok(Some(v)) => values.push(v),
425                Ok(None) => break,
426                Err(e) => return Err(e),
427            }
428        }
429
430        Ok(values)
431    }
432
433    #[inline(never)]
434    fn index_of(&self, keys: Keys) -> Option<(usize, &'static str)> {
435        // Do not unroll loop to save space, because it creates a bigger file.
436        // Which is strange, since `index_of2` actually benefits from it.
437
438        for key in &keys.0 {
439            if !key.is_empty() {
440                if let Some(i) = self.0.iter().position(|v| v == key) {
441                    return Some((i, key));
442                }
443            }
444        }
445
446        None
447    }
448
449    #[cfg(any(feature = "eq-separator", feature = "short-space-opt"))]
450    #[inline(never)]
451    fn index_of2(&self, keys: Keys) -> Option<(usize, &'static str)> {
452        // Loop unroll to save space.
453
454        if !keys.first().is_empty() {
455            if let Some(i) = self.0.iter().position(|v| index_predicate(v, keys.first())) {
456                return Some((i, keys.first()));
457            }
458        }
459
460        if !keys.second().is_empty() {
461            if let Some(i) = self
462                .0
463                .iter()
464                .position(|v| index_predicate(v, keys.second()))
465            {
466                return Some((i, keys.second()));
467            }
468        }
469
470        None
471    }
472
473    /// Parses a free-standing argument using `FromStr` trait.
474    ///
475    /// This is a shorthand for `free_from_fn(FromStr::from_str)`
476    pub fn free_from_str<T>(&mut self) -> Result<T, Error>
477    where
478        T: FromStr,
479        <T as FromStr>::Err: Display,
480    {
481        self.free_from_fn(FromStr::from_str)
482    }
483
484    /// Parses a free-standing argument using a specified function.
485    ///
486    /// Parses the first argument from the list of remaining arguments.
487    /// Therefore, it's up to the caller to check if the argument is actually
488    /// a free-standing one and not an unused flag/option.
489    ///
490    /// Sadly, there is no way to automatically check for flag/option.
491    /// `-`, `--`, `-1`, `-0.5`, `--.txt` - all of this arguments can have different
492    /// meaning depending on the caller requirements.
493    ///
494    /// Must be used only once for each argument.
495    ///
496    /// # Errors
497    ///
498    /// - When argument parsing failed.
499    /// - When argument is not present.
500    #[inline(never)]
501    pub fn free_from_fn<T, E: Display>(&mut self, f: fn(&str) -> Result<T, E>) -> Result<T, Error> {
502        self.opt_free_from_fn(f)?.ok_or(Error::MissingArgument)
503    }
504
505    /// Parses an optional free-standing argument using `FromStr` trait.
506    ///
507    /// The same as [Arguments::free_from_str], but returns `Ok(None)` when argument is not present.
508    pub fn opt_free_from_str<T>(&mut self) -> Result<Option<T>, Error>
509    where
510        T: FromStr,
511        <T as FromStr>::Err: Display,
512    {
513        self.opt_free_from_fn(FromStr::from_str)
514    }
515
516    /// Parses an optional free-standing argument using a specified function.
517    ///
518    /// The same as [Arguments::free_from_fn], but returns `Ok(None)` when argument is not present.
519    #[inline(never)]
520    pub fn opt_free_from_fn<T, E: Display>(
521        &mut self,
522        f: fn(&str) -> Result<T, E>,
523    ) -> Result<Option<T>, Error> {
524        if self.0.is_empty() {
525            Ok(None)
526        } else {
527            let value = self.0.remove(0);
528            match f(&value) {
529                Ok(value) => Ok(Some(value)),
530                Err(e) => Err(Error::Utf8ArgumentParsingFailed {
531                    value: value.to_string(),
532                    cause: error_to_string(e),
533                }),
534            }
535        }
536    }
537
538    /// Returns a list of remaining arguments.
539    ///
540    /// It's up to the caller what to do with them.
541    /// One can report an error about unused arguments,
542    /// other can use them for further processing.
543    pub fn finish(self) -> Vec<String> {
544        self.0
545    }
546}
547
548// Display::to_string() is usually inlined, so by wrapping it in a non-inlined
549// function we are reducing the size a bit.
550#[inline(never)]
551fn error_to_string<E: Display>(e: E) -> String {
552    e.to_string()
553}
554
555#[cfg(feature = "eq-separator")]
556#[inline(never)]
557fn starts_with_plus_eq(text: &str, prefix: &str) -> bool {
558    if text.get(0..prefix.len()) == Some(prefix) && text.as_bytes().get(prefix.len()) == Some(&b'=')
559    {
560        return true;
561    }
562
563    false
564}
565
566#[cfg(feature = "short-space-opt")]
567#[inline(never)]
568fn starts_with_short_prefix(text: &str, prefix: &str) -> bool {
569    if prefix.starts_with("--") {
570        return false; // Only works for short keys
571    }
572    if text.get(0..prefix.len()) == Some(prefix) {
573        return true;
574    }
575
576    false
577}
578
579#[cfg(all(feature = "eq-separator", feature = "short-space-opt"))]
580#[inline]
581fn index_predicate(text: &str, prefix: &str) -> bool {
582    starts_with_plus_eq(text, prefix) || starts_with_short_prefix(text, prefix)
583}
584#[cfg(all(feature = "eq-separator", not(feature = "short-space-opt")))]
585#[inline]
586fn index_predicate(text: &str, prefix: &str) -> bool {
587    starts_with_plus_eq(text, prefix)
588}
589#[cfg(all(feature = "short-space-opt", not(feature = "eq-separator")))]
590#[inline]
591fn index_predicate(text: &str, prefix: &str) -> bool {
592    starts_with_short_prefix(text, prefix)
593}
594
595#[cfg(any(feature = "eq-separator", feature = "short-space-opt"))]
596#[inline]
597fn ends_with(text: &str, c: u8) -> bool {
598    if text.is_empty() {
599        false
600    } else {
601        text.as_bytes()[text.len() - 1] == c
602    }
603}
604
605/// A keys container.
606///
607/// Should not be used directly.
608#[doc(hidden)]
609#[derive(Clone, Copy, Debug)]
610pub struct Keys([&'static str; 2]);
611
612impl Keys {
613    #[inline]
614    fn first(&self) -> &'static str {
615        self.0[0]
616    }
617
618    #[inline]
619    fn second(&self) -> &'static str {
620        self.0[1]
621    }
622}
623
624impl From<[&'static str; 2]> for Keys {
625    #[inline]
626    fn from(v: [&'static str; 2]) -> Self {
627        debug_assert!(v[0].starts_with("-"), "an argument should start with '-'");
628        validate_shortflag(v[0]);
629        debug_assert!(
630            !v[0].starts_with("--"),
631            "the first argument should be short"
632        );
633        debug_assert!(v[1].starts_with("--"), "the second argument should be long");
634        Keys(v)
635    }
636}
637
638fn validate_shortflag(short_key: &'static str) {
639    let mut chars = short_key[1..].chars();
640    if let Some(first) = chars.next() {
641        debug_assert!(
642            short_key.len() == 2 || chars.all(|c| c == first),
643            "short keys should be a single character or a repeated character"
644        );
645    }
646}
647
648impl From<&'static str> for Keys {
649    #[inline]
650    fn from(v: &'static str) -> Self {
651        debug_assert!(v.starts_with("-"), "an argument should start with '-'");
652        if !v.starts_with("--") {
653            validate_shortflag(v);
654        }
655        Keys([v, ""])
656    }
657}