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    /// Take a `--` that [`Argv::stop_flags`] made a word for the separator
108    /// it is: the words after it are those after a `--`.
109    #[doc(hidden)]
110    #[inline]
111    pub fn separate(&mut self) {
112        if self.mode == Mode::Values {
113            self.mode = Mode::Stopped;
114        }
115    }
116
117    /// Recognize flags again after `--` or [`Argv::stop_flags`]: a command's
118    /// `restart_token` starts a fresh invocation.
119    #[inline]
120    pub fn resume_flags(&mut self) {
121        if matches!(self.mode, Mode::Stopped | Mode::Values) {
122            self.mode = Mode::Word;
123        }
124    }
125
126    #[inline(always)]
127    pub(crate) fn set_mode(&mut self, mode: Mode) {
128        self.mode = mode;
129    }
130
131    /// Whether the bundle being read is a `+` bundle.
132    #[inline(always)]
133    pub(crate) fn plus(&self) -> bool {
134        self.plus
135    }
136
137    #[inline(always)]
138    pub(crate) fn set_plus(&mut self, plus: bool) {
139        self.plus = plus;
140    }
141
142    /// Byte offset from the start of the command line, one separator counted per word.
143    #[inline(always)]
144    pub fn offset(&self) -> usize {
145        (self.total - self.remaining) as usize
146    }
147
148    /// The unread part of the current word; empty at the end.
149    #[inline(always)]
150    pub fn front(&self) -> &'i [u8] {
151        match self.words.first() {
152            Some(word) => &word[self.skip as usize..],
153            None => &[],
154        }
155    }
156
157    /// Read `n` bytes of the current word, staying inside it.
158    #[inline(always)]
159    pub(crate) fn take_bytes(&mut self, n: usize) {
160        debug_assert!(n < self.front().len() + 1);
161        self.skip += n as u32;
162        self.remaining -= n as u32;
163    }
164
165    /// Read the rest of the current word and move to the next one.
166    #[inline(always)]
167    pub(crate) fn take_word(&mut self) -> &'i BStr {
168        let rest = self.front();
169        self.remaining -= rest.len() as u32 + 1;
170        self.words = &self.words[1..];
171        self.skip = 0;
172        if self.mode == Mode::Bundle {
173            self.mode = Mode::Word;
174        }
175        BStr::new(rest)
176    }
177
178    /// Number of whole words spanning `offset`, which must fall on a word boundary.
179    fn words_in(&self, offset: usize) -> usize {
180        let mut end = 0;
181        for (n, (at, _)) in self.iter_offsets().enumerate() {
182            if at == offset {
183                return n;
184            }
185            end = at;
186        }
187        assert_eq!(
188            offset, self.remaining as usize,
189            "offset {offset} is inside a word (last start {end})"
190        );
191        self.words.len()
192    }
193}
194
195impl std::fmt::Debug for Argv<'_> {
196    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
197        let mut words: Vec<&BStr> = self.words.to_vec();
198        if let Some(first) = words.first_mut() {
199            *first = BStr::new(&first[self.skip as usize..]);
200        }
201        f.debug_struct("Argv")
202            .field("mode", &self.mode)
203            .field("words", &words)
204            .finish()
205    }
206}
207
208/// Iterator over the remaining words with their offsets.
209#[derive(Clone, Debug)]
210pub struct IterOffsets<'i> {
211    words: std::slice::Iter<'i, &'i BStr>,
212    skip: usize,
213    offset: usize,
214}
215
216impl<'i> Iterator for IterOffsets<'i> {
217    type Item = (usize, &'i BStr);
218
219    fn next(&mut self) -> Option<Self::Item> {
220        let word = BStr::new(&self.words.next()?[self.skip..]);
221        self.skip = 0;
222        let at = self.offset;
223        self.offset += word.len() + 1;
224        Some((at, word))
225    }
226}
227
228impl Offset for Argv<'_> {
229    #[inline(always)]
230    fn offset_from(&self, start: &Self) -> usize {
231        (start.remaining - self.remaining) as usize
232    }
233}
234
235impl SliceLen for Argv<'_> {
236    #[inline(always)]
237    fn slice_len(&self) -> usize {
238        self.remaining as usize
239    }
240}
241
242/// Tokens are words (the unread part of the current one first); slices are
243/// runs of whole words, so `next_slice` must not be asked to split a word.
244impl<'i> Stream for Argv<'i> {
245    type Token = &'i BStr;
246    type Slice = &'i [&'i BStr];
247    type IterOffsets = IterOffsets<'i>;
248    type Checkpoint = Self;
249
250    #[inline(always)]
251    fn iter_offsets(&self) -> Self::IterOffsets {
252        IterOffsets {
253            words: self.words.iter(),
254            skip: self.skip as usize,
255            offset: 0,
256        }
257    }
258    #[inline(always)]
259    fn eof_offset(&self) -> usize {
260        self.remaining as usize
261    }
262    #[inline(always)]
263    fn next_token(&mut self) -> Option<Self::Token> {
264        if self.is_empty() {
265            None
266        } else {
267            Some(self.take_word())
268        }
269    }
270    #[inline(always)]
271    fn peek_token(&self) -> Option<Self::Token> {
272        (!self.is_empty()).then(|| BStr::new(self.front()))
273    }
274    fn offset_for<P>(&self, predicate: P) -> Option<usize>
275    where
276        P: Fn(Self::Token) -> bool,
277    {
278        self.iter_offsets()
279            .find(|&(_, word)| predicate(word))
280            .map(|(at, _)| at)
281    }
282    fn offset_at(&self, tokens: usize) -> Result<usize, Needed> {
283        let mut iter = self.iter_offsets();
284        match iter.nth(tokens) {
285            Some((at, _)) => Ok(at),
286            None if tokens == self.words.len() => Ok(self.remaining as usize),
287            None => Err(Needed::new(tokens - self.words.len())),
288        }
289    }
290    fn next_slice(&mut self, offset: usize) -> Self::Slice {
291        let slice = self.peek_slice(offset);
292        for _ in 0..slice.len() {
293            self.take_word();
294        }
295        slice
296    }
297    fn peek_slice(&self, offset: usize) -> Self::Slice {
298        assert!(
299            self.skip == 0 || offset == 0,
300            "cannot slice whole words inside a bundle"
301        );
302        &self.words[..self.words_in(offset)]
303    }
304    #[inline(always)]
305    fn checkpoint(&self) -> Self::Checkpoint {
306        *self
307    }
308    #[inline(always)]
309    fn reset(&mut self, checkpoint: &Self::Checkpoint) {
310        *self = *checkpoint;
311    }
312    fn trace(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
313        write!(f, "{self:?}")
314    }
315}
316
317impl StreamIsPartial for Argv<'_> {
318    type PartialState = ();
319
320    #[inline(always)]
321    fn complete(&mut self) -> Self::PartialState {}
322    #[inline(always)]
323    fn restore_partial(&mut self, _state: Self::PartialState) {}
324    #[inline(always)]
325    fn is_partial_supported() -> bool {
326        false
327    }
328}
329
330impl Location for Argv<'_> {
331    #[inline(always)]
332    fn previous_token_end(&self) -> usize {
333        self.offset()
334    }
335    #[inline(always)]
336    fn current_token_start(&self) -> usize {
337        self.offset()
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use winnow::combinator::repeat;
344    use winnow::prelude::*;
345    use winnow::token::{any, take};
346
347    use super::*;
348
349    fn line<'a>(words: &'a [&'a str]) -> Vec<&'a BStr> {
350        words.iter().map(BStr::new).collect()
351    }
352
353    #[test]
354    fn generic_token_parsers_see_whole_words() {
355        let words = line(&["a b", "", "c"]);
356        let mut input = Argv::new(&words);
357        assert_eq!(
358            any::<_, crate::Error>.parse_next(&mut input).unwrap(),
359            "a b"
360        );
361        assert_eq!(input.offset(), 4);
362        let rest: &[&BStr] = take::<_, _, crate::Error>(2usize)
363            .parse_next(&mut input)
364            .unwrap();
365        assert_eq!(rest, &words[1..]);
366        assert!(input.is_empty());
367    }
368
369    #[test]
370    fn a_flag_matches_beside_a_word_holding_any_byte() {
371        let words = [BStr::new(b"--verbose"), BStr::new(b"a\0\xff")];
372        let mut input = Argv::new(&words);
373        let flag = crate::token::arg(&mut input).unwrap();
374        assert!(matches!(flag, crate::token::Arg::Long(f) if f.name == b"verbose"));
375        let word = crate::token::arg(&mut input).unwrap();
376        assert!(matches!(word, crate::token::Arg::Word(w) if w.value == &b"a\0\xff"[..]));
377    }
378
379    #[test]
380    fn a_multibyte_short_is_one_letter() {
381        let words = [BStr::new("-β".as_bytes())];
382        let mut input = Argv::new(&words);
383        let flag = crate::token::arg(&mut input).unwrap();
384        assert!(matches!(flag, crate::token::Arg::Short(f) if f.letter == 'β'));
385        assert!(input.is_empty());
386    }
387
388    #[test]
389    fn reading_a_letter_counts_as_progress() {
390        let words = line(&["-abc"]);
391        let mut input = Argv::new(&words);
392        let letters: Vec<_> = repeat(0.., crate::token::arg)
393            .parse_next(&mut input)
394            .unwrap();
395        assert_eq!(letters.len(), 3);
396        assert!(input.is_empty());
397    }
398
399    #[test]
400    fn checkpoint_restores_the_position_inside_a_bundle() {
401        let words = line(&["-ab", "x"]);
402        let mut input = Argv::new(&words);
403        crate::token::arg(&mut input).unwrap();
404        let saved = input.checkpoint();
405        crate::token::arg(&mut input).unwrap();
406        crate::token::arg(&mut input).unwrap();
407        input.reset(&saved);
408        assert_eq!((input.mode(), input.front()), (Mode::Bundle, &b"b"[..]));
409    }
410}