Skip to main content

winnow_args/
token.rs

1//! Lexing: one command-line item at a time.
2//!
3//! [`arg`] reads the next item: a long flag, one short letter, a word, or the
4//! `--` separator. It does not know which flags take a value, so it stops
5//! where a value *would* start; the caller, which knows the flag, then calls
6//! [`Arg::switch`] (no value) or [`Arg::value`] (read it). The derive, the
7//! [combinators](crate::combinator) and a hand-written `dispatch!` on flag
8//! names all read items this way.
9//!
10//! | command line | items                                        |
11//! |--------------|----------------------------------------------|
12//! | `--path=x`   | `Long` named `path` with value `x`           |
13//! | `--path x`   | `Long` named `path`, then [`Arg::value`] reads `x` |
14//! | `-vpx`       | `Short` `v`, `Short` `p`, then [`Arg::value`] reads `x` |
15//! | `-p=x`       | `Short` `p`, then [`Arg::value`] reads `x`   |
16//! | `-`          | `Word` `-`                                   |
17//! | `--`         | `Separator`; every later word is a `Word`    |
18
19use winnow::error::{ModalError as _, ParserError};
20use winnow::prelude::*;
21use winnow::stream::BStr;
22
23use crate::error::Error;
24use crate::stream::{Argv, Mode};
25use crate::value::FromArg;
26
27/// One lexed command-line item.
28///
29/// Its fields are public and plain so a `dispatch!` arm can match flags as
30/// patterns, both spellings at once:
31///
32/// ```
33/// use winnow::combinator::{dispatch, fail};
34/// use winnow::prelude::*;
35/// use winnow_args::BStr;
36/// use winnow_args::combinator::args;
37/// use winnow_args::token::{Arg, LongFlag, ShortFlag, arg};
38/// use winnow_args::{Argv, Error};
39///
40/// let words = [BStr::new("-p"), BStr::new("x"), BStr::new("f")];
41/// let (mut path, mut files) = (None::<String>, Vec::<String>::new());
42/// let (p, f) = (&mut path, &mut files);
43/// args(dispatch! {arg;
44///     a @ (Arg::Long(LongFlag { name: b"path", .. }) | Arg::Short(ShortFlag { letter: 'p', .. })) => {
45///         a.value_as().map(|v| *p = Some(v))
46///     },
47///     Arg::Word(w) => w.value_as("FILE").map(|file| f.push(file)),
48///     _ => fail,
49/// })
50/// .parse_next(&mut Argv::new(&words))?;
51/// assert_eq!((path.as_deref(), &files[..]), (Some("x"), &["f".to_string()][..]));
52/// # Ok::<(), Error>(())
53/// ```
54#[derive(Copy, Clone, Debug, PartialEq, Eq)]
55pub enum Arg<'i> {
56    /// `--name` or `--name=value`. The word is consumed whole.
57    Long(LongFlag<'i>),
58    /// One letter of `-abc`. The stream is left on the next letter (or the
59    /// attached value), or at the next word if this was the last letter.
60    Short(ShortFlag),
61    /// A word that is not a flag: a positional value, a lone `-`, or anything
62    /// after `--`.
63    Word(Word<'i>),
64    /// The first `--`.
65    Separator {
66        /// Where it is.
67        offset: usize,
68    },
69}
70
71/// `--name` or `--name=value`.
72#[derive(Copy, Clone, Debug, PartialEq, Eq)]
73pub struct LongFlag<'i> {
74    /// The text between `--` and the first `=`, as bytes so it can be a pattern.
75    pub name: &'i [u8],
76    /// The text after the first `=`, if there was one.
77    pub value: Option<&'i BStr>,
78    /// Where the flag starts.
79    pub offset: usize,
80}
81
82/// One letter of `-abc`.
83#[derive(Copy, Clone, Debug, PartialEq, Eq)]
84pub struct ShortFlag {
85    /// The letter.
86    pub letter: char,
87    /// Spelled `+c` rather than `-c` ([`arg_plus`]); fits in padding, so `Arg`
88    /// does not grow.
89    pub plus: bool,
90    /// Where the letter is.
91    pub offset: usize,
92}
93
94/// A word that is not a flag.
95#[derive(Copy, Clone, Debug, PartialEq, Eq)]
96pub struct Word<'i> {
97    /// The word.
98    pub value: &'i BStr,
99    /// Where it starts.
100    pub offset: usize,
101    /// Whether it came after `--`, where no word names a subcommand.
102    pub after_separator: bool,
103}
104
105impl<'i> Arg<'i> {
106    /// The item as the user typed it, for error messages.
107    pub fn spelling(&self) -> String {
108        match self {
109            Arg::Long(f) => format!("--{}", BStr::new(f.name)),
110            Arg::Short(f) => format!("{}{}", if f.plus { '+' } else { '-' }, f.letter),
111            Arg::Word(w) => w.value.to_string(),
112            Arg::Separator { .. } => "--".to_owned(),
113        }
114    }
115
116    /// Where the item starts.
117    pub fn offset(&self) -> usize {
118        match *self {
119            Arg::Long(LongFlag { offset, .. })
120            | Arg::Short(ShortFlag { offset, .. })
121            | Arg::Word(Word { offset, .. })
122            | Arg::Separator { offset } => offset,
123        }
124    }
125
126    /// The error for this item when nothing accepts it.
127    pub fn unexpected(&self) -> Error {
128        match self {
129            Arg::Long(_) | Arg::Short(_) => Error::unknown_flag(self.offset(), self.spelling()),
130            Arg::Word(_) | Arg::Separator { .. } => {
131                Error::unexpected_arg(self.offset(), self.spelling())
132            }
133        }
134    }
135
136    /// Finish a switch. Only a long flag can have been given a value,
137    /// `--verbose=yes`, which is an error; in `-vx` the `x` is another letter.
138    #[inline(always)]
139    pub fn check_switch(&self) -> Result<(), Error> {
140        match self {
141            Arg::Long(LongFlag { value: Some(v), .. }) => Err(self.unexpected_value(v)),
142            _ => Ok(()),
143        }
144    }
145
146    // The errors of the continuations above, built out of line: they are
147    // inlined into every flag's arm, and only this call should be.
148    #[cold]
149    #[inline(never)]
150    fn unexpected_value(&self, value: &BStr) -> Error {
151        Error::unexpected_value(self.offset(), self.spelling(), value)
152    }
153
154    #[cold]
155    #[inline(never)]
156    fn missing_value(&self) -> Error {
157        Error::missing_value(self.offset(), self.spelling())
158    }
159
160    /// Finish an option, committing: a missing value is an error, not a reason
161    /// to try something else.
162    ///
163    /// - `--name=value`: the attached text.
164    /// - `-nvalue`, `-n=value`: the rest of the bundle, less one `=`.
165    /// - `--name value`, `-n value`: the next word, unless it is flag-like. A
166    ///   lone `-` is a value; `--` is not.
167    #[inline(always)]
168    pub fn read_value(&self, input: &mut Argv<'i>) -> Result<&'i BStr, Error> {
169        self.read_value_with(input, ValueOptions::DEFAULT)
170    }
171
172    /// One more value for a flag that takes several (`-platform_version macos
173    /// 11.0 12.0`): the next word, whatever it looks like.
174    #[inline]
175    pub fn read_next(&self, input: &mut Argv<'i>) -> Result<&'i BStr, Error> {
176        if input.is_empty() {
177            return Err(self.missing_value());
178        }
179        Ok(input.take_word())
180    }
181
182    /// One more value for a flag that takes a variable number of them
183    /// (`--include a b`): the next word, unless the words ran out, it is
184    /// flag-like (as [`Arg::read_value_with`] judges it under `options`), or it
185    /// is `terminator`, which is dropped.
186    #[inline]
187    pub fn read_more(
188        &self,
189        input: &mut Argv<'i>,
190        options: ValueOptions,
191        terminator: Option<&BStr>,
192    ) -> Option<&'i BStr> {
193        if input.is_empty() {
194            return None;
195        }
196        if terminator.is_some_and(|t| **t == *input.front()) {
197            input.take_word();
198            return None;
199        }
200        detached(input, options)
201    }
202
203    /// Fail if a flag declared to take at least `min` values got fewer.
204    #[inline]
205    pub fn check_values(&self, got: usize, min: usize) -> Result<(), Error> {
206        if got < min {
207            return Err(self.missing_value());
208        }
209        Ok(())
210    }
211
212    /// Whether a value is attached to this flag in its own word:
213    /// `--name=value`, `-nvalue`. Asked before reading it.
214    #[inline(always)]
215    pub fn has_attached_value(&self, input: &Argv<'i>) -> bool {
216        match self {
217            Arg::Long(LongFlag { value, .. }) => value.is_some(),
218            Arg::Short(_) => input.mode() == Mode::Bundle,
219            _ => false,
220        }
221    }
222
223    /// [`Arg::read_value`] under `options`: which detached words it may take.
224    #[inline(always)]
225    pub fn read_value_with(
226        &self,
227        input: &mut Argv<'i>,
228        options: ValueOptions,
229    ) -> Result<&'i BStr, Error> {
230        match self.next_value(input, options) {
231            Some(v) => Ok(v),
232            None => Err(self.missing_value()),
233        }
234    }
235
236    /// [`Arg::read_value`] for a flag whose value may be left out: where that
237    /// would report a missing value, `--color` alone or before another flag,
238    /// this gives `missing` instead.
239    #[inline(always)]
240    pub fn read_value_or(&self, input: &mut Argv<'i>, missing: &'static BStr) -> &'i BStr {
241        self.read_value_or_with(input, ValueOptions::DEFAULT, missing)
242    }
243
244    /// [`Arg::read_value_or`] under `options`.
245    #[inline(always)]
246    pub fn read_value_or_with(
247        &self,
248        input: &mut Argv<'i>,
249        options: ValueOptions,
250        missing: &'static BStr,
251    ) -> &'i BStr {
252        self.next_value(input, options).unwrap_or(missing)
253    }
254
255    /// [`Arg::read_value`] for a flag whose value may be left out, told apart
256    /// from a value of zero length: `--color` alone or before another flag
257    /// gives `None`, `--color=` gives `Some(b"")`. What an `Option` field
258    /// stores.
259    #[inline(always)]
260    pub fn read_value_opt_with(
261        &self,
262        input: &mut Argv<'i>,
263        options: ValueOptions,
264    ) -> Option<&'i BStr> {
265        self.next_value(input, options)
266    }
267
268    #[inline(always)]
269    fn next_value(&self, input: &mut Argv<'i>, options: ValueOptions) -> Option<&'i BStr> {
270        match self {
271            Arg::Long(LongFlag { value: Some(v), .. }) => Some(v),
272            Arg::Short(_) if input.mode() == Mode::Bundle => {
273                let v = input.take_word();
274                if options.keep_equals {
275                    return Some(v);
276                }
277                Some(BStr::new(v.strip_prefix(b"=").unwrap_or(v)))
278            }
279            _ => {
280                if input.is_empty() || options.require_equals {
281                    return None;
282                }
283                detached(input, options)
284            }
285        }
286    }
287
288    /// [`Arg::read_value`], converted with [`FromArg`].
289    #[inline]
290    pub fn read_value_as<T: FromArg>(&self, input: &mut Argv<'i>) -> Result<T, Error> {
291        let v = self.read_value(input)?;
292        self.convert(v)
293    }
294
295    /// Convert `value`, one of this flag's values, naming the flag on failure.
296    #[inline]
297    pub fn convert<T: FromArg>(&self, value: &BStr) -> Result<T, Error> {
298        T::from_arg(value)
299            .map_err(|cause| Error::invalid_value(self.offset(), self.spelling(), value, cause))
300    }
301
302    /// [`Arg::read_value`] split on `delimiter`, each piece converted.
303    pub fn values_as<T: FromArg>(&self, delimiter: u8) -> impl Parser<Argv<'i>, Vec<T>, Error> {
304        move |input: &mut Argv<'i>| {
305            let value = self.read_value(input)?;
306            split(value, delimiter).map(|v| self.convert(v)).collect()
307        }
308    }
309
310    /// [`Arg::check_switch`] as a parser, for a `dispatch!` arm.
311    pub fn switch(&self) -> impl Parser<Argv<'i>, (), Error> {
312        move |_: &mut Argv<'i>| self.check_switch()
313    }
314
315    /// [`Arg::read_value`] as a parser, for a `dispatch!` arm.
316    pub fn value(&self) -> impl Parser<Argv<'i>, &'i BStr, Error> {
317        move |input: &mut Argv<'i>| self.read_value(input)
318    }
319
320    /// [`Arg::read_value_or`], converted, as a parser for a `dispatch!` arm.
321    pub fn value_or<T: FromArg>(&self, missing: &'static BStr) -> impl Parser<Argv<'i>, T, Error> {
322        move |input: &mut Argv<'i>| {
323            let v = self.read_value_or(input, missing);
324            self.convert(v)
325        }
326    }
327
328    /// [`Arg::read_value_or_with`], converted, as a parser for a `dispatch!` arm.
329    pub fn value_or_with<T: FromArg>(
330        &self,
331        options: ValueOptions,
332        missing: &'static BStr,
333    ) -> impl Parser<Argv<'i>, T, Error> {
334        move |input: &mut Argv<'i>| {
335            let v = self.read_value_or_with(input, options, missing);
336            self.convert(v)
337        }
338    }
339
340    /// [`Arg::read_value_with`], converted, as a parser for a `dispatch!` arm.
341    pub fn value_as_with<T: FromArg>(
342        &self,
343        options: ValueOptions,
344    ) -> impl Parser<Argv<'i>, T, Error> {
345        move |input: &mut Argv<'i>| {
346            let v = self.read_value_with(input, options)?;
347            self.convert(v)
348        }
349    }
350
351    /// [`Arg::read_value_as`] as a parser, for a `dispatch!` arm.
352    pub fn value_as<T: FromArg>(&self) -> impl Parser<Argv<'i>, T, Error> {
353        move |input: &mut Argv<'i>| self.read_value_as(input)
354    }
355}
356
357/// The pieces of `value` between `delimiter`s; empty pieces are kept.
358#[inline]
359pub fn split(value: &BStr, delimiter: u8) -> impl Iterator<Item = &BStr> {
360    value.split(move |&b| b == delimiter).map(BStr::new)
361}
362
363impl<'i> Word<'i> {
364    /// The pieces of the word between `delimiter`s, each at its own offset.
365    #[inline]
366    pub fn split(&self, delimiter: u8) -> impl Iterator<Item = Word<'i>> {
367        let (base, after_separator) = (self.offset, self.after_separator);
368        let start = self.value.as_ptr() as usize;
369        split(self.value, delimiter).map(move |value| Word {
370            value,
371            offset: base + (value.as_ptr() as usize - start),
372            after_separator,
373        })
374    }
375
376    /// Convert the word with [`FromArg`]; `name` is how errors refer to it.
377    #[inline]
378    pub fn convert<T: FromArg>(&self, name: &str) -> Result<T, Error> {
379        T::from_arg(self.value)
380            .map_err(|cause| Error::invalid_value(self.offset, name, self.value, cause))
381    }
382
383    /// [`Word::convert`] as a parser, for a `dispatch!` arm.
384    pub fn value_as<T: FromArg>(&self, name: &'static str) -> impl Parser<Argv<'i>, T, Error> {
385        move |_: &mut Argv<'i>| self.convert(name)
386    }
387}
388
389/// The next word as a flag's detached value, unless it is flag-like and
390/// `options` does not take it anyway. The input is not empty.
391#[inline(always)]
392fn detached<'i>(input: &mut Argv<'i>, options: ValueOptions) -> Option<&'i BStr> {
393    let next = input.front();
394    if input.mode() == Mode::Word
395        && is_flag_like(next)
396        && !options.hyphen_values
397        && !(options.negative_numbers && is_negative_number(next))
398    {
399        return None;
400    }
401    Some(input.take_word())
402}
403
404/// How a flag takes a detached value, beyond the default of any word that is
405/// not flag-like.
406///
407/// More options may be added: build one from [`ValueOptions::DEFAULT`]
408/// (`ValueOptions { hyphen_values: true, ..ValueOptions::DEFAULT }`).
409#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
410pub struct ValueOptions {
411    /// A negative number is a value, not a flag: `--offset -1`.
412    pub negative_numbers: bool,
413    /// Any next word is the value, `--` and flag-like ones included: `--args -x`.
414    pub hyphen_values: bool,
415    /// Only an attached value: `--inspect=9229`, `-i9229`; the next word is never taken.
416    pub require_equals: bool,
417    /// A short option's attached value keeps a leading `=`: `-L=dir` is
418    /// `=dir`, as GNU ld reads it (a sysroot-relative path), not `dir`.
419    pub keep_equals: bool,
420}
421
422impl ValueOptions {
423    /// Any word that is not flag-like.
424    pub const DEFAULT: Self = Self {
425        negative_numbers: false,
426        hyphen_values: false,
427        require_equals: false,
428        keep_equals: false,
429    };
430}
431
432/// Whether `word` is a negative number: `-` then digits, at most one `.`, and an
433/// optional exponent (`e` or `E`, an optional sign, at least one digit). Narrower
434/// than a float parse on purpose: `-inf` and `-NaN` are likelier misspelled flags.
435#[inline]
436pub fn is_negative_number(word: &[u8]) -> bool {
437    let Some(rest) = word.strip_prefix(b"-") else {
438        return false;
439    };
440    let (mantissa, exponent) = match rest.iter().position(|b| matches!(b, b'e' | b'E')) {
441        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
442        None => (rest, None),
443    };
444    let (mut digit, mut dot) = (false, false);
445    for &b in mantissa {
446        match b {
447            b'0'..=b'9' => digit = true,
448            b'.' if !dot => dot = true,
449            _ => return false,
450        }
451    }
452    let exponent_ok = match exponent {
453        None => true,
454        Some(e) => {
455            let digits = e
456                .strip_prefix(b"+")
457                .or_else(|| e.strip_prefix(b"-"))
458                .unwrap_or(e);
459            !digits.is_empty() && digits.iter().all(u8::is_ascii_digit)
460        }
461    };
462    digit && exponent_ok
463}
464
465/// A negative number standing where a word starts, as a positional value: for a
466/// positional declared `allow_negative_numbers`, which would otherwise see a flag.
467pub fn number<'i>(input: &mut Argv<'i>) -> Result<Word<'i>, Error> {
468    if input.mode() != Mode::Word || !is_negative_number(input.front()) {
469        return Err(Error::from_input(input));
470    }
471    let offset = input.offset();
472    Ok(Word {
473        after_separator: false,
474        value: input.take_word(),
475        offset,
476    })
477}
478
479/// Whether a detached word would be read as a flag rather than a value.
480#[inline(always)]
481pub(crate) fn is_flag_like(word: &[u8]) -> bool {
482    word.len() > 1 && word[0] == b'-'
483}
484
485/// Read one short letter, and note whether the bundle goes on.
486#[inline(always)]
487fn letter(input: &mut Argv<'_>) -> char {
488    let bytes = input.front();
489    let (c, width) = match bytes[0] {
490        b if b.is_ascii() => (b as char, 1),
491        b => {
492            let width = match b {
493                0xC2..=0xDF => 2,
494                0xE0..=0xEF => 3,
495                0xF0..=0xF4 => 4,
496                _ => 1,
497            };
498            match bytes.get(..width).and_then(|b| std::str::from_utf8(b).ok()) {
499                Some(s) => (
500                    s.chars().next().unwrap_or(char::REPLACEMENT_CHARACTER),
501                    width,
502                ),
503                None => (char::REPLACEMENT_CHARACTER, 1),
504            }
505        }
506    };
507    input.take_bytes(width);
508    if input.front().is_empty() {
509        input.take_word();
510        input.set_mode(Mode::Word);
511    } else {
512        input.set_mode(Mode::Bundle);
513    }
514    c
515}
516
517/// What the next item is, without consuming it.
518#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
519pub enum Kind {
520    /// `--name` or `--name=value`.
521    Long,
522    /// A letter of `-abc`, at its start or part-way through.
523    Short,
524    /// A word that is not a flag.
525    Word,
526    /// The first `--`.
527    Separator,
528}
529
530/// Classify the next item without consuming it; backtracks at the end of the
531/// command line. Meant as a `dispatch!` scrutinee, so a word is only offered to
532/// positional parsers and a flag only to flag parsers.
533///
534/// `dispatch!` is a `move` closure: give it `&mut` references, or a `bool` it
535/// sets would be a copy.
536///
537/// ```
538/// use winnow::combinator::{alt, dispatch, fail};
539/// use winnow::prelude::*;
540/// use winnow_args::BStr;
541/// use winnow_args::combinator::{args, positional, short};
542/// use winnow_args::token::{Kind, kind};
543/// use winnow_args::{Argv, Error};
544///
545/// let words = [BStr::new("a"), BStr::new("-v"), BStr::new("b")];
546/// let (mut verbose, mut files) = (false, Vec::<String>::new());
547/// let (v, f) = (&mut verbose, &mut files);
548/// args(dispatch! {kind;
549///     Kind::Long | Kind::Short => alt((short('v').long("verbose").switch().map(|()| *v = true),)),
550///     Kind::Word => positional("FILE").map(|file| f.push(file)),
551///     Kind::Separator => fail,
552/// })
553/// .parse_next(&mut Argv::new(&words))?;
554/// assert!(verbose);
555/// assert_eq!(files, ["a", "b"]);
556/// # Ok::<(), Error>(())
557/// ```
558#[inline]
559pub fn kind(input: &mut Argv<'_>) -> Result<Kind, Error> {
560    if input.is_empty() {
561        return Err(Error::from_input(input));
562    }
563    Ok(match input.mode() {
564        Mode::Bundle => Kind::Short,
565        Mode::Stopped | Mode::Values => Kind::Word,
566        Mode::Word => match input.front() {
567            b"--" => Kind::Separator,
568            [b'-', b'-', ..] => Kind::Long,
569            [b'-', _, ..] => Kind::Short,
570            _ => Kind::Word,
571        },
572    })
573}
574
575/// Lex the next item. Backtracks at the end of the command line.
576// `always`: returned out of line, `Result<Arg, Error>` goes through memory and
577// stalls on store-to-load forwarding (docs/PERF.md, step 5).
578#[inline(always)]
579pub fn arg<'i>(input: &mut Argv<'i>) -> Result<Arg<'i>, Error> {
580    let kind = kind(input)?;
581    let offset = input.offset();
582    Ok(match kind {
583        Kind::Separator => {
584            input.take_word();
585            input.set_mode(Mode::Stopped);
586            Arg::Separator { offset }
587        }
588        Kind::Long => {
589            let word = &input.take_word()[2..];
590            Arg::Long(match word.iter().position(|&b| b == b'=') {
591                Some(eq) => LongFlag {
592                    name: &word[..eq],
593                    value: Some(BStr::new(&word[eq + 1..])),
594                    offset,
595                },
596                None => LongFlag {
597                    name: word,
598                    value: None,
599                    offset,
600                },
601            })
602        }
603        Kind::Short => {
604            // A bundle's first letter is reported at its `-`, later ones at themselves.
605            let plus = if input.mode() == Mode::Word {
606                input.take_bytes(1);
607                input.set_plus(false);
608                false
609            } else {
610                input.plus()
611            };
612            Arg::Short(ShortFlag {
613                letter: letter(input),
614                plus,
615                offset,
616            })
617        }
618        Kind::Word => Arg::Word(Word {
619            after_separator: input.mode() == Mode::Stopped,
620            value: input.take_word(),
621            offset,
622        }),
623    })
624}
625
626/// A word that is not a flag: a positional value.
627pub fn word<'i>(input: &mut Argv<'i>) -> Result<Word<'i>, Error> {
628    if kind(input)? != Kind::Word {
629        return Err(Error::from_input(input));
630    }
631    let offset = input.offset();
632    Ok(Word {
633        after_separator: input.mode() == Mode::Stopped,
634        value: input.take_word(),
635        offset,
636    })
637}
638
639/// A flag-like word taken whole as a value, for `unknown_flags = "value"`:
640/// the last branch of an `alt`, after every flag. Only a whole word: once a
641/// bundle has bound a letter, an unknown one after it is still an error.
642pub fn flag_word<'i>(input: &mut Argv<'i>) -> Result<Word<'i>, Error> {
643    let front = input.front();
644    if input.is_empty() || input.mode() != Mode::Word || !is_flag_like(front) || front == b"--" {
645        return Err(Error::from_input(input));
646    }
647    let offset = input.offset();
648    Ok(Word {
649        after_separator: false,
650        value: input.take_word(),
651        offset,
652    })
653}
654
655/// [`arg`] for a command that also takes `+` options (`set +e`,
656/// `declare +x`): a word `+abc` is a bundle like `-abc`, its letters reported
657/// with [`ShortFlag::plus`] set. A lone `+` is a word.
658#[inline(always)]
659pub fn arg_plus<'i>(input: &mut Argv<'i>) -> Result<Arg<'i>, Error> {
660    if input.mode() == Mode::Word && !input.is_empty() {
661        let front = input.front();
662        if front.len() > 1 && front[0] == b'+' {
663            let offset = input.offset();
664            input.take_bytes(1);
665            input.set_plus(true);
666            return Ok(Arg::Short(ShortFlag {
667                letter: letter(input),
668                plus: true,
669                offset,
670            }));
671        }
672    }
673    arg(input)
674}
675
676/// GNU's `getopt_long_only`: a single-dash word whose name (up to the first
677/// `=`) `is_long` accepts is a long option, `-entry=main` as `--entry=main`,
678/// tried before the word is read as a bundle of short options. `None`,
679/// consuming nothing, for anything else, which [`arg`] then lexes as usual.
680///
681/// `is_long` decides which names may be spelled with one dash: `ld` keeps some
682/// to two (`--omagic`, since `-omagic` is `-o magic`), and a letter that always
683/// takes the rest of the word (`-lfoo`) wins over any long name it starts.
684#[inline]
685pub fn long_only<'i>(input: &mut Argv<'i>, is_long: impl Fn(&[u8]) -> bool) -> Option<Arg<'i>> {
686    if input.is_empty() || input.mode() != Mode::Word {
687        return None;
688    }
689    let body = input.front().strip_prefix(b"-")?;
690    if body.len() < 2 || body[0] == b'-' {
691        return None;
692    }
693    let (name, value) = match body.iter().position(|&b| b == b'=') {
694        Some(eq) => (&body[..eq], Some(&body[eq + 1..])),
695        None => (body, None),
696    };
697    if !is_long(name) {
698        return None;
699    }
700    let offset = input.offset();
701    let word = &input.take_word()[1..];
702    let name = &word[..name.len()];
703    let value = value.map(|_| BStr::new(&word[name.len() + 1..]));
704    Some(Arg::Long(LongFlag {
705        name,
706        value,
707        offset,
708    }))
709}
710
711/// `--` as a word, for a positional declared `double_dash = "preserve"`: the
712/// separator is its value and flags go on being flags.
713pub fn separator_word<'i>(input: &mut Argv<'i>) -> Result<Word<'i>, Error> {
714    if kind(input)? != Kind::Separator {
715        return Err(Error::from_input(input));
716    }
717    let offset = input.offset();
718    Ok(Word {
719        after_separator: false,
720        value: input.take_word(),
721        offset,
722    })
723}
724
725/// Succeed at the end of the command line; otherwise report what is left over.
726pub fn finish(input: &mut Argv<'_>) -> Result<(), Error> {
727    if input.is_empty() {
728        return Ok(());
729    }
730    Err(arg(input)?.unexpected().cut())
731}