Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 67 additions & 9 deletions argv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,14 @@ pub struct Flag<'a> {
/// `var=#true` — is bounded on how many times it was *given*, which no single token
/// can decide, so that bound stays with the metadata and is checked after the parse.
pub var_max: ::core::option::Option<u32>,
/// The byte that makes one word several values, if the flag declares one.
///
/// Here rather than with the metadata for the same reason [`var_max`](Self::var_max)
/// is: it decides *where* a word lands. A bound counts values, and a delimiter is what
/// makes a word stop being one of them — `--include a,b,c` is three, so a `var_max` of
/// two is already past its bound on the single word it was entitled to take. Binding
/// cannot count without it.
pub delimiter: ::core::option::Option<u8>,
/// Whether the flag is recognized by every command beneath the one that
/// declares it.
pub global: bool,
Expand All @@ -271,6 +279,7 @@ impl Flag<'_> {
takes_value: false,
variadic: false,
var_max: ::core::option::Option::None,
delimiter: ::core::option::Option::None,
global: false,
};

Expand All @@ -297,6 +306,11 @@ pub struct Arg<'a> {
/// command. `u32` rather than `usize` because a CLI that bounds a variadic above four
/// billion has other problems, and this table is read on the hot path.
pub var_max: ::core::option::Option<u32>,
/// The byte that makes one word several values, if the argument declares one.
///
/// See [`Flag::delimiter`]: a bound counts values, and only this says how many values a
/// word carries.
pub delimiter: ::core::option::Option<u8>,
/// This argument's relationship to the `--` separator.
pub double_dash: DoubleDash,
/// Unused by binding, kept so a table entry can carry its own name for
Expand All @@ -310,6 +324,7 @@ impl Arg<'_> {
key: 0,
var: false,
var_max: ::core::option::Option::None,
delimiter: ::core::option::Option::None,
double_dash: DoubleDash::Optional,
name: "",
};
Expand Down Expand Up @@ -1119,12 +1134,23 @@ impl<'t, 'v> Parser<'t, 'v> {
match self.argv.get(self.pos) {
Some(next) if !is_flag_like(bytes(next)) && bytes(next) != b"--" => {
self.pos += 1;
self.collected += 1;
self.collected += values_in(bytes(next), flag.delimiter);
// Same rule as a positional: a bounded occurrence takes that many and
// leaves the rest to whatever follows.
if flag.var_max.is_some_and(|max| self.collected >= max) {
self.collecting = None;
}
// Stopping is only the same as staying within the bound while one word
// is one value. A delimited word can carry the occurrence past it in a
// single step, and that word cannot be split between two owners, so the
// overshoot is an error rather than a place to stop.
if let Some(max) = flag.var_max.filter(|max| self.collected > *max) {
return Some(Err(Error::VarTooMany {
name: flag.name,
max: max as usize,
got: self.collected as usize,
}));
}
return Some(Ok(Event::Flag {
flag,
value: Some(bytes(next)),
Expand Down Expand Up @@ -1215,7 +1241,7 @@ impl<'t, 'v> Parser<'t, 'v> {
None
};
if flag.variadic {
self.start_collecting(flag);
self.start_collecting(flag, value.unwrap_or(b""))?;
}
return Ok(Event::Flag {
flag,
Expand Down Expand Up @@ -1310,7 +1336,7 @@ impl<'t, 'v> Parser<'t, 'v> {
rest
};
if flag.variadic {
self.start_collecting(flag);
self.start_collecting(flag, value)?;
}
Ok(Event::Flag {
flag,
Expand Down Expand Up @@ -1420,7 +1446,17 @@ impl<'t, 'v> Parser<'t, 'v> {
// bound, at which point the words after it belong to whatever comes next. That is
// what makes `[a]… [b]` expressible at all.
if arg.var {
self.arg_taken += 1;
self.arg_taken += values_in(token, arg.delimiter);
// Before advancing, which resets the count: as with a variadic flag, reaching
// the bound and passing it are the same event once a word can carry several
// values, and only the second is a mistake.
if let Some(max) = arg.var_max.filter(|max| self.arg_taken > *max) {
return Err(Error::VarTooMany {
name: arg.name,
max: max as usize,
got: self.arg_taken as usize,
});
}
if arg.var_max.is_some_and(|max| self.arg_taken >= max) {
self.advance_arg();
}
Expand Down Expand Up @@ -1459,15 +1495,24 @@ impl<'t, 'v> Parser<'t, 'v> {

/// A variadic flag occurrence begins, counting from zero.
///
/// The value it was given on the same token counts, which is why this starts at one:
/// `--include a b` with `var_max=2` takes `a` and `b`, not three words.
fn start_collecting(&mut self, flag: &'t Flag<'t>) {
self.collected = 1;
self.collecting = if flag.var_max.is_some_and(|max| max <= 1) {
/// The value it was given on the same token counts, which is why this starts at what
/// that value holds: `--include a b` with `var_max=2` takes `a` and `b`, not three
/// words — and `--include a,b` has already taken both on the one token.
fn start_collecting(&mut self, flag: &'t Flag<'t>, first: &[u8]) -> Result<(), Error<'t, 'v>> {
self.collected = values_in(first, flag.delimiter);
if let Some(max) = flag.var_max.filter(|max| self.collected > *max) {
return Err(Error::VarTooMany {
name: flag.name,
max: max as usize,
got: self.collected as usize,
});
}
self.collecting = if flag.var_max.is_some_and(|max| self.collected >= max) {
None
} else {
Some(flag)
};
Ok(())
}

fn next_arg(&self) -> Option<&'t Arg<'t>> {
Expand Down Expand Up @@ -1530,6 +1575,19 @@ fn bytes<'v>(s: &'v &'v OsStr) -> &'v [u8] {
s.as_encoded_bytes()
}

/// How many values one word carries.
///
/// One, until a delimiter is declared — and then one per separator, counting the same way
/// splitting on it does: `a,b` is two, `a,` is two with an empty second, and `` is one.
/// Counted rather than split because binding only needs the number, and the split itself
/// belongs to the layer that owns the values.
fn values_in(word: &[u8], delimiter: ::core::option::Option<u8>) -> u32 {
match delimiter {
Some(d) => 1 + word.iter().filter(|b| **b == d).count() as u32,
None => 1,
}
}

/// Whether a token should be read as a flag.
///
/// `-` alone is a value, conventionally stdin. A negative number is a value too,
Expand Down
13 changes: 13 additions & 0 deletions argv/src/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,9 @@ pub struct FlagMeta<'a> {
/// flag win, this reports it: the combination has no meaning, so honouring one
/// side silently would hide a mistake.
pub conflicts: &'a [&'a str],
/// The character one word is split on to make several values, as clap's
/// `value_delimiter` does. Only ever set where several values can land.
pub delimiter: Option<char>,
/// Whether this flag must be given on its own.
///
/// The whole-command form of [`conflicts`](Self::conflicts): everything the command
Expand Down Expand Up @@ -653,6 +656,7 @@ impl FlagMeta<'_> {
var_max: None,
overrides: &[],
conflicts: &[],
delimiter: None,
Comment thread
cursor[bot] marked this conversation as resolved.
exclusive: false,
requires: &[],
required_if: &[],
Expand All @@ -678,6 +682,8 @@ pub struct ArgMeta<'a> {
pub hide: bool,
pub var_min: Option<usize>,
pub var_max: Option<usize>,
/// The character one word is split on to make several positional values.
pub delimiter: Option<char>,
/// Heading to list this argument under in help output.
pub help_heading: Option<&'a str>,
/// What answers for this argument when a shell asks. See [`FlagMeta::complete`].
Expand All @@ -701,6 +707,7 @@ impl ArgMeta<'_> {
hide: false,
var_min: None,
var_max: None,
delimiter: None,
help_heading: None,
};
}
Expand Down Expand Up @@ -1160,6 +1167,9 @@ fn write_flag(out: &mut String, meta: &FlagMeta<'_>, depth: usize) -> core::fmt:
if meta.exclusive {
out.push_str(" exclusive=#true");
}
if let Some(delimiter) = meta.delimiter {
write!(out, " delimiter={}", quoted(&delimiter.to_string()))?;
}
write_single_list(out, "requires", meta.requires)?;
write_single_list(out, "required_if", meta.required_if)?;
write_single_list(out, "required_unless", meta.required_unless)?;
Expand Down Expand Up @@ -1240,6 +1250,9 @@ fn write_arg(out: &mut String, meta: &ArgMeta<'_>, depth: usize) -> core::fmt::R
if let Some(max) = meta.var_max {
write!(out, " var_max={max}")?;
}
if let Some(delimiter) = meta.delimiter {
write!(out, " delimiter={}", quoted(&delimiter.to_string()))?;
}
if meta.arg.double_dash != DoubleDash::Optional {
let mode = match meta.arg.double_dash {
DoubleDash::Required => "required",
Expand Down
6 changes: 6 additions & 0 deletions conformance/src/argv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ fn code(err: Error<'_, '_>) -> ErrorCode {
Error::MissingFlagValue { .. } => ErrorCode::MissingFlagValue,
Error::UnexpectedArg { .. } => ErrorCode::UnexpectedArg,
Error::ArgRequiresDoubleDash { .. } => ErrorCode::ArgRequiresDoubleDash,
// Binding does raise this one, and only this one of the bounds: a `var_max` stops a
// collection rather than judging it, so it can only be *exceeded* when a delimiter
// makes one word several values — which is a question about where a word lands, and
// so the parser's. `var_min` remains the layer above's, having nothing to do with
// where anything landed.
Error::VarTooMany { .. } => ErrorCode::VarTooMany,
Error::TooDeep => panic!("no corpus spec is anywhere near MAX_DEPTH"),
// The parser cannot raise these — they come from the layer above it, which
// this harness does not exercise: it builds tables from a spec rather than
Expand Down
15 changes: 15 additions & 0 deletions conformance/src/tables.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,16 @@ fn build_flag(f: &SpecFlag) -> &'static Flag<'static> {
// Saturating rather than truncating: `4294967296 as u32` is zero, which would read
// as "stop at once" rather than "no real limit".
.map(|max| u32::try_from(max).unwrap_or(u32::MAX)),
// A bound counts values, and this is what says how many a word carries. ASCII, not
// "fits in a byte": `§` is one byte as a scalar and two as UTF-8, and matching its
// low byte would find the continuation bytes inside unrelated characters. The spec
// refuses non-ASCII, so this only ever discards something already rejected.
delimiter: f
.arg
.as_ref()
.and_then(|a| a.delimiter)
.filter(char::is_ascii)
.map(|d| d as u8),
global: f.global,
}))
}
Expand All @@ -271,6 +281,7 @@ fn build_arg(a: &SpecArg) -> &'static Arg<'static> {
.var_max
.filter(|_| a.var)
.map(|max| u32::try_from(max).unwrap_or(u32::MAX)),
delimiter: a.delimiter.filter(char::is_ascii).map(|d| d as u8),
double_dash: double_dash(&a.double_dash),
}))
}
Expand Down Expand Up @@ -302,6 +313,9 @@ fn flag_meta(
hide: f.hide,
count: f.count,
repeatable: f.var,
// The separator as declared, a `char`: the metadata is the cold model and says what
// the spec said, where the binding table beside it holds the byte binding counts by.
delimiter: arg.and_then(|a| a.delimiter),
var_min: f.var_min.or(arg.and_then(|a| a.var_min)),
var_max: f.var_max.or(arg.and_then(|a| a.var_max)),
overrides: strs(&f.overrides),
Expand Down Expand Up @@ -331,6 +345,7 @@ fn arg_meta(
choices: a.choices.as_ref().map(|c| strs(&c.choices)).unwrap_or(&[]),
required: a.required,
hide: a.hide,
delimiter: a.delimiter,
var_min: a.var_min,
var_max: a.var_max,
help_heading: opt(&a.help_heading),
Expand Down
Loading