aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-17 02:46:55 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-17 02:46:55 +0800
commit63d3fad181b83123c63335495b6c72ae8e5156cd (patch)
tree9efcbd7f744ec14ec0dfd7e8065ee732c4b14805
parent71ef8a1b94452b94b5e1933f8c4857c333ff4014 (diff)
feat: add multi-value argument support with boundary checking
Add `MultiArgMatcher` for greedy flag matching and `MultiPickableWithBoundary` trait with `VecUntil<T>` wrapper for bounded multi-value collection. Introduce `BoundaryCheck` trait to allow positional multi-value parameters to stop consuming arguments when a non-matching value is encountered.
-rw-r--r--mingling_picker/src/builtin/pick_numbers.rs62
-rw-r--r--mingling_picker/src/parselib.rs3
-rw-r--r--mingling_picker/src/parselib/multi_arg_matcher.rs141
-rw-r--r--mingling_picker/src/pickable.rs3
-rw-r--r--mingling_picker/src/pickable/multi_pickable.rs200
-rw-r--r--mingling_picker/test/src/test.rs2
-rw-r--r--mingling_picker/test/src/test/multi_arg_test.rs181
-rw-r--r--mingling_picker/test/src/test/multi_value_test.rs66
8 files changed, 657 insertions, 1 deletions
diff --git a/mingling_picker/src/builtin/pick_numbers.rs b/mingling_picker/src/builtin/pick_numbers.rs
index 6c0f0b7..a5ab0a9 100644
--- a/mingling_picker/src/builtin/pick_numbers.rs
+++ b/mingling_picker/src/builtin/pick_numbers.rs
@@ -1,4 +1,4 @@
-use crate::{SinglePickable, pickable_needed::*};
+use crate::{BoundaryCheck, SinglePickable, pickable_needed::*};
macro_rules! impl_single_pickable_num {
($($t:ty),+ $(,)?) => {
@@ -13,8 +13,68 @@ macro_rules! impl_single_pickable_num {
};
}
+/// Returns `true` if `raw` looks like an integer (digits, optional `+`/`-` prefix,
+/// no decimal point or exponent).
+fn is_int_like(raw: &str) -> bool {
+ let s = raw.trim();
+ let bytes = s.as_bytes();
+ if bytes.is_empty() {
+ return false;
+ }
+ let mut i = 0;
+ if bytes[0] == b'-' || bytes[0] == b'+' {
+ i = 1;
+ }
+ if i >= bytes.len() {
+ return false;
+ }
+ for &b in &bytes[i..] {
+ if !b.is_ascii_digit() {
+ return false;
+ }
+ }
+ true
+}
+
+/// Returns `true` if `raw` looks like a float (contains `.`, `e`, or `E`).
+fn is_float_like(raw: &str) -> bool {
+ let s = raw.trim();
+ s.contains('.') || s.contains('e') || s.contains('E')
+}
+
impl_single_pickable_num! {
i8, i16, i32, i64, i128, isize,
u8, u16, u32, u64, u128, usize,
f32, f64,
}
+
+// Integer boundary: only accept strings that look like integers.
+// Float-like strings trigger a boundary.
+macro_rules! impl_boundary_check_int {
+ ($($t:ty),+ $(,)?) => {
+ $(impl BoundaryCheck for $t {
+ fn check_boundary(raw: &str) -> bool {
+ !is_int_like(raw)
+ }
+ })+
+ };
+}
+
+impl_boundary_check_int! {
+ i8, i16, i32, i64, i128, isize,
+ u8, u16, u32, u64, u128, usize,
+}
+
+// Float boundary: only accept strings that look like floats.
+// Integer-like strings trigger a boundary.
+impl BoundaryCheck for f32 {
+ fn check_boundary(raw: &str) -> bool {
+ !is_float_like(raw) || raw.parse::<f32>().is_err()
+ }
+}
+
+impl BoundaryCheck for f64 {
+ fn check_boundary(raw: &str) -> bool {
+ !is_float_like(raw) || raw.parse::<f64>().is_err()
+ }
+}
diff --git a/mingling_picker/src/parselib.rs b/mingling_picker/src/parselib.rs
index 456a34d..7fbd606 100644
--- a/mingling_picker/src/parselib.rs
+++ b/mingling_picker/src/parselib.rs
@@ -4,6 +4,9 @@ pub use flag_matcher::*;
mod arg_matcher;
pub use arg_matcher::*;
+mod multi_arg_matcher;
+pub use multi_arg_matcher::*;
+
mod pos_matcher;
pub use pos_matcher::*;
diff --git a/mingling_picker/src/parselib/multi_arg_matcher.rs b/mingling_picker/src/parselib/multi_arg_matcher.rs
new file mode 100644
index 0000000..e8bdb71
--- /dev/null
+++ b/mingling_picker/src/parselib/multi_arg_matcher.rs
@@ -0,0 +1,141 @@
+use crate::{
+ matcher_needed::*,
+ parselib::{build_possible_flags, seek_end_of_options},
+};
+
+/// `MultiArgMatcher` matches a named flag and **all** consecutive arguments
+/// that follow it, stopping at the next flag, the `--` marker, or the end
+/// of the argument list.
+///
+/// This is the tag implementation for `Multi` and `GreedyMulti` types
+/// such as `Vec<String>` (`--files a.txt b.txt`).
+///
+/// # Behavior
+///
+/// | Input | `on_match_all` |
+/// |-------|----------------|
+/// | `--val a b --val d e` | `[0, 1, 2, 5, 6]` (two groups) |
+/// | `--val=1 2` | `[0, 1]` (eq mode + one extra value) |
+///
+/// Args after `--` are ignored.
+pub struct MultiArgMatcher;
+
+impl Matcher for MultiArgMatcher {
+ fn on_match_one(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Option<usize> {
+ if arg_info.positional {
+ return args.first().map(|a| a.raw_idx);
+ }
+
+ let possible_flags = build_possible_flags(style, arg_info);
+ let end = seek_end_of_options(args, style);
+ let sep = style.value_separator;
+
+ for arg in args {
+ if end.is_some_and(|e| arg.raw_idx >= e) {
+ break;
+ }
+ let matched = possible_flags
+ .iter()
+ .any(|f| Self::flag_match(arg.raw, f, style.case_sensitive, sep));
+ if matched {
+ return Some(arg.raw_idx);
+ }
+ }
+ None
+ }
+
+ fn on_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Vec<usize> {
+ if arg_info.positional {
+ let end = seek_end_of_options(args, style);
+ return args
+ .iter()
+ .take_while(|a| end.is_none_or(|e| a.raw_idx < e))
+ .map(|a| a.raw_idx)
+ .collect();
+ }
+
+ let possible_flags = build_possible_flags(style, arg_info);
+ let end = seek_end_of_options(args, style);
+ let sep = style.value_separator;
+ let is_flag = |raw: &str| {
+ raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix)
+ };
+ let is_our_flag = |raw: &str| {
+ possible_flags
+ .iter()
+ .any(|f| Self::flag_match(raw, f, style.case_sensitive, sep))
+ };
+
+ let mut result = Vec::new();
+ let mut i = 0;
+ while i < args.len() {
+ if end.is_some_and(|e| args[i].raw_idx >= e) {
+ break;
+ }
+
+ let matched = is_our_flag(args[i].raw);
+
+ if matched {
+ result.push(args[i].raw_idx);
+
+ if Self::is_eq_match(args[i].raw, &possible_flags, style.case_sensitive, sep) {
+ i += 1;
+ while i < args.len()
+ && end.is_none_or(|e| args[i].raw_idx < e)
+ && !is_flag(args[i].raw)
+ {
+ result.push(args[i].raw_idx);
+ i += 1;
+ }
+ continue;
+ }
+
+ i += 1;
+ while i < args.len()
+ && end.is_none_or(|e| args[i].raw_idx < e)
+ && !is_flag(args[i].raw)
+ {
+ result.push(args[i].raw_idx);
+ i += 1;
+ }
+ continue;
+ }
+ i += 1;
+ }
+
+ result
+ }
+}
+
+impl MultiArgMatcher {
+ #[inline(always)]
+ fn flag_match(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool {
+ let eq = |r: &str, f: &str| r.len() > f.len() && r.as_bytes().get(f.len()) == Some(&(sep as u8));
+
+ if case_sensitive {
+ raw == flag_str || (raw.starts_with(flag_str) && eq(raw, flag_str))
+ } else {
+ raw.eq_ignore_ascii_case(flag_str)
+ || (raw.len() > flag_str.len()
+ && raw[..flag_str.len()].eq_ignore_ascii_case(flag_str)
+ && raw.as_bytes()[flag_str.len()] == sep as u8)
+ }
+ }
+
+ #[inline(always)]
+ fn is_eq_match(raw: &str, flags: &[String], case_sensitive: bool, sep: char) -> bool {
+ flags.iter().any(|f| {
+ Self::flag_match(raw, f, case_sensitive, sep)
+ && raw.len() > f.len()
+ && raw.as_bytes().get(f.len()) == Some(&(sep as u8))
+ })
+ }
+}
diff --git a/mingling_picker/src/pickable.rs b/mingling_picker/src/pickable.rs
index 959aad8..758ae9a 100644
--- a/mingling_picker/src/pickable.rs
+++ b/mingling_picker/src/pickable.rs
@@ -3,6 +3,9 @@ use crate::{PickerArg, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs
mod single_pickable;
pub use single_pickable::*;
+mod multi_pickable;
+pub use multi_pickable::*;
+
/// `Pickable` trait defines how to parse a type instance from command-line arguments.
///
/// This trait is the core abstraction of the `Picker` argument parsing system, dividing the
diff --git a/mingling_picker/src/pickable/multi_pickable.rs b/mingling_picker/src/pickable/multi_pickable.rs
new file mode 100644
index 0000000..bbed6c6
--- /dev/null
+++ b/mingling_picker/src/pickable/multi_pickable.rs
@@ -0,0 +1,200 @@
+use std::marker::PhantomData;
+use std::ops::{Deref, DerefMut};
+
+use crate::{
+ matcher_needed::Matcher,
+ parselib::{MultiArgMatcher, ParserStyle},
+ Pickable, PickerArg, PickerArgAttr, PickerArgResult,
+ SinglePickable, TagPhaseContext,
+};
+
+/// Boundary check for multi-value positional parameters.
+pub trait BoundaryCheck {
+ /// Returns `true` if `raw` is **not** part of the current parameter group.
+ fn check_boundary(raw: &str) -> bool;
+}
+
+// ============================================================
+// MultiPickableWithBoundary — the single multi-value trait
+// ============================================================
+
+/// Trait for multi-value parameters.
+///
+/// - [`pick_multi`](MultiPickableWithBoundary::pick_multi): converts collected
+/// strings into `Self`.
+/// - [`Checker`](MultiPickableWithBoundary::Checker): a type implementing
+/// [`BoundaryCheck`] used by the positional matcher to determine where one
+/// group ends and another begins.
+pub trait MultiPickableWithBoundary: Sized {
+ /// The boundary checker type.
+ type Checker: BoundaryCheck;
+
+ /// Convert the collected raw strings into `Self`.
+ fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self>;
+}
+
+/// Marker: unit type that always accepts — no boundary.
+pub struct NoBoundary;
+
+impl BoundaryCheck for NoBoundary {
+ #[inline(always)]
+ fn check_boundary(_raw: &str) -> bool {
+ false // never stop: greedy
+ }
+}
+
+/// `Vec<T>` is greedy — it takes everything with `NoBoundary`.
+impl<T: SinglePickable> MultiPickableWithBoundary for Vec<T> {
+ type Checker = NoBoundary;
+
+ fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self> {
+ let mut result = Vec::with_capacity(raw.len());
+ for s in &raw {
+ match T::pick_single(Some(s)) {
+ PickerArgResult::Parsed(v) => result.push(v),
+ PickerArgResult::NotFound => return PickerArgResult::NotFound,
+ PickerArgResult::Unparsed => {}
+ }
+ }
+ PickerArgResult::Parsed(result)
+ }
+}
+
+/// `VecUntil<T>` uses `T` itself as the boundary checker.
+impl<T> MultiPickableWithBoundary for VecUntil<T>
+where
+ T: SinglePickable + BoundaryCheck,
+{
+ type Checker = T;
+
+ fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self> {
+ let mut inner = Vec::with_capacity(raw.len());
+ for s in &raw {
+ match T::pick_single(Some(s)) {
+ PickerArgResult::Parsed(v) => inner.push(v),
+ PickerArgResult::NotFound => return PickerArgResult::NotFound,
+ PickerArgResult::Unparsed => {}
+ }
+ }
+ PickerArgResult::Parsed(VecUntil { inner, _marker: PhantomData })
+ }
+}
+
+// ============================================================
+// VecUntil<T> — Vec wrapper with boundary checking
+// ============================================================
+
+#[derive(Debug, Clone, PartialEq, Eq, Default)]
+pub struct VecUntil<T> {
+ inner: Vec<T>,
+ _marker: PhantomData<T>,
+}
+
+impl<T> VecUntil<T> {
+ pub fn into_inner(self) -> Vec<T> {
+ self.inner
+ }
+}
+
+impl<T> From<Vec<T>> for VecUntil<T> {
+ fn from(v: Vec<T>) -> Self {
+ VecUntil { inner: v, _marker: PhantomData }
+ }
+}
+
+impl<T> From<VecUntil<T>> for Vec<T> {
+ fn from(v: VecUntil<T>) -> Self {
+ v.inner
+ }
+}
+
+impl<T> Deref for VecUntil<T> {
+ type Target = Vec<T>;
+ fn deref(&self) -> &Vec<T> {
+ &self.inner
+ }
+}
+
+/// If the first raw string looks like a named flag (starts with the
+/// style's long or short prefix), strip it — it's the flag, not a value.
+fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] {
+ if let Some(first) = raw_strs.first() {
+ let style = ParserStyle::global_style();
+ if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) {
+ return &raw_strs[1..];
+ }
+ }
+ raw_strs
+}
+
+impl<T> DerefMut for VecUntil<T> {
+ fn deref_mut(&mut self) -> &mut Vec<T> {
+ &mut self.inner
+ }
+}
+
+// Concrete Pickable impls
+
+impl<'a, T> Pickable<'a> for Vec<T>
+where
+ T: SinglePickable,
+{
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_multi(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ MultiArgMatcher::match_all(ctx.into())
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ let strs = strip_flag(raw_strs);
+ let owned: Vec<String> = strs.iter().map(|&s| s.to_string()).collect();
+ <Vec<T> as MultiPickableWithBoundary>::pick_multi(owned)
+ }
+}
+
+impl<'a, T> Pickable<'a> for VecUntil<T>
+where
+ T: SinglePickable + BoundaryCheck,
+{
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_multi(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ let args = ctx.args;
+ let is_positional = ctx.arg_info.positional;
+ let positions = MultiArgMatcher::match_all(ctx.into());
+ if positions.is_empty() {
+ return positions;
+ }
+
+ // Apply BoundaryCheck to trim positions that don't belong.
+ // For named: positions[0] is the flag, values start at [1..].
+ // For positional: all positions are values.
+ let start = if is_positional { 0 } else { 1 };
+ if start >= positions.len() {
+ return positions; // flag only, no values
+ }
+
+ let mut cut = start;
+ for &idx in &positions[start..] {
+ if let Some(raw) = args.get(idx) {
+ if T::check_boundary(raw) {
+ break; // boundary hit — stop here
+ }
+ }
+ cut += 1;
+ }
+
+ positions[..cut].to_vec()
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ // Strip the flag position from named args.
+ let strs = strip_flag(raw_strs);
+ let owned: Vec<String> = strs.iter().map(|&s| s.to_string()).collect();
+ <VecUntil<T> as MultiPickableWithBoundary>::pick_multi(owned)
+ }
+}
diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs
index 7d18c86..9c53514 100644
--- a/mingling_picker/test/src/test.rs
+++ b/mingling_picker/test/src/test.rs
@@ -1,5 +1,7 @@
mod arg_matcher_test;
mod basic_test;
+mod multi_arg_test;
+mod multi_value_test;
mod pos_matcher_test;
mod priority_test;
mod route_test;
diff --git a/mingling_picker/test/src/test/multi_arg_test.rs b/mingling_picker/test/src/test/multi_arg_test.rs
new file mode 100644
index 0000000..377357e
--- /dev/null
+++ b/mingling_picker/test/src/test/multi_arg_test.rs
@@ -0,0 +1,181 @@
+use mingling_picker::parselib::{Matcher, MultiArgMatcher, UNIX_STYLE};
+use mingling_picker::PickerArgInfo;
+
+use crate::make_args;
+
+// on_match_one — basic
+
+#[test]
+fn test_multi_one_finds_first_flag() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--other", 0), ("--val", 1), ("a", 2), ("b", 3)]);
+ let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(1));
+}
+
+#[test]
+fn test_multi_one_no_match() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--other", 0)]);
+ let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+// on_match_all — named, basic multi-value
+
+#[test]
+fn test_multi_all_flag_takes_all_values_until_next_flag() {
+ // --val a b c → all three values belong to --val
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("c", 3)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3]);
+}
+
+#[test]
+fn test_multi_all_stops_at_next_flag() {
+ // --val a b --other c d → only a,b belong to --val
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--other", 3), ("c", 4)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2]);
+}
+
+#[test]
+fn test_multi_all_flag_no_values() {
+ // --val at end with no values → just the flag
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_multi_all_empty() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = vec![];
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// on_match_all — named, multiple occurrences of same flag
+
+#[test]
+fn test_multi_all_two_occurrences() {
+ // --val a b --val c d → two groups
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[
+ ("--val", 0), ("a", 1), ("b", 2),
+ ("--val", 3), ("c", 4), ("d", 5),
+ ]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3, 4, 5]);
+}
+
+#[test]
+fn test_multi_all_skips_non_matching_args() {
+ // --val a --other b --val c → only --val groups
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[
+ ("--val", 0), ("a", 1),
+ ("--other", 2), ("b", 3),
+ ("--val", 4), ("c", 5),
+ ]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 4, 5]);
+}
+
+// on_match_all — named, eq mode
+
+#[test]
+fn test_multi_all_eq_mode() {
+ // --val=a b → eq mode + one extra value
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val=a", 0), ("b", 1)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_multi_all_eq_mode_no_extra() {
+ // --val=a alone → just the eq flag
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val=a", 0)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_multi_all_mixed_eq_and_regular() {
+ // --val=a b --val c d
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[
+ ("--val=a", 0), ("b", 1), ("--val", 2), ("c", 3), ("d", 4),
+ ]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3, 4]);
+}
+
+// on_match_all — short flag
+
+#[test]
+fn test_multi_all_short_flag() {
+ let mut info = PickerArgInfo::new();
+ info.set_short('v');
+ info.set_long("val");
+ let args = make_args(&[("-v", 0), ("a", 1), ("b", 2)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2]);
+}
+
+// on_match_all — end-of-options marker
+
+#[test]
+fn test_multi_all_stops_at_end_of_options() {
+ // --val a -- b → stops before `--`
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("--", 2), ("b", 3)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_multi_all_flag_after_end_ignored() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--", 0), ("--val", 1), ("a", 2)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// on_match_all — positional
+
+#[test]
+fn test_multi_all_positional() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("a.txt", 0), ("b.txt", 1)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_multi_all_positional_stops_at_end_of_options() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("a.txt", 0), ("--", 1), ("b.txt", 2)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
diff --git a/mingling_picker/test/src/test/multi_value_test.rs b/mingling_picker/test/src/test/multi_value_test.rs
new file mode 100644
index 0000000..50d08af
--- /dev/null
+++ b/mingling_picker/test/src/test/multi_value_test.rs
@@ -0,0 +1,66 @@
+use mingling_picker::value::Flag;
+use mingling_picker::{IntoPicker, VecUntil, macros::arg};
+
+#[test]
+fn test_vec_until_i16_named() {
+ let (nums, rest): (VecUntil<i16>, Flag) = vec!["--nums", "1", "2", "3", "abc"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .pick(&arg![rest: Flag])
+ .unwrap();
+ assert_eq!(*nums, vec![1i16, 2, 3]);
+ assert_eq!(rest, Flag::Inactive);
+}
+
+#[test]
+fn test_vec_until_i16_stops_at_non_number() {
+ let nums: VecUntil<i16> = vec!["--nums", "42", "abc", "100"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*nums, vec![42i16]);
+}
+
+#[test]
+fn test_vec_until_i16_empty() {
+ let nums: VecUntil<i16> = vec!["--nums"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert!(nums.is_empty());
+}
+
+#[test]
+fn test_vec_until_i16_stops_at_next_flag() {
+ let nums: VecUntil<i16> = vec!["--nums", "1", "2", "--other", "3"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*nums, vec![1i16, 2]);
+}
+
+#[test]
+fn test_vec_until_i16_stops_at_end_of_options() {
+ let nums: VecUntil<i16> = vec!["--nums", "1", "2", "--", "3"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*nums, vec![1i16, 2]);
+}
+
+// Two VecUntil<T> with different boundary behaviours
+
+#[test]
+fn test_vec_until_f64_and_i16_positional() {
+ // Both are positional VecUntil. f64 takes all valid floats,
+ // i16 takes what's left. But note: "1", "2", "3" are also valid
+ // f64 values, so f64's check_boundary never fires — it consumes
+ // everything, and i16 gets nothing.
+ let (floats, ints): (VecUntil<f64>, VecUntil<i16>) = vec!["1.5", "2.5", "3.5", "1", "2", "3"]
+ .to_picker()
+ .pick(&arg![VecUntil<f64>])
+ .pick(&arg![VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*floats, vec![1.5, 2.5, 3.5]);
+ assert_eq!(*ints, vec![1i16, 2, 3]);
+}