Skip to main content

winnow_args/
value.rs

1//! Turning a value's bytes into a Rust type.
2//!
3//! A field's type says how its value is read, through [`FromArg`]:
4//!
5//! | Type | Reads |
6//! |---|---|
7//! | `String`, `char`, `bool`, integers, floats, `std::net` addresses | UTF-8 text, parsed with `str::parse` |
8//! | `PathBuf`, `OsString` | the bytes as they are on Unix, UTF-8 elsewhere |
9//! | `Vec<u8>` | the bytes as they are |
10//! | an enum deriving `ValueEnum` | one of its variants' names |
11//! | [`Parsed<T>`] | any `T: FromStr`, a type from another crate included |
12//! | [`CInt<T>`] | an integer in C syntax: `0x400000`, `010000` |
13//! | [`KeyValue<K, V>`] | `key=value`, split at the first `=` |
14//!
15//! Any other type implements [`FromArg`] itself. In an `Occurrence` variant,
16//! [`Spanned<T>`] is a `T` with where it was found.
17
18use std::ffi::OsString;
19use std::path::PathBuf;
20
21use winnow::stream::BStr;
22
23use crate::error::BoxError;
24
25/// A type a flag's value can be converted into.
26///
27/// Values arrive as bytes borrowed from the command line. Conversion is the
28/// only place a value can fail, so a command line that is not valid UTF-8 still
29/// parses — flags still match — and only a value that is actually converted to
30/// text can be rejected for it.
31///
32/// For a type that is [`FromStr`](std::str::FromStr), [`Parsed<T>`] needs no
33/// impl. Otherwise:
34///
35/// ```
36/// use winnow_args::error::BoxError;
37/// use winnow_args::{BStr, FromArg};
38///
39/// /// `WIDTHxHEIGHT`.
40/// struct Size(u32, u32);
41///
42/// impl FromArg for Size {
43///     fn from_arg(value: &BStr) -> Result<Self, BoxError> {
44///         let (w, h) = std::str::from_utf8(value)?
45///             .split_once('x')
46///             .ok_or("expected WIDTHxHEIGHT")?;
47///         Ok(Size(w.parse()?, h.parse()?))
48///     }
49/// }
50///
51/// let Size(w, h) = Size::from_arg(BStr::new("80x24")).unwrap();
52/// assert_eq!((w, h), (80, 24));
53/// ```
54pub trait FromArg: Sized {
55    /// The values accepted, when there is a fixed set: help lists them as
56    /// `[possible values: …]`. Empty by default; `#[derive(ValueEnum)]` fills
57    /// it with each visible variant's name.
58    const CHOICES: &'static [&'static str] = &[];
59
60    /// The name `#[derive(ValueEnum)]` files its choices under when the
61    /// documentation is built. Empty for an implementation written by hand.
62    #[doc(hidden)]
63    const SPEC: &'static str = "";
64
65    /// What the value is, for the shell that completes it, when a field
66    /// states no `hint`. Nothing a shell knows by default: a number, an
67    /// address, a type of the program. A `PathBuf`, a `String` and an
68    /// `OsString` are a file: a word is one more often than not, and a shell
69    /// that offers files stops no other word from being typed.
70    const HINT: crate::complete::Hint = crate::complete::Hint::Nothing;
71
72    /// Convert one value.
73    fn from_arg(value: &BStr) -> Result<Self, BoxError>;
74}
75
76/// The cause of an [`ErrorKind::InvalidChoice`](crate::ErrorKind::InvalidChoice):
77/// the value is not one of `choices`.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub struct ChoiceError {
80    /// Every accepted spelling.
81    pub choices: &'static [&'static str],
82}
83
84impl std::fmt::Display for ChoiceError {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        write!(f, "expected one of {}", self.choices.join(", "))
87    }
88}
89
90impl std::error::Error for ChoiceError {}
91
92/// The value as `&str`.
93pub fn to_str(value: &BStr) -> Result<&str, std::str::Utf8Error> {
94    std::str::from_utf8(value)
95}
96
97impl FromArg for String {
98    const HINT: crate::complete::Hint = crate::complete::Hint::Files;
99
100    fn from_arg(value: &BStr) -> Result<Self, BoxError> {
101        Ok(to_str(value)?.to_owned())
102    }
103}
104
105impl FromArg for Vec<u8> {
106    fn from_arg(value: &BStr) -> Result<Self, BoxError> {
107        Ok(value.to_vec())
108    }
109}
110
111impl FromArg for OsString {
112    const HINT: crate::complete::Hint = crate::complete::Hint::Files;
113
114    #[cfg(unix)]
115    fn from_arg(value: &BStr) -> Result<Self, BoxError> {
116        use std::os::unix::ffi::OsStrExt as _;
117        Ok(std::ffi::OsStr::from_bytes(value).to_owned())
118    }
119
120    #[cfg(not(unix))]
121    fn from_arg(value: &BStr) -> Result<Self, BoxError> {
122        Ok(to_str(value)?.into())
123    }
124}
125
126impl FromArg for PathBuf {
127    const HINT: crate::complete::Hint = crate::complete::Hint::Files;
128
129    fn from_arg(value: &BStr) -> Result<Self, BoxError> {
130        OsString::from_arg(value).map(PathBuf::from)
131    }
132}
133
134macro_rules! from_str {
135    ($($ty:ty),* $(,)?) => {$(
136        impl FromArg for $ty {
137            fn from_arg(value: &BStr) -> Result<Self, BoxError> {
138                Ok(to_str(value)?.parse::<$ty>()?)
139            }
140        }
141    )*};
142}
143
144from_str!(
145    bool,
146    char,
147    u8,
148    u16,
149    u32,
150    u64,
151    u128,
152    usize,
153    i8,
154    i16,
155    i32,
156    i64,
157    i128,
158    isize,
159    f32,
160    f64,
161    std::net::IpAddr,
162    std::net::Ipv4Addr,
163    std::net::Ipv6Addr,
164    std::net::SocketAddr,
165);
166
167/// Any [`FromStr`](std::str::FromStr) type as a value: the text is parsed with
168/// `str::parse`, and its error becomes the invalid value's cause.
169///
170/// For a type from another crate, which can implement neither [`FromArg`]
171/// here nor derive `ValueEnum` (a `strum` enum, say).
172///
173/// ```
174/// use winnow_args::FromArg;
175/// use winnow_args::value::Parsed;
176///
177/// let Parsed(ip) = Parsed::<std::net::Ipv4Addr>::from_arg("127.0.0.1".into())?;
178/// assert!(ip.is_loopback());
179/// assert!(Parsed::<std::net::Ipv4Addr>::from_arg("localhost".into()).is_err());
180/// # Ok::<(), Box<dyn std::error::Error + Send + Sync>>(())
181/// ```
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
183pub struct Parsed<T>(pub T);
184
185impl<T> FromArg for Parsed<T>
186where
187    T: std::str::FromStr,
188    T::Err: Into<BoxError>,
189{
190    fn from_arg(value: &BStr) -> Result<Self, BoxError> {
191        to_str(value)?.parse().map(Parsed).map_err(Into::into)
192    }
193}
194
195/// A value with where it was found: an `Occurrence` variant holding a
196/// `Spanned<T>` gets the flag's (or word's) offset and whether the value was
197/// attached (`-zfoo`, `--name=value`) or the next word (`-z foo`).
198///
199/// For errors that point at an item of a sequence (`--pop-state` with nothing
200/// pushed), and for messages that echo the spelling given.
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
202pub struct Spanned<T> {
203    /// The value.
204    pub value: T,
205    /// Byte offset of the flag, or of the word for a positional, as
206    /// [`Argv::offset`](crate::Argv::offset) counts.
207    pub offset: usize,
208    /// Whether the value was in the flag's own word.
209    pub attached: bool,
210}
211
212/// An integer in C syntax, as linkers and assemblers read one: `0x`/`0X` hex,
213/// a leading `0` for octal, otherwise decimal, with a sign for signed types.
214/// `--image-base=0x400000`, `-z max-page-size=0x1000`, `-Ttext=010000`.
215#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
216pub struct CInt<T>(pub T);
217
218/// The error of a malformed [`CInt`].
219#[derive(Debug, Clone, PartialEq, Eq)]
220pub struct CIntError(&'static str);
221
222impl std::fmt::Display for CIntError {
223    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224        f.write_str(self.0)
225    }
226}
227
228impl std::error::Error for CIntError {}
229
230/// The sign, radix and digits of a C integer.
231fn c_digits(text: &str) -> Result<(bool, u32, &str), CIntError> {
232    let (negative, rest) = match text.strip_prefix('-') {
233        Some(rest) => (true, rest),
234        None => (false, text.strip_prefix('+').unwrap_or(text)),
235    };
236    let (radix, digits) =
237        if let Some(hex) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) {
238            (16, hex)
239        } else if rest.len() > 1 && rest.starts_with('0') {
240            (8, &rest[1..])
241        } else {
242            (10, rest)
243        };
244    if digits.is_empty() || !digits.chars().all(|c| c.is_digit(radix)) {
245        return Err(CIntError("expected a number: decimal, 0x hex or 0 octal"));
246    }
247    Ok((negative, radix, digits))
248}
249
250macro_rules! c_int {
251    ($($ty:ty),* $(,)?) => {$(
252        impl FromArg for CInt<$ty> {
253            fn from_arg(value: &BStr) -> Result<Self, BoxError> {
254                let (negative, radix, digits) = c_digits(to_str(value)?)?;
255                let magnitude = u128::from_str_radix(digits, radix)?;
256                let n = if negative {
257                    0i128.checked_sub_unsigned(magnitude).and_then(|n| <$ty>::try_from(n).ok())
258                } else {
259                    <$ty>::try_from(magnitude).ok()
260                };
261                n.map(CInt).ok_or_else(|| CIntError("number out of range").into())
262            }
263        }
264    )*};
265}
266
267c_int!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
268
269/// `key=value`, split at the first `=`: `--defsym=sym=expr`,
270/// `-z max-page-size=4096`, `--section-start=.text=0x1000`. The value may hold
271/// more `=`.
272#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
273pub struct KeyValue<K, V> {
274    /// Before the first `=`.
275    pub key: K,
276    /// After it.
277    pub value: V,
278}
279
280/// The error of a [`KeyValue`] with no `=`.
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub struct MissingEquals;
283
284impl std::fmt::Display for MissingEquals {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        f.write_str("expected `key=value`")
287    }
288}
289
290impl std::error::Error for MissingEquals {}
291
292impl<K: FromArg, V: FromArg> FromArg for KeyValue<K, V> {
293    fn from_arg(value: &BStr) -> Result<Self, BoxError> {
294        let eq = value.iter().position(|&b| b == b'=').ok_or(MissingEquals)?;
295        Ok(KeyValue {
296            key: K::from_arg(BStr::new(&value[..eq]))?,
297            value: V::from_arg(BStr::new(&value[eq + 1..]))?,
298        })
299    }
300}
301
302/// Whether a documentation build finds `T`'s choices by `path`: it has none,
303/// or `#[derive(ValueEnum)]` filed them where `path` leads. A type named
304/// through an alias, or one that implements [`FromArg`] by hand, does not.
305#[doc(hidden)]
306pub const fn documented_choices<T: FromArg>(path: &str) -> bool {
307    if T::SPEC.is_empty() {
308        T::CHOICES.is_empty()
309    } else {
310        filed_as(T::SPEC, path)
311    }
312}
313
314/// Whether a documentation build finds a type by `path`, as a field writes
315/// it: `add::Opts`, `crate::Opts`, `dep::Opts`, or the name it states.
316///
317/// `spec` is the type's, `CRATE:MODULES:Name` from a derive in such a build.
318/// The name is the last of `path`. What is before it starts at a crate's
319/// root (`crate::`, or the name of the type's crate), or names modules of the
320/// type, one after the other: the module it is in, or one above that brings
321/// it out with a `pub use`. `winnow-args-spec` looks for the same, so that it
322/// never takes another type of the name for this. An empty `spec` is a type written by hand, and
323/// filed by hand.
324#[doc(hidden)]
325pub const fn filed_as(spec: &str, path: &str) -> bool {
326    let (spec, path) = (spec.as_bytes(), path.as_bytes());
327    if spec.is_empty() {
328        return true;
329    }
330    // CRATE:MODULES:Name, or the name alone.
331    let (mut first, mut last, mut at) = (spec.len(), 0, 0);
332    while at < spec.len() {
333        if spec[at] == b':' {
334            if first == spec.len() {
335                first = at;
336            }
337            last = at + 1;
338        }
339        at += 1;
340    }
341    // `a::b::Name`: the name, and the modules before it.
342    let mut name = path.len();
343    while name > 0 && path[name - 1] != b':' {
344        name -= 1;
345    }
346    if !same_bytes(spec, last, spec.len(), path, name, path.len()) {
347        return false;
348    }
349    if first == spec.len() || name == 0 {
350        return true;
351    }
352    // From a crate's root, a `pub use` may bring the type out anywhere.
353    let to = name - 2;
354    if starts_with(path, b"crate::") || first + 2 >= last {
355        return true;
356    }
357    if among_modules(spec, first + 1, last - 1, path, 0, to) {
358        return true;
359    }
360    let mut end = 0;
361    while end < to && path[end] != b':' {
362        end += 1;
363    }
364    same_bytes(spec, 0, first, path, 0, end)
365}
366
367/// Whether the modules `a/b/c` in `spec` have the modules `a::b` or `b::c` in
368/// `path` among them, in a row.
369const fn among_modules(
370    spec: &[u8],
371    start: usize,
372    mut end: usize,
373    path: &[u8],
374    from: usize,
375    to: usize,
376) -> bool {
377    loop {
378        if ends_with_modules(spec, start, end, path, from, to) {
379            return true;
380        }
381        while end > start && spec[end - 1] != b'/' {
382            end -= 1;
383        }
384        if end == start {
385            return false;
386        }
387        end -= 1;
388    }
389}
390
391/// Whether the modules `a/b/c` in `spec` end with the modules `b::c` in `path`.
392const fn ends_with_modules(
393    spec: &[u8],
394    start: usize,
395    mut end: usize,
396    path: &[u8],
397    from: usize,
398    mut to: usize,
399) -> bool {
400    loop {
401        let (mut module, mut written) = (end, to);
402        while module > start && spec[module - 1] != b'/' {
403            module -= 1;
404        }
405        while written > from && path[written - 1] != b':' {
406            written -= 1;
407        }
408        if !same_bytes(spec, module, end, path, written, to) {
409            return false;
410        }
411        if written == from {
412            return true;
413        }
414        if module == start {
415            return false;
416        }
417        (end, to) = (module - 1, written - 2);
418    }
419}
420
421const fn starts_with(text: &[u8], prefix: &[u8]) -> bool {
422    text.len() >= prefix.len() && same_bytes(text, 0, prefix.len(), prefix, 0, prefix.len())
423}
424
425const fn same_bytes(a: &[u8], from: usize, to: usize, b: &[u8], start: usize, end: usize) -> bool {
426    if to - from != end - start {
427        return false;
428    }
429    let mut at = 0;
430    while at < to - from {
431        if a[from + at] != b[start + at] {
432            return false;
433        }
434        at += 1;
435    }
436    true
437}
438
439/// Whether the choices a field states are those of its type `T`, when `T`
440/// has any: what a documentation build shows is then what is accepted.
441#[doc(hidden)]
442pub const fn stated_choices<T: FromArg>(stated: &[&str]) -> bool {
443    if T::CHOICES.is_empty() {
444        return true;
445    }
446    if T::CHOICES.len() != stated.len() {
447        return false;
448    }
449    let mut at = 0;
450    while at < stated.len() {
451        if !same(T::CHOICES[at], stated[at]) {
452            return false;
453        }
454        at += 1;
455    }
456    true
457}
458
459const fn same(a: &str, b: &str) -> bool {
460    same_bytes(a.as_bytes(), 0, a.len(), b.as_bytes(), 0, b.len())
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn a_path_leads_to_a_type_by_its_modules_or_its_crate() {
469        let spec = "app:src/cli/add:Opts";
470        for path in [
471            "Opts",
472            "add::Opts",
473            "cli::add::Opts",
474            "cli::Opts",
475            "crate::Opts",
476            "crate::cli::add::Opts",
477            "app::Opts",
478            "app::cli::Opts",
479            "app::src::cli::add::Opts",
480            "app::prelude::Opts",
481            "crate::prelude::Opts",
482        ] {
483            assert!(filed_as(spec, path), "{path}");
484        }
485        for path in [
486            "AddOpts",
487            "remove::Opts",
488            "add::cli::Opts",
489            "src::add::Opts",
490            "dep::Opts",
491            "dep::cli::add::Opts",
492            "std::net::Opts",
493        ] {
494            assert!(!filed_as(spec, path), "{path}");
495        }
496        // The name alone, as any other build has it, and a type written by hand.
497        assert!(filed_as("Opts", "anywhere::Opts"));
498        assert!(!filed_as("Opts", "anywhere::Other"));
499        assert!(filed_as("", "anywhere::Opts"));
500        assert!(filed_as("app::Opts", "x::Opts"));
501    }
502
503    fn c<T>(text: &str) -> Result<T, String>
504    where
505        CInt<T>: FromArg,
506    {
507        CInt::<T>::from_arg(BStr::new(text))
508            .map(|CInt(n)| n)
509            .map_err(|e| e.to_string())
510    }
511
512    #[test]
513    fn c_integers() {
514        assert_eq!(c::<u64>("0x400000"), Ok(0x40_0000));
515        assert_eq!(c::<u64>("0X1f"), Ok(31));
516        assert_eq!(c::<u64>("010"), Ok(8));
517        assert_eq!(c::<u64>("0"), Ok(0));
518        assert_eq!(c::<u64>("4096"), Ok(4096));
519        assert_eq!(c::<i64>("-0x10"), Ok(-16));
520        assert_eq!(c::<i8>("-128"), Ok(-128));
521        assert!(c::<u8>("256").is_err());
522        assert!(c::<u64>("-1").is_err());
523        assert!(c::<u64>("08").is_err());
524        assert!(c::<u64>("0x").is_err());
525        assert!(c::<u64>("").is_err());
526    }
527
528    #[test]
529    fn key_values() {
530        let kv = KeyValue::<String, String>::from_arg(BStr::new("sym=a=b")).unwrap();
531        assert_eq!((kv.key.as_str(), kv.value.as_str()), ("sym", "a=b"));
532        let kv = KeyValue::<String, CInt<u64>>::from_arg(BStr::new(".text=0x1000")).unwrap();
533        assert_eq!((kv.key.as_str(), kv.value.0), (".text", 0x1000));
534        assert!(KeyValue::<String, String>::from_arg(BStr::new("nothing")).is_err());
535    }
536}