diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-07-16 23:06:43 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-07-16 23:06:43 +0800 |
| commit | 7bf5ba795361f6abe9d591a2912816ed41a9760b (patch) | |
| tree | cc4f4590f9e6eb9a765473a07967cbd9cad71643 | |
| parent | c906c757aac0260594beb4c9efde4b0e199512ff (diff) | |
feat(parselib): introduce PositionalMatcher for positional-only args
Extract positional argument matching from ArgMatcher into a dedicated
matcher that respects the `--` terminator and skips flag-like tokens
before it. Add test suite covering basic, after-`--`, and Windows-style
behaviour.
| -rw-r--r-- | mingling_picker/src/builtin/pick_string.rs | 4 | ||||
| -rw-r--r-- | mingling_picker/src/parselib.rs | 3 | ||||
| -rw-r--r-- | mingling_picker/src/parselib/pos_matcher.rs | 71 | ||||
| -rw-r--r-- | mingling_picker/test/src/test.rs | 1 | ||||
| -rw-r--r-- | mingling_picker/test/src/test/pos_matcher_test.rs | 141 |
5 files changed, 218 insertions, 2 deletions
diff --git a/mingling_picker/src/builtin/pick_string.rs b/mingling_picker/src/builtin/pick_string.rs index f56571a..3363165 100644 --- a/mingling_picker/src/builtin/pick_string.rs +++ b/mingling_picker/src/builtin/pick_string.rs @@ -1,4 +1,4 @@ -use crate::parselib::{ArgMatcher, Matcher, ParserStyle}; +use crate::parselib::{ArgMatcher, Matcher, ParserStyle, PositionalMatcher}; use crate::pickable_needed::*; impl<'a> Pickable<'a> for String { @@ -8,7 +8,7 @@ impl<'a> Pickable<'a> for String { fn tag(ctx: TagPhaseContext) -> Vec<usize> { if ctx.arg_info.positional { - ArgMatcher::match_one(ctx.into()) + PositionalMatcher::match_one(ctx.into()) .map(|i| vec![i]) .unwrap_or_default() } else { diff --git a/mingling_picker/src/parselib.rs b/mingling_picker/src/parselib.rs index 49e3cc2..4e9f329 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 pos_matcher; +pub use pos_matcher::*; + mod style; pub use style::*; diff --git a/mingling_picker/src/parselib/pos_matcher.rs b/mingling_picker/src/parselib/pos_matcher.rs new file mode 100644 index 0000000..a7052d4 --- /dev/null +++ b/mingling_picker/src/parselib/pos_matcher.rs @@ -0,0 +1,71 @@ +use crate::{matcher_needed::*, parselib::seek_end_of_options}; + +/// `PositionalMatcher` matches positional arguments — values not associated +/// with any named flag. +/// +/// # Rules +/// +/// * Before `--`: skips any argument that starts with the style's long or short +/// prefix (those belong to named matchers). +/// * After `--`: takes **everything** — the `--` marker signals that all +/// remaining values are positional, even if they look like flags. +/// * Runs at the lowest priority (see [`PickerArgAttr::Positional`]). +pub struct PositionalMatcher; + +impl PositionalMatcher { + /// Check whether `raw` looks like a named flag (starts with a prefix). + #[inline(always)] + fn is_flag_like(raw: &str, style: &ParserStyle) -> bool { + raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix) + } +} + +impl Matcher for PositionalMatcher { + fn on_match_one( + args: &[MaskedArg], + style: &ParserStyle, + _arg_info: &PickerArgInfo, + ) -> Option<usize> { + let end = seek_end_of_options(args, style); + + for arg in args { + if end.is_some_and(|e| arg.raw_idx == e) { + // Hit `--`: everything from here on is positional, + // including the first arg after `--`. + continue; + } + if end.is_some_and(|e| arg.raw_idx > e) { + // After `--`: accept everything. + return Some(arg.raw_idx); + } + // Before `--`: skip flag-like args. + if !Self::is_flag_like(arg.raw, style) { + return Some(arg.raw_idx); + } + } + + None + } + + fn on_match_all( + args: &[MaskedArg], + style: &ParserStyle, + _arg_info: &PickerArgInfo, + ) -> Vec<usize> { + let end = seek_end_of_options(args, style); + let mut after_end = false; + let mut result = Vec::new(); + + for arg in args { + if end.is_some_and(|e| arg.raw_idx == e) { + after_end = true; + continue; + } + if after_end || !Self::is_flag_like(arg.raw, style) { + result.push(arg.raw_idx); + } + } + + result + } +} diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs index dc251dd..e27615a 100644 --- a/mingling_picker/test/src/test.rs +++ b/mingling_picker/test/src/test.rs @@ -1,5 +1,6 @@ mod arg_matcher_test; mod basic_test; +mod pos_matcher_test; mod route_test; mod style_test; mod value_flag_test; diff --git a/mingling_picker/test/src/test/pos_matcher_test.rs b/mingling_picker/test/src/test/pos_matcher_test.rs new file mode 100644 index 0000000..b5319f5 --- /dev/null +++ b/mingling_picker/test/src/test/pos_matcher_test.rs @@ -0,0 +1,141 @@ +use mingling_picker::PickerArgInfo; +use mingling_picker::parselib::{MaskedArg, Matcher, PositionalMatcher, UNIX_STYLE, WINDOWS_STYLE}; + +fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec<MaskedArg<'a>> { + pairs + .iter() + .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx }) + .collect() +} + +// on_match_one — basic + +#[test] +fn test_pos_one_takes_first_non_flag() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("file.txt", 1)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(1)); +} + +#[test] +fn test_pos_one_takes_first_if_no_flag() { + let info = PickerArgInfo::new(); + let args = make_args(&[("file.txt", 0)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_pos_one_all_flags_returns_none() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("--name", 1)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_pos_one_empty_returns_none() { + let info = PickerArgInfo::new(); + let args = vec![]; + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_one — after `--` + +#[test] +fn test_pos_one_after_end_takes_even_flag_like() { + // After `--`, accept everything including `--verbose`. + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0), ("--verbose", 1), ("file.txt", 2)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(1)); +} + +#[test] +fn test_pos_one_only_end_returns_none() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0)]); + let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// on_match_one — Windows style prefix + +#[test] +fn test_pos_one_windows_skips_slash_prefix() { + let info = PickerArgInfo::new(); + let args = make_args(&[("/Verbose", 0), ("file.txt", 1)]); + let result = PositionalMatcher::on_match_one(&args, &WINDOWS_STYLE, &info); + assert_eq!(result, Some(1)); +} + +// on_match_all — basic + +#[test] +fn test_pos_all_collects_non_flags() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--verbose", 0), ("a.txt", 1), ("--name", 2), ("b.txt", 3)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 3]); +} + +#[test] +fn test_pos_all_only_flags_returns_empty() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--a", 0), ("--b", 1)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +#[test] +fn test_pos_all_empty() { + let info = PickerArgInfo::new(); + let args = vec![]; + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// on_match_all — after `--` + +#[test] +fn test_pos_all_after_end_accepts_everything() { + // After `--`, even `--verbose` is accepted as positional. + let info = PickerArgInfo::new(); + let args = make_args(&[ + ("--verbose", 0), + ("a.txt", 1), + ("--", 2), + ("--flag", 3), + ("b.txt", 4), + ]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 3, 4]); +} + +#[test] +fn test_pos_all_only_after_end() { + let info = PickerArgInfo::new(); + let args = make_args(&[("--", 0), ("arg1", 1), ("--arg2", 2)]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![1, 2]); +} + +// on_match_all — mixed before/after `--` + +#[test] +fn test_pos_all_mixed() { + let info = PickerArgInfo::new(); + let args = make_args(&[ + ("infile", 0), + ("--verbose", 1), + ("--", 2), + ("outfile", 3), + ("--extra", 4), + ]); + let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info); + // Before `--`: "infile" (skip --verbose). + // After `--`: everything — "outfile", "--extra". + assert_eq!(result, vec![0, 3, 4]); +} |
