Skip to main content

winnow_args/
stream.rs

1//! The input: `argv` as a slice of words.
2//!
3//! [`Argv`] is a winnow [`Stream`] whose tokens are the words of the command
4//! line, borrowed as [`BStr`], so nothing is copied or joined and any byte,
5//! NUL included, can appear in a word. A word is never re-split: `"a b"` is
6//! one token, the same as the shell handed it over.
7//!
8//! Offsets are counted in bytes as if every word were followed by one
9//! separator, so a position inside a bundle of short flags (`-vq`) is still
10//! a single number that shrinks as the lexer reads a letter. That keeps
11//! `repeat`'s "parser must consume" check and error offsets meaningful.
12
13use std::ffi::OsStr;
14
15use winnow::stream::{BStr, Location, Needed, Offset, SliceLen, Stream, StreamIsPartial};
16
17/// Borrow each argument as a [`BStr`] word.
18pub fn words<S: AsRef<OsStr>>(args: &[S]) -> Vec<&BStr> {
19    args.iter()
20        .map(|arg| BStr::new(arg.as_ref().as_encoded_bytes()))
21        .collect()
22}
23
24/// Where in the grammar a position is.
25#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
26#[non_exhaustive]
27pub enum Mode {
28    /// At the start of a word, and flags are still recognized.
29    Word,
30    /// Inside a bundle of short flags: the rest of the current word is more
31    /// letters, or the attached value of the letter just read.
32    Bundle,
33    /// Past a `--`: every word is a value.
34    Stopped,
35    /// Every word is a value, but no `--` was typed: an argument declared
36    /// `double_dash = "automatic"` or `stop_flags` has taken a value ([`Argv::stop_flags`]).
37    Values,
38}
39
40/// A command line being parsed: the [`Stream`] every parser in this crate reads.
41///
42/// Its checkpoint is the whole state, so `alt` backtracking restores the
43/// [`Mode`] and the position inside a bundle along with the word.
44#[derive(Copy, Clone, PartialEq, Eq)]
45pub struct Argv<'i> {
46    /// Unfinished words; `words[0]` is the current one.
47    words: &'i [&'i BStr],
48    /// Bytes of `words[0]` already read, when inside a bundle.
49    skip: u32,
50    /// Offset to the end: unread bytes plus one separator per unfinished word.
51    remaining: u32,
52    total: u32,
53    mode: Mode,
54    /// The bundle being read is a `+` bundle (`set +eu`); meaningful only in
55    /// [`Mode::Bundle`]. Sits in padding: `Argv` stays 32 bytes.
56    plus: bool,
57}
58
59// The checkpoint is the whole `Argv`, copied on every `alt` branch: kept at
60// two 16-byte moves (docs/PERF.md, step 5). Smaller where pointers are 32 bits.
61const _: () = assert!(std::mem::size_of::<Argv<'static>>() <= 32);
62
63impl<'i> Argv<'i> {
64    /// Parse `words`, which should not include the program name.
65    pub fn new(words: &'i [&'i BStr]) -> Self {
66        let total: usize = words.iter().map(|w| w.len() + 1).sum();
67        // Kept narrow so a checkpoint is two 16-byte moves; argv is capped far below this.
68        let total = u32::try_from(total).expect("a command line under 4 GiB");
69        Self {
70            words,
71            skip: 0,
72            remaining: total,
73            total,
74            mode: Mode::Word,
75            plus: false,
76        }
77    }
78
79    /// Whether every word has been consumed.
80    #[inline(always)]
81    pub fn is_empty(&self) -> bool {
82        self.words.is_empty()
83    }
84
85    /// How many words are left, the current one included.
86    #[inline(always)]
87    pub fn words_left(&self) -> usize {
88        self.words.len()
89    }
90
91    /// Where in the grammar the stream is.
92    #[inline(always)]
93    pub fn mode(&self) -> Mode {
94        self.mode
95    }
96
97    /// Read every remaining word as a value, as if `--` had been typed. An
98    /// argument declared `double_dash = "automatic"` or `stop_flags` does this
99    /// once it has a value.
100    #[inline]
101    pub fn stop_flags(&mut self) {
102        if self.mode == Mode::Word {
103            self.mode = Mode::Values;
104        }
105    }
106
107    /// Recognize flags again after `--` or [`Argv::stop_flags`]: a command's
108    /// `restart_token` starts a fresh invocation.
109    #[inline]
110    pub fn resume_flags(&mut self) {
111        if matches!(self.mode, Mode::Stopped | Mode::Values) {
112            self.mode = Mode::Word;
113        }
114    }
115
116    #[inline(always)]
117    pub(crate) fn set_mode(&mut self, mode: Mode) {
118        self.mode = mode;
119    }
120
121    /// Whether the bundle being read is a `+` bundle.
122    #[inline(always)]
123    pub(crate) fn plus(&self) -> bool {
124        self.plus
125    }
126
127    #[inline(always)]
128    pub(crate) fn set_plus(&mut self, plus: bool) {
129        self.plus = plus;
130    }
131
132    /// Byte offset from the start of the command line, one separator counted per word.
133    #[inline(always)]
134    pub fn offset(&self) -> usize {
135        (self.total - self.remaining) as usize
136    }
137
138    /// The unread part of the current word; empty at the end.
139    #[inline(always)]
140    pub fn front(&self) -> &'i [u8] {
141        match self.words.first() {
142            Some(word) => &word[self.skip as usize..],
143            None => &[],
144        }
145    }
146
147    /// Read `n` bytes of the current word, staying inside it.
148    #[inline(always)]
149    pub(crate) fn take_bytes(&mut self, n: usize) {
150        debug_assert!(n < self.front().len() + 1);
151        self.skip += n as u32;
152        self.remaining -= n as u32;
153    }
154
155    /// Read the rest of the current word and move to the next one.
156    #[inline(always)]
157    pub(crate) fn take_word(&mut self) -> &'i BStr {
158        let rest = self.front();
159        self.remaining -= rest.len() as u32 + 1;
160        self.words = &self.words[1..];
161        self.skip = 0;
162        if self.mode == Mode::Bundle {
163            self.mode = Mode::Word;
164        }
165        BStr::new(rest)
166    }
167
168    /// Number of whole words spanning `offset`, which must fall on a word boundary.
169    fn words_in(&self, offset: usize) -> usize {
170        let mut end = 0;
171        for (n, (at, _)) in self.iter_offsets().enumerate() {
172            if at == offset {
173                return n;
174            }
175            end = at;
176        }
177        assert_eq!(
178            offset, self.remaining as usize,
179            "offset {offset} is inside a word (last start {end})"
180        );
181        self.words.len()
182    }
183}
184
185impl std::fmt::Debug for Argv<'_> {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        let mut words: Vec<&BStr> = self.words.to_vec();
188        if let Some(first) = words.first_mut() {
189            *first = BStr::new(&first[self.skip as usize..]);
190        }
191        f.debug_struct("Argv")
192            .field("mode", &self.mode)
193            .field("words", &words)
194            .finish()
195    }
196}
197
198/// Iterator over the remaining words with their offsets.
199#[derive(Clone, Debug)]
200pub struct IterOffsets<'i> {
201    words: std::slice::Iter<'i, &'i BStr>,
202    skip: usize,
203    offset: usize,
204}
205
206impl<'i> Iterator for IterOffsets<'i> {
207    type Item = (usize, &'i BStr);
208
209    fn next(&mut self) -> Option<Self::Item> {
210        let word = BStr::new(&self.words.next()?[self.skip..]);
211        self.skip = 0;
212        let at = self.offset;
213        self.offset += word.len() + 1;
214        Some((at, word))
215    }
216}
217
218impl Offset for Argv<'_> {
219    #[inline(always)]
220    fn offset_from(&self, start: &Self) -> usize {
221        (start.remaining - self.remaining) as usize
222    }
223}
224
225impl SliceLen for Argv<'_> {
226    #[inline(always)]
227    fn slice_len(&self) -> usize {
228        self.remaining as usize
229    }
230}
231
232/// Tokens are words (the unread part of the current one first); slices are
233/// runs of whole words, so `next_slice` must not be asked to split a word.
234impl<'i> Stream for Argv<'i> {
235    type Token = &'i BStr;
236    type Slice = &'i [&'i BStr];
237    type IterOffsets = IterOffsets<'i>;
238    type Checkpoint = Self;
239
240    #[inline(always)]
241    fn iter_offsets(&self) -> Self::IterOffsets {
242        IterOffsets {
243            words: self.words.iter(),
244            skip: self.skip as usize,
245            offset: 0,
246        }
247    }
248    #[inline(always)]
249    fn eof_offset(&self) -> usize {
250        self.remaining as usize
251    }
252    #[inline(always)]
253    fn next_token(&mut self) -> Option<Self::Token> {
254        if self.is_empty() {
255            None
256        } else {
257            Some(self.take_word())
258        }
259    }
260    #[inline(always)]
261    fn peek_token(&self) -> Option<Self::Token> {
262        (!self.is_empty()).then(|| BStr::new(self.front()))
263    }
264    fn offset_for<P>(&self, predicate: P) -> Option<usize>
265    where
266        P: Fn(Self::Token) -> bool,
267    {
268        self.iter_offsets()
269            .find(|&(_, word)| predicate(word))
270            .map(|(at, _)| at)
271    }
272    fn offset_at(&self, tokens: usize) -> Result<usize, Needed> {
273        let mut iter = self.iter_offsets();
274        match iter.nth(tokens) {
275            Some((at, _)) => Ok(at),
276            None if tokens == self.words.len() => Ok(self.remaining as usize),
277            None => Err(Needed::new(tokens - self.words.len())),
278        }
279    }
280    fn next_slice(&mut self, offset: usize) -> Self::Slice {
281        let slice = self.peek_slice(offset);
282        for _ in 0..slice.len() {
283            self.take_word();
284        }
285        slice
286    }
287    fn peek_slice(&self, offset: usize) -> Self::Slice {
288        assert!(
289            self.skip == 0 || offset == 0,
290            "cannot slice whole words inside a bundle"
291        );
292        &self.words[..self.words_in(offset)]
293    }
294    #[inline(always)]
295    fn checkpoint(&self) -> Self::Checkpoint {
296        *self
297    }
298    #[inline(always)]
299    fn reset(&mut self, checkpoint: &Self::Checkpoint) {
300        *self = *checkpoint;
301    }
302    fn trace(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        write!(f, "{self:?}")
304    }
305}
306
307impl StreamIsPartial for Argv<'_> {
308    type PartialState = ();
309
310    #[inline(always)]
311    fn complete(&mut self) -> Self::PartialState {}
312    #[inline(always)]
313    fn restore_partial(&mut self, _state: Self::PartialState) {}
314    #[inline(always)]
315    fn is_partial_supported() -> bool {
316        false
317    }
318}
319
320impl Location for Argv<'_> {
321    #[inline(always)]
322    fn previous_token_end(&self) -> usize {
323        self.offset()
324    }
325    #[inline(always)]
326    fn current_token_start(&self) -> usize {
327        self.offset()
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use winnow::combinator::repeat;
334    use winnow::prelude::*;
335    use winnow::token::{any, take};
336
337    use super::*;
338
339    fn line<'a>(words: &'a [&'a str]) -> Vec<&'a BStr> {
340        words.iter().map(BStr::new).collect()
341    }
342
343    #[test]
344    fn generic_token_parsers_see_whole_words() {
345        let words = line(&["a b", "", "c"]);
346        let mut input = Argv::new(&words);
347        assert_eq!(
348            any::<_, crate::Error>.parse_next(&mut input).unwrap(),
349            "a b"
350        );
351        assert_eq!(input.offset(), 4);
352        let rest: &[&BStr] = take::<_, _, crate::Error>(2usize)
353            .parse_next(&mut input)
354            .unwrap();
355        assert_eq!(rest, &words[1..]);
356        assert!(input.is_empty());
357    }
358
359    #[test]
360    fn a_flag_matches_beside_a_word_holding_any_byte() {
361        let words = [BStr::new(b"--verbose"), BStr::new(b"a\0\xff")];
362        let mut input = Argv::new(&words);
363        let flag = crate::token::arg(&mut input).unwrap();
364        assert!(matches!(flag, crate::token::Arg::Long(f) if f.name == b"verbose"));
365        let word = crate::token::arg(&mut input).unwrap();
366        assert!(matches!(word, crate::token::Arg::Word(w) if w.value == &b"a\0\xff"[..]));
367    }
368
369    #[test]
370    fn a_multibyte_short_is_one_letter() {
371        let words = [BStr::new("-β".as_bytes())];
372        let mut input = Argv::new(&words);
373        let flag = crate::token::arg(&mut input).unwrap();
374        assert!(matches!(flag, crate::token::Arg::Short(f) if f.letter == 'β'));
375        assert!(input.is_empty());
376    }
377
378    #[test]
379    fn reading_a_letter_counts_as_progress() {
380        let words = line(&["-abc"]);
381        let mut input = Argv::new(&words);
382        let letters: Vec<_> = repeat(0.., crate::token::arg)
383            .parse_next(&mut input)
384            .unwrap();
385        assert_eq!(letters.len(), 3);
386        assert!(input.is_empty());
387    }
388
389    #[test]
390    fn checkpoint_restores_the_position_inside_a_bundle() {
391        let words = line(&["-ab", "x"]);
392        let mut input = Argv::new(&words);
393        crate::token::arg(&mut input).unwrap();
394        let saved = input.checkpoint();
395        crate::token::arg(&mut input).unwrap();
396        crate::token::arg(&mut input).unwrap();
397        input.reset(&saved);
398        assert_eq!((input.mode(), input.front()), (Mode::Bundle, &b"b"[..]));
399    }
400}