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    #[inline(always)]
256    fn next_value(&self, input: &mut Argv<'i>, options: ValueOptions) -> Option<&'i BStr> {
257        match self {
258            Arg::Long(LongFlag { value: Some(v), .. }) => Some(v),
259            Arg::Short(_) if input.mode() == Mode::Bundle => {
260                let v = input.take_word();
261                if options.keep_equals {
262                    return Some(v);
263                }
264                Some(BStr::new(v.strip_prefix(b"=").unwrap_or(v)))
265            }
266            _ => {
267                if input.is_empty() || options.require_equals {
268                    return None;
269                }
270                detached(input, options)
271            }
272        }
273    }
274
275    /// [`Arg::read_value`], converted with [`FromArg`].
276    #[inline]
277    pub fn read_value_as<T: FromArg>(&self, input: &mut Argv<'i>) -> Result<T, Error> {
278        let v = self.read_value(input)?;
279        self.convert(v)
280    }
281
282    /// Convert `value`, one of this flag's values, naming the flag on failure.
283    #[inline]
284    pub fn convert<T: FromArg>(&self, value: &BStr) -> Result<T, Error> {
285        T::from_arg(value)
286            .map_err(|cause| Error::invalid_value(self.offset(), self.spelling(), value, cause))
287    }
288
289    /// [`Arg::read_value`] split on `delimiter`, each piece converted.
290    pub fn values_as<T: FromArg>(&self, delimiter: u8) -> impl Parser<Argv<'i>, Vec<T>, Error> {
291        move |input: &mut Argv<'i>| {
292            let value = self.read_value(input)?;
293            split(value, delimiter).map(|v| self.convert(v)).collect()
294        }
295    }
296
297    /// [`Arg::check_switch`] as a parser, for a `dispatch!` arm.
298    pub fn switch(&self) -> impl Parser<Argv<'i>, (), Error> {
299        move |_: &mut Argv<'i>| self.check_switch()
300    }
301
302    /// [`Arg::read_value`] as a parser, for a `dispatch!` arm.
303    pub fn value(&self) -> impl Parser<Argv<'i>, &'i BStr, Error> {
304        move |input: &mut Argv<'i>| self.read_value(input)
305    }
306
307    /// [`Arg::read_value_or`], converted, as a parser for a `dispatch!` arm.
308    pub fn value_or<T: FromArg>(&self, missing: &'static BStr) -> impl Parser<Argv<'i>, T, Error> {
309        move |input: &mut Argv<'i>| {
310            let v = self.read_value_or(input, missing);
311            self.convert(v)
312        }
313    }
314
315    /// [`Arg::read_value_or_with`], converted, as a parser for a `dispatch!` arm.
316    pub fn value_or_with<T: FromArg>(
317        &self,
318        options: ValueOptions,
319        missing: &'static BStr,
320    ) -> impl Parser<Argv<'i>, T, Error> {
321        move |input: &mut Argv<'i>| {
322            let v = self.read_value_or_with(input, options, missing);
323            self.convert(v)
324        }
325    }
326
327    /// [`Arg::read_value_with`], converted, as a parser for a `dispatch!` arm.
328    pub fn value_as_with<T: FromArg>(
329        &self,
330        options: ValueOptions,
331    ) -> impl Parser<Argv<'i>, T, Error> {
332        move |input: &mut Argv<'i>| {
333            let v = self.read_value_with(input, options)?;
334            self.convert(v)
335        }
336    }
337
338    /// [`Arg::read_value_as`] as a parser, for a `dispatch!` arm.
339    pub fn value_as<T: FromArg>(&self) -> impl Parser<Argv<'i>, T, Error> {
340        move |input: &mut Argv<'i>| self.read_value_as(input)
341    }
342}
343
344/// The pieces of `value` between `delimiter`s; empty pieces are kept.
345#[inline]
346pub fn split(value: &BStr, delimiter: u8) -> impl Iterator<Item = &BStr> {
347    value.split(move |&b| b == delimiter).map(BStr::new)
348}
349
350impl<'i> Word<'i> {
351    /// The pieces of the word between `delimiter`s, each at its own offset.
352    #[inline]
353    pub fn split(&self, delimiter: u8) -> impl Iterator<Item = Word<'i>> {
354        let (base, after_separator) = (self.offset, self.after_separator);
355        let start = self.value.as_ptr() as usize;
356        split(self.value, delimiter).map(move |value| Word {
357            value,
358            offset: base + (value.as_ptr() as usize - start),
359            after_separator,
360        })
361    }
362
363    /// Convert the word with [`FromArg`]; `name` is how errors refer to it.
364    #[inline]
365    pub fn convert<T: FromArg>(&self, name: &str) -> Result<T, Error> {
366        T::from_arg(self.value)
367            .map_err(|cause| Error::invalid_value(self.offset, name, self.value, cause))
368    }
369
370    /// [`Word::convert`] as a parser, for a `dispatch!` arm.
371    pub fn value_as<T: FromArg>(&self, name: &'static str) -> impl Parser<Argv<'i>, T, Error> {
372        move |_: &mut Argv<'i>| self.convert(name)
373    }
374}
375
376/// The next word as a flag's detached value, unless it is flag-like and
377/// `options` does not take it anyway. The input is not empty.
378#[inline(always)]
379fn detached<'i>(input: &mut Argv<'i>, options: ValueOptions) -> Option<&'i BStr> {
380    let next = input.front();
381    if input.mode() == Mode::Word
382        && is_flag_like(next)
383        && !options.hyphen_values
384        && !(options.negative_numbers && is_negative_number(next))
385    {
386        return None;
387    }
388    Some(input.take_word())
389}
390
391/// How a flag takes a detached value, beyond the default of any word that is
392/// not flag-like.
393///
394/// More options may be added: build one from [`ValueOptions::DEFAULT`]
395/// (`ValueOptions { hyphen_values: true, ..ValueOptions::DEFAULT }`).
396#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
397pub struct ValueOptions {
398    /// A negative number is a value, not a flag: `--offset -1`.
399    pub negative_numbers: bool,
400    /// Any next word is the value, `--` and flag-like ones included: `--args -x`.
401    pub hyphen_values: bool,
402    /// Only an attached value: `--inspect=9229`, `-i9229`; the next word is never taken.
403    pub require_equals: bool,
404    /// A short option's attached value keeps a leading `=`: `-L=dir` is
405    /// `=dir`, as GNU ld reads it (a sysroot-relative path), not `dir`.
406    pub keep_equals: bool,
407}
408
409impl ValueOptions {
410    /// Any word that is not flag-like.
411    pub const DEFAULT: Self = Self {
412        negative_numbers: false,
413        hyphen_values: false,
414        require_equals: false,
415        keep_equals: false,
416    };
417}
418
419/// Whether `word` is a negative number: `-` then digits, at most one `.`, and an
420/// optional exponent (`e` or `E`, an optional sign, at least one digit). Narrower
421/// than a float parse on purpose: `-inf` and `-NaN` are likelier misspelled flags.
422#[inline]
423pub fn is_negative_number(word: &[u8]) -> bool {
424    let Some(rest) = word.strip_prefix(b"-") else {
425        return false;
426    };
427    let (mantissa, exponent) = match rest.iter().position(|b| matches!(b, b'e' | b'E')) {
428        Some(at) => (&rest[..at], Some(&rest[at + 1..])),
429        None => (rest, None),
430    };
431    let (mut digit, mut dot) = (false, false);
432    for &b in mantissa {
433        match b {
434            b'0'..=b'9' => digit = true,
435            b'.' if !dot => dot = true,
436            _ => return false,
437        }
438    }
439    let exponent_ok = match exponent {
440        None => true,
441        Some(e) => {
442            let digits = e
443                .strip_prefix(b"+")
444                .or_else(|| e.strip_prefix(b"-"))
445                .unwrap_or(e);
446            !digits.is_empty() && digits.iter().all(u8::is_ascii_digit)
447        }
448    };
449    digit && exponent_ok
450}
451
452/// A negative number standing where a word starts, as a positional value: for a
453/// positional declared `allow_negative_numbers`, which would otherwise see a flag.
454pub fn number<'i>(input: &mut Argv<'i>) -> Result<Word<'i>, Error> {
455    if input.mode() != Mode::Word || !is_negative_number(input.front()) {
456        return Err(Error::from_input(input));
457    }
458    let offset = input.offset();
459    Ok(Word {
460        after_separator: false,
461        value: input.take_word(),
462        offset,
463    })
464}
465
466/// Whether a detached word would be read as a flag rather than a value.
467#[inline(always)]
468pub(crate) fn is_flag_like(word: &[u8]) -> bool {
469    word.len() > 1 && word[0] == b'-'
470}
471
472/// Read one short letter, and note whether the bundle goes on.
473#[inline(always)]
474fn letter(input: &mut Argv<'_>) -> char {
475    let bytes = input.front();
476    let (c, width) = match bytes[0] {
477        b if b.is_ascii() => (b as char, 1),
478        b => {
479            let width = match b {
480                0xC2..=0xDF => 2,
481                0xE0..=0xEF => 3,
482                0xF0..=0xF4 => 4,
483                _ => 1,
484            };
485            match bytes.get(..width).and_then(|b| std::str::from_utf8(b).ok()) {
486                Some(s) => (
487                    s.chars().next().unwrap_or(char::REPLACEMENT_CHARACTER),
488                    width,
489                ),
490                None => (char::REPLACEMENT_CHARACTER, 1),
491            }
492        }
493    };
494    input.take_bytes(width);
495    if input.front().is_empty() {
496        input.take_word();
497        input.set_mode(Mode::Word);
498    } else {
499        input.set_mode(Mode::Bundle);
500    }
501    c
502}
503
504/// What the next item is, without consuming it.
505#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
506pub enum Kind {
507    /// `--name` or `--name=value`.
508    Long,
509    /// A letter of `-abc`, at its start or part-way through.
510    Short,
511    /// A word that is not a flag.
512    Word,
513    /// The first `--`.
514    Separator,
515}
516
517/// Classify the next item without consuming it; backtracks at the end of the
518/// command line. Meant as a `dispatch!` scrutinee, so a word is only offered to
519/// positional parsers and a flag only to flag parsers.
520///
521/// `dispatch!` is a `move` closure: give it `&mut` references, or a `bool` it
522/// sets would be a copy.
523///
524/// ```
525/// use winnow::combinator::{alt, dispatch, fail};
526/// use winnow::prelude::*;
527/// use winnow_args::BStr;
528/// use winnow_args::combinator::{args, positional, short};
529/// use winnow_args::token::{Kind, kind};
530/// use winnow_args::{Argv, Error};
531///
532/// let words = [BStr::new("a"), BStr::new("-v"), BStr::new("b")];
533/// let (mut verbose, mut files) = (false, Vec::<String>::new());
534/// let (v, f) = (&mut verbose, &mut files);
535/// args(dispatch! {kind;
536///     Kind::Long | Kind::Short => alt((short('v').long("verbose").switch().map(|()| *v = true),)),
537///     Kind::Word => positional("FILE").map(|file| f.push(file)),
538///     Kind::Separator => fail,
539/// })
540/// .parse_next(&mut Argv::new(&words))?;
541/// assert!(verbose);
542/// assert_eq!(files, ["a", "b"]);
543/// # Ok::<(), Error>(())
544/// ```
545#[inline]
546pub fn kind(input: &mut Argv<'_>) -> Result<Kind, Error> {
547    if input.is_empty() {
548        return Err(Error::from_input(input));
549    }
550    Ok(match input.mode() {
551        Mode::Bundle => Kind::Short,
552        Mode::Stopped | Mode::Values => Kind::Word,
553        Mode::Word => match input.front() {
554            b"--" => Kind::Separator,
555            [b'-', b'-', ..] => Kind::Long,
556            [b'-', _, ..] => Kind::Short,
557            _ => Kind::Word,
558        },
559    })
560}
561
562/// Lex the next item. Backtracks at the end of the command line.
563// `always`: returned out of line, `Result<Arg, Error>` goes through memory and
564// stalls on store-to-load forwarding (docs/PERF.md, step 5).
565#[inline(always)]
566pub fn arg<'i>(input: &mut Argv<'i>) -> Result<Arg<'i>, Error> {
567    let kind = kind(input)?;
568    let offset = input.offset();
569    Ok(match kind {
570        Kind::Separator => {
571            input.take_word();
572            input.set_mode(Mode::Stopped);
573            Arg::Separator { offset }
574        }
575        Kind::Long => {
576            let word = &input.take_word()[2..];
577            Arg::Long(match word.iter().position(|&b| b == b'=') {
578                Some(eq) => LongFlag {
579                    name: &word[..eq],
580                    value: Some(BStr::new(&word[eq + 1..])),
581                    offset,
582                },
583                None => LongFlag {
584                    name: word,
585                    value: None,
586                    offset,
587                },
588            })
589        }
590        Kind::Short => {
591            // A bundle's first letter is reported at its `-`, later ones at themselves.
592            let plus = if input.mode() == Mode::Word {
593                input.take_bytes(1);
594                input.set_plus(false);
595                false
596            } else {
597                input.plus()
598            };
599            Arg::Short(ShortFlag {
600                letter: letter(input),
601                plus,
602                offset,
603            })
604        }
605        Kind::Word => Arg::Word(Word {
606            after_separator: input.mode() == Mode::Stopped,
607            value: input.take_word(),
608            offset,
609        }),
610    })
611}
612
613/// A word that is not a flag: a positional value.
614pub fn word<'i>(input: &mut Argv<'i>) -> Result<Word<'i>, Error> {
615    if kind(input)? != Kind::Word {
616        return Err(Error::from_input(input));
617    }
618    let offset = input.offset();
619    Ok(Word {
620        after_separator: input.mode() == Mode::Stopped,
621        value: input.take_word(),
622        offset,
623    })
624}
625
626/// A flag-like word taken whole as a value, for `unknown_flags = "value"`:
627/// the last branch of an `alt`, after every flag. Only a whole word: once a
628/// bundle has bound a letter, an unknown one after it is still an error.
629pub fn flag_word<'i>(input: &mut Argv<'i>) -> Result<Word<'i>, Error> {
630    let front = input.front();
631    if input.is_empty() || input.mode() != Mode::Word || !is_flag_like(front) || front == b"--" {
632        return Err(Error::from_input(input));
633    }
634    let offset = input.offset();
635    Ok(Word {
636        after_separator: false,
637        value: input.take_word(),
638        offset,
639    })
640}
641
642/// [`arg`] for a command that also takes `+` options (`set +e`,
643/// `declare +x`): a word `+abc` is a bundle like `-abc`, its letters reported
644/// with [`ShortFlag::plus`] set. A lone `+` is a word.
645#[inline(always)]
646pub fn arg_plus<'i>(input: &mut Argv<'i>) -> Result<Arg<'i>, Error> {
647    if input.mode() == Mode::Word && !input.is_empty() {
648        let front = input.front();
649        if front.len() > 1 && front[0] == b'+' {
650            let offset = input.offset();
651            input.take_bytes(1);
652            input.set_plus(true);
653            return Ok(Arg::Short(ShortFlag {
654                letter: letter(input),
655                plus: true,
656                offset,
657            }));
658        }
659    }
660    arg(input)
661}
662
663/// GNU's `getopt_long_only`: a single-dash word whose name (up to the first
664/// `=`) `is_long` accepts is a long option, `-entry=main` as `--entry=main`,
665/// tried before the word is read as a bundle of short options. `None`,
666/// consuming nothing, for anything else, which [`arg`] then lexes as usual.
667///
668/// `is_long` decides which names may be spelled with one dash: `ld` keeps some
669/// to two (`--omagic`, since `-omagic` is `-o magic`), and a letter that always
670/// takes the rest of the word (`-lfoo`) wins over any long name it starts.
671#[inline]
672pub fn long_only<'i>(input: &mut Argv<'i>, is_long: impl Fn(&[u8]) -> bool) -> Option<Arg<'i>> {
673    if input.is_empty() || input.mode() != Mode::Word {
674        return None;
675    }
676    let body = input.front().strip_prefix(b"-")?;
677    if body.len() < 2 || body[0] == b'-' {
678        return None;
679    }
680    let (name, value) = match body.iter().position(|&b| b == b'=') {
681        Some(eq) => (&body[..eq], Some(&body[eq + 1..])),
682        None => (body, None),
683    };
684    if !is_long(name) {
685        return None;
686    }
687    let offset = input.offset();
688    let word = &input.take_word()[1..];
689    let name = &word[..name.len()];
690    let value = value.map(|_| BStr::new(&word[name.len() + 1..]));
691    Some(Arg::Long(LongFlag {
692        name,
693        value,
694        offset,
695    }))
696}
697
698/// `--` as a word, for a positional declared `double_dash = "preserve"`: the
699/// separator is its value and flags go on being flags.
700pub fn separator_word<'i>(input: &mut Argv<'i>) -> Result<Word<'i>, Error> {
701    if kind(input)? != Kind::Separator {
702        return Err(Error::from_input(input));
703    }
704    let offset = input.offset();
705    Ok(Word {
706        after_separator: false,
707        value: input.take_word(),
708        offset,
709    })
710}
711
712/// Succeed at the end of the command line; otherwise report what is left over.
713pub fn finish(input: &mut Argv<'_>) -> Result<(), Error> {
714    if input.is_empty() {
715        return Ok(());
716    }
717    Err(arg(input)?.unexpected().cut())
718}