use std::ffi::OsStr;
use winnow::stream::{BStr, Location, Needed, Offset, SliceLen, Stream, StreamIsPartial};
pub fn words<S: AsRef<OsStr>>(args: &[S]) -> Vec<&BStr> {
args.iter()
.map(|arg| BStr::new(arg.as_ref().as_encoded_bytes()))
.collect()
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Mode {
Word,
Bundle,
Stopped,
Values,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct Argv<'i> {
words: &'i [&'i BStr],
skip: u32,
remaining: u32,
total: u32,
mode: Mode,
plus: bool,
}
const _: () = assert!(std::mem::size_of::<Argv<'static>>() <= 32);
impl<'i> Argv<'i> {
pub fn new(words: &'i [&'i BStr]) -> Self {
let total: usize = words.iter().map(|w| w.len() + 1).sum();
let total = u32::try_from(total).expect("a command line under 4 GiB");
Self {
words,
skip: 0,
remaining: total,
total,
mode: Mode::Word,
plus: false,
}
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.words.is_empty()
}
#[inline(always)]
pub fn words_left(&self) -> usize {
self.words.len()
}
#[inline(always)]
pub fn mode(&self) -> Mode {
self.mode
}
#[inline]
pub fn stop_flags(&mut self) {
if self.mode == Mode::Word {
self.mode = Mode::Values;
}
}
#[inline]
pub fn resume_flags(&mut self) {
if matches!(self.mode, Mode::Stopped | Mode::Values) {
self.mode = Mode::Word;
}
}
#[inline(always)]
pub(crate) fn set_mode(&mut self, mode: Mode) {
self.mode = mode;
}
#[inline(always)]
pub(crate) fn plus(&self) -> bool {
self.plus
}
#[inline(always)]
pub(crate) fn set_plus(&mut self, plus: bool) {
self.plus = plus;
}
#[inline(always)]
pub fn offset(&self) -> usize {
(self.total - self.remaining) as usize
}
#[inline(always)]
pub fn front(&self) -> &'i [u8] {
match self.words.first() {
Some(word) => &word[self.skip as usize..],
None => &[],
}
}
#[inline(always)]
pub(crate) fn take_bytes(&mut self, n: usize) {
debug_assert!(n < self.front().len() + 1);
self.skip += n as u32;
self.remaining -= n as u32;
}
#[inline(always)]
pub(crate) fn take_word(&mut self) -> &'i BStr {
let rest = self.front();
self.remaining -= rest.len() as u32 + 1;
self.words = &self.words[1..];
self.skip = 0;
if self.mode == Mode::Bundle {
self.mode = Mode::Word;
}
BStr::new(rest)
}
fn words_in(&self, offset: usize) -> usize {
let mut end = 0;
for (n, (at, _)) in self.iter_offsets().enumerate() {
if at == offset {
return n;
}
end = at;
}
assert_eq!(
offset, self.remaining as usize,
"offset {offset} is inside a word (last start {end})"
);
self.words.len()
}
}
impl std::fmt::Debug for Argv<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut words: Vec<&BStr> = self.words.to_vec();
if let Some(first) = words.first_mut() {
*first = BStr::new(&first[self.skip as usize..]);
}
f.debug_struct("Argv")
.field("mode", &self.mode)
.field("words", &words)
.finish()
}
}
#[derive(Clone, Debug)]
pub struct IterOffsets<'i> {
words: std::slice::Iter<'i, &'i BStr>,
skip: usize,
offset: usize,
}
impl<'i> Iterator for IterOffsets<'i> {
type Item = (usize, &'i BStr);
fn next(&mut self) -> Option<Self::Item> {
let word = BStr::new(&self.words.next()?[self.skip..]);
self.skip = 0;
let at = self.offset;
self.offset += word.len() + 1;
Some((at, word))
}
}
impl Offset for Argv<'_> {
#[inline(always)]
fn offset_from(&self, start: &Self) -> usize {
(start.remaining - self.remaining) as usize
}
}
impl SliceLen for Argv<'_> {
#[inline(always)]
fn slice_len(&self) -> usize {
self.remaining as usize
}
}
impl<'i> Stream for Argv<'i> {
type Token = &'i BStr;
type Slice = &'i [&'i BStr];
type IterOffsets = IterOffsets<'i>;
type Checkpoint = Self;
#[inline(always)]
fn iter_offsets(&self) -> Self::IterOffsets {
IterOffsets {
words: self.words.iter(),
skip: self.skip as usize,
offset: 0,
}
}
#[inline(always)]
fn eof_offset(&self) -> usize {
self.remaining as usize
}
#[inline(always)]
fn next_token(&mut self) -> Option<Self::Token> {
if self.is_empty() {
None
} else {
Some(self.take_word())
}
}
#[inline(always)]
fn peek_token(&self) -> Option<Self::Token> {
(!self.is_empty()).then(|| BStr::new(self.front()))
}
fn offset_for<P>(&self, predicate: P) -> Option<usize>
where
P: Fn(Self::Token) -> bool,
{
self.iter_offsets()
.find(|&(_, word)| predicate(word))
.map(|(at, _)| at)
}
fn offset_at(&self, tokens: usize) -> Result<usize, Needed> {
let mut iter = self.iter_offsets();
match iter.nth(tokens) {
Some((at, _)) => Ok(at),
None if tokens == self.words.len() => Ok(self.remaining as usize),
None => Err(Needed::new(tokens - self.words.len())),
}
}
fn next_slice(&mut self, offset: usize) -> Self::Slice {
let slice = self.peek_slice(offset);
for _ in 0..slice.len() {
self.take_word();
}
slice
}
fn peek_slice(&self, offset: usize) -> Self::Slice {
assert!(
self.skip == 0 || offset == 0,
"cannot slice whole words inside a bundle"
);
&self.words[..self.words_in(offset)]
}
#[inline(always)]
fn checkpoint(&self) -> Self::Checkpoint {
*self
}
#[inline(always)]
fn reset(&mut self, checkpoint: &Self::Checkpoint) {
*self = *checkpoint;
}
fn trace(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{self:?}")
}
}
impl StreamIsPartial for Argv<'_> {
type PartialState = ();
#[inline(always)]
fn complete(&mut self) -> Self::PartialState {}
#[inline(always)]
fn restore_partial(&mut self, _state: Self::PartialState) {}
#[inline(always)]
fn is_partial_supported() -> bool {
false
}
}
impl Location for Argv<'_> {
#[inline(always)]
fn previous_token_end(&self) -> usize {
self.offset()
}
#[inline(always)]
fn current_token_start(&self) -> usize {
self.offset()
}
}
#[cfg(test)]
mod tests {
use winnow::combinator::repeat;
use winnow::prelude::*;
use winnow::token::{any, take};
use super::*;
fn line<'a>(words: &'a [&'a str]) -> Vec<&'a BStr> {
words.iter().map(BStr::new).collect()
}
#[test]
fn generic_token_parsers_see_whole_words() {
let words = line(&["a b", "", "c"]);
let mut input = Argv::new(&words);
assert_eq!(
any::<_, crate::Error>.parse_next(&mut input).unwrap(),
"a b"
);
assert_eq!(input.offset(), 4);
let rest: &[&BStr] = take::<_, _, crate::Error>(2usize)
.parse_next(&mut input)
.unwrap();
assert_eq!(rest, &words[1..]);
assert!(input.is_empty());
}
#[test]
fn a_flag_matches_beside_a_word_holding_any_byte() {
let words = [BStr::new(b"--verbose"), BStr::new(b"a\0\xff")];
let mut input = Argv::new(&words);
let flag = crate::token::arg(&mut input).unwrap();
assert!(matches!(flag, crate::token::Arg::Long(f) if f.name == b"verbose"));
let word = crate::token::arg(&mut input).unwrap();
assert!(matches!(word, crate::token::Arg::Word(w) if w.value == &b"a\0\xff"[..]));
}
#[test]
fn a_multibyte_short_is_one_letter() {
let words = [BStr::new("-β".as_bytes())];
let mut input = Argv::new(&words);
let flag = crate::token::arg(&mut input).unwrap();
assert!(matches!(flag, crate::token::Arg::Short(f) if f.letter == 'β'));
assert!(input.is_empty());
}
#[test]
fn reading_a_letter_counts_as_progress() {
let words = line(&["-abc"]);
let mut input = Argv::new(&words);
let letters: Vec<_> = repeat(0.., crate::token::arg)
.parse_next(&mut input)
.unwrap();
assert_eq!(letters.len(), 3);
assert!(input.is_empty());
}
#[test]
fn checkpoint_restores_the_position_inside_a_bundle() {
let words = line(&["-ab", "x"]);
let mut input = Argv::new(&words);
crate::token::arg(&mut input).unwrap();
let saved = input.checkpoint();
crate::token::arg(&mut input).unwrap();
crate::token::arg(&mut input).unwrap();
input.reset(&saved);
assert_eq!((input.mode(), input.front()), (Mode::Bundle, &b"b"[..]));
}
}