Skip to main content

winnow_args/
combinator.rs

1//! Parsers written by hand: name a flag, then say what it is.
2//!
3//! [`short`] and [`long`] name a flag; [`Named::switch`], [`Named::argument`]
4//! and the like turn the name into a winnow parser that reads **one
5//! occurrence** of it. [`args`] runs such a parser over the whole command
6//! line, and the closures given to `map` decide what to keep:
7//!
8//! ```
9//! use winnow::prelude::*;
10//! use winnow::combinator::alt;
11//! use winnow_args::BStr;
12//! use winnow_args::{Argv, Error, combinator::{args, short}};
13//!
14//! let verbose = short('v').long("verbose");
15//! let path = short('p').long("path");
16//!
17//! let words = [BStr::new("-v"), BStr::new("--path=/tmp")];
18//! let mut input = Argv::new(&words);
19//!
20//! let (mut v, mut p) = (false, None);
21//! args(alt((
22//!     verbose.switch().map(|()| v = true),
23//!     path.argument().map(|x| p = Some(x)),
24//! )))
25//! .parse_next(&mut input)?;
26//!
27//! assert!(v);
28//! assert_eq!(p.map(|x| &**x), Some(&b"/tmp"[..]));
29//! # Ok::<(), Error>(())
30//! ```
31//!
32//! What a missing or repeated flag means is up to those closures: here a
33//! repeated `--path` keeps the last one, and a missing one leaves `None`. The
34//! derive makes the same decisions from each field's type.
35
36use winnow::combinator::{alt, repeat, trace};
37use winnow::error::{ModalError as _, ParserError as _};
38use winnow::prelude::*;
39
40use crate::error::Error;
41use crate::stream::Argv;
42use crate::token::{self, Arg, ValueOptions};
43use crate::value::FromArg;
44use winnow::stream::BStr;
45
46/// The names a flag answers to: one short letter, and `N` long names (the first
47/// is the flag's name, the rest are aliases).
48///
49/// `N` is part of the type so the common `Named` stays 24 bytes: the parsers
50/// built from it copy it on every use, and larger copies stall
51/// (docs/PERF.md, step 8). Only a flag with aliases pays for them.
52#[derive(Copy, Clone, Debug, PartialEq, Eq)]
53pub struct Named<const N: usize = 1> {
54    short: Option<char>,
55    longs: [Option<&'static str>; N],
56    /// Sits in padding: a plain `Named` stays 24 bytes.
57    options: ValueOptions,
58}
59
60/// A flag named `-c`.
61pub const fn short(c: char) -> Named {
62    Named {
63        short: Some(c),
64        longs: [None],
65        options: ValueOptions::DEFAULT,
66    }
67}
68
69/// A flag named `--name`.
70pub const fn long(name: &'static str) -> Named {
71    Named {
72        short: None,
73        longs: [Some(name)],
74        options: ValueOptions::DEFAULT,
75    }
76}
77
78impl<const N: usize> Named<N> {
79    /// Also answer to `-c`.
80    pub const fn short(mut self, c: char) -> Self {
81        self.short = Some(c);
82        self
83    }
84
85    /// Answer to `--name`, replacing any long names.
86    pub const fn long(self, name: &'static str) -> Named {
87        Named {
88            short: self.short,
89            longs: [Some(name)],
90            options: self.options,
91        }
92    }
93
94    /// Answer to all of these long names, replacing any: the first is the
95    /// flag's name, the rest are aliases.
96    pub const fn longs<const M: usize>(self, names: [&'static str; M]) -> Named<M> {
97        let mut longs = [None; M];
98        let mut i = 0;
99        while i < M {
100            longs[i] = Some(names[i]);
101            i += 1;
102        }
103        Named {
104            short: self.short,
105            longs,
106            options: self.options,
107        }
108    }
109
110    /// Take the next word as the value whatever it looks like: `--args -x`, `--args --`.
111    pub const fn allow_hyphen_values(mut self) -> Self {
112        self.options.hyphen_values = true;
113        self
114    }
115
116    /// `true` for this flag, `false` for `no`, as in
117    /// `long("color").negated_by(long("no-color"))`. Fold with "last wins".
118    pub fn negated_by<'i, const M: usize>(
119        self,
120        no: Named<M>,
121    ) -> impl Parser<Argv<'i>, bool, Error> {
122        trace(
123            "negated_by",
124            alt((self.switch().value(true), no.switch().value(false))),
125        )
126    }
127
128    /// Only an attached value: `--name=v`, `-cv`; the next word is never taken.
129    pub const fn require_equals(mut self) -> Self {
130        self.options.require_equals = true;
131        self
132    }
133
134    /// Take a negative number as a detached value: `--offset -1`.
135    pub const fn allow_negative_numbers(mut self) -> Self {
136        self.options.negative_numbers = true;
137        self
138    }
139
140    /// Whether `arg` is this flag.
141    #[inline]
142    pub fn matches(&self, arg: &Arg<'_>) -> bool {
143        match arg {
144            Arg::Long(f) => self
145                .longs
146                .iter()
147                .any(|l| l.is_some_and(|l| f.name == l.as_bytes())),
148            Arg::Short(f) => self.short == Some(f.letter),
149            Arg::Word(_) | Arg::Separator { .. } => false,
150        }
151    }
152
153    /// How to name the flag in a message: the long name if it has one.
154    pub fn display(&self) -> String {
155        match (self.longs.first().copied().flatten(), self.short) {
156            (Some(l), _) => format!("--{l}"),
157            (None, Some(c)) => format!("-{c}"),
158            (None, None) => String::new(),
159        }
160    }
161
162    /// The error for a required flag that never appeared.
163    pub fn missing(&self, input: &Argv<'_>) -> Error {
164        Error::missing_required(input.offset(), self.display())
165    }
166
167    /// A switch: `--name` or `-c`, no value.
168    pub fn switch<'i>(mut self) -> impl Parser<Argv<'i>, (), Error> {
169        trace("switch", move |input: &mut Argv<'i>| {
170            self.parse_next(input)?.switch().parse_next(input)
171        })
172    }
173
174    /// An option taking one value, as bytes.
175    pub fn argument<'i>(mut self) -> impl Parser<Argv<'i>, &'i BStr, Error> {
176        trace("argument", move |input: &mut Argv<'i>| {
177            let arg = self.parse_next(input)?;
178            arg.read_value_with(input, self.options)
179        })
180    }
181
182    /// An option taking one value, converted with [`FromArg`].
183    pub fn argument_as<'i, T: FromArg>(mut self) -> impl Parser<Argv<'i>, T, Error> {
184        trace("argument_as", move |input: &mut Argv<'i>| {
185            self.parse_next(input)?
186                .value_as_with(self.options)
187                .parse_next(input)
188        })
189    }
190
191    /// An option whose value may be left out: a bare `--name`, or one followed by
192    /// another flag, gives `missing`. `--name=v` and `--name v` give `v`.
193    pub fn argument_or<'i, T: FromArg>(
194        mut self,
195        missing: &'static str,
196    ) -> impl Parser<Argv<'i>, T, Error> {
197        trace("argument_or", move |input: &mut Argv<'i>| {
198            let arg = self.parse_next(input)?;
199            let value = arg.read_value_or_with(input, self.options, BStr::new(missing));
200            arg.convert(value)
201        })
202    }
203
204    /// An option whose value is split on `delimiter`, each piece converted:
205    /// `--tags a,b` gives two.
206    pub fn arguments_as<'i, T: FromArg>(
207        mut self,
208        delimiter: u8,
209    ) -> impl Parser<Argv<'i>, Vec<T>, Error> {
210        trace("arguments_as", move |input: &mut Argv<'i>| {
211            let arg = self.parse_next(input)?;
212            let value = arg.read_value_with(input, self.options)?;
213            token::split(value, delimiter)
214                .map(|v| arg.convert(v))
215                .collect()
216        })
217    }
218}
219
220/// One occurrence of the flag's name, value not yet read.
221impl<'i, const N: usize> Parser<Argv<'i>, Arg<'i>, Error> for Named<N> {
222    #[inline]
223    fn parse_next(&mut self, input: &mut Argv<'i>) -> Result<Arg<'i>, Error> {
224        token::arg
225            .verify(|arg: &Arg<'i>| self.matches(arg))
226            .parse_next(input)
227    }
228}
229
230/// A positional value: one word that is not a flag, converted with [`FromArg`].
231/// `name` is how errors refer to it.
232///
233/// This matches one word; which positional a word fills is up to the caller,
234/// as with flags.
235pub fn positional<'i, T: FromArg>(name: &'static str) -> impl Parser<Argv<'i>, T, Error> {
236    trace("positional", move |input: &mut Argv<'i>| {
237        token::word(input)?.value_as(name).parse_next(input)
238    })
239}
240
241/// The spellings of a subcommand: one name, or a name and its aliases.
242pub trait Names {
243    /// Whether `word` is one of them.
244    fn contains(&self, word: &[u8]) -> bool;
245}
246
247impl Names for &str {
248    #[inline]
249    fn contains(&self, word: &[u8]) -> bool {
250        self.as_bytes() == word
251    }
252}
253
254impl<const N: usize> Names for [&str; N] {
255    #[inline]
256    fn contains(&self, word: &[u8]) -> bool {
257        self.iter().any(|name| name.as_bytes() == word)
258    }
259}
260
261/// A subcommand: the word `name` (or any of `["name", "alias", …]`), then
262/// `inner` for the rest of the line.
263///
264/// Once the word matches, `inner`'s errors are committed. After `--` no word is
265/// a subcommand.
266pub fn command<'i, O, P>(name: impl Names, mut inner: P) -> impl Parser<Argv<'i>, O, Error>
267where
268    P: Parser<Argv<'i>, O, Error>,
269{
270    trace("command", move |input: &mut Argv<'i>| {
271        let start = input.checkpoint();
272        match token::word(input) {
273            Ok(word) if name.contains(word.value) && !word.after_separator => {
274                inner.parse_next(input).map_err(|e| e.cut())
275            }
276            Ok(_) => {
277                input.reset(&start);
278                Err(Error::from_input(input))
279            }
280            Err(e) => Err(e),
281        }
282    })
283}
284
285/// Apply `item` until it stops matching, then require the command line to be
286/// over. What is left is reported as an unknown flag or an unexpected argument.
287///
288/// `item` is usually an `alt` of occurrence parsers each `map`ped into a
289/// variable. The `--` separator is consumed here, so a positional parser in
290/// `item` sees the words after it as plain words.
291pub fn args<'i, P>(mut item: P) -> impl Parser<Argv<'i>, (), Error>
292where
293    P: Parser<Argv<'i>, (), Error>,
294{
295    trace("args", move |input: &mut Argv<'i>| {
296        let separator = token::arg
297            .verify(|a: &Arg<'i>| matches!(a, Arg::Separator { .. }))
298            .void();
299        repeat::<_, _, (), _, _>(0.., winnow::combinator::alt((item.by_ref(), separator)))
300            .parse_next(input)?;
301        token::finish(input).map_err(|e| e.cut())
302    })
303}