diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-07-16 19:42:28 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-07-16 19:42:28 +0800 |
| commit | 20253556eacdcb068210254b540f47a4522fc6f5 (patch) | |
| tree | fa9724522de731e2c8d416bddf4c88f092083195 | |
| parent | 2287f5db3c12049afd274496a49b223365fb1e89 (diff) | |
feat(parselib): implement ArgMatcher for named and positional args
Implement `on_match_one` and `on_match_all` for `ArgMatcher`,
supporting named flags (with eq-mode and short flags), positional
arguments, end-of-options marker (`--`), and case-insensitive matching
| -rw-r--r-- | mingling_picker/src/parselib/arg_matcher.rs | 151 | ||||
| -rw-r--r-- | mingling_picker/test/src/test.rs | 1 | ||||
| -rw-r--r-- | mingling_picker/test/src/test/arg_matcher_test.rs | 277 |
3 files changed, 420 insertions, 9 deletions
diff --git a/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs index e8be999..931652a 100644 --- a/mingling_picker/src/parselib/arg_matcher.rs +++ b/mingling_picker/src/parselib/arg_matcher.rs @@ -1,21 +1,154 @@ -use crate::matcher_needed::*; +use crate::{matcher_needed::*, parselib::build_possible_flags}; +/// `ArgMatcher` is used for parameters that carry a single value. +/// +/// It handles two scenarios: +/// +/// **Named** — `--name Alice` or `--name=Alice`. +/// Each flag occurrence consumes **one** following argument as its value, +/// regardless of what it is (even if it looks like a flag). +/// This ensures the mask correctly claims the value slot; validation is +/// the `Pickable`'s responsibility. +/// +/// **Positional** — no flag prefix, matched by position. +/// +/// # Examples +/// +/// | Input | `on_match_one` | `on_match_all` | +/// |-------|----------------|----------------| +/// | `--name Alice` | `[0, 1]` (via Pickable tag) | `[0, 1]` | +/// | `--name=Alice` | `[0]` | `[0]` | +/// | `--val a --val b` | `[0, 1]` | `[0, 1, 2, 3]` | +/// +/// Args after `--` are ignored. pub struct ArgMatcher; +impl ArgMatcher { + /// Check whether `raw` matches `flag_str` (exact or eq-separated). + #[inline(always)] + fn matches(raw: &str, flag_str: &str, case_sensitive: bool) -> bool { + if case_sensitive { + raw == flag_str + || raw.starts_with(flag_str) && raw.as_bytes().get(flag_str.len()) == Some(&b'=') + } 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()] == b'=') + } + } + + /// Check whether the argument at the given position (in the masked slice) + /// contains its value inline (eq mode), so no extra slot is needed. + #[inline(always)] + fn is_eq_mode(raw: &str, flag_str: &str) -> bool { + raw.len() > flag_str.len() && raw.as_bytes().get(flag_str.len()) == Some(&b'=') + } +} + impl Matcher for ArgMatcher { fn on_match_one( - _args: &[MaskedArg], - _style: &ParserStyle, - _arg_info: &PickerArgInfo, + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, ) -> Option<usize> { - todo!() + if arg_info.positional { + // Positional: first available position. + 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); + + for arg in args { + // Stop at end-of-options marker. + if end.is_some_and(|e| arg.raw_idx >= e) { + break; + } + + let matched = possible_flags + .iter() + .any(|f| Self::matches(arg.raw, f, style.case_sensitive)); + if matched { + return Some(arg.raw_idx); + } + } + + None } fn on_match_all( - _args: &[MaskedArg], - _style: &ParserStyle, - _arg_info: &PickerArgInfo, + args: &[MaskedArg], + style: &ParserStyle, + arg_info: &PickerArgInfo, ) -> Vec<usize> { - todo!() + if arg_info.positional { + // Positional: all available positions before `--`. + 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 mut result = Vec::new(); + let mut i = 0; + while i < args.len() { + // Stop at end-of-options marker. + if end.is_some_and(|e| args[i].raw_idx >= e) { + break; + } + + let matched = possible_flags + .iter() + .any(|f| Self::matches(args[i].raw, f, style.case_sensitive)); + + if matched { + // Find which flag matched to check eq mode. + let flag_str = possible_flags + .iter() + .find(|f| Self::matches(args[i].raw, f, style.case_sensitive)) + .expect("already matched"); + + result.push(args[i].raw_idx); // flag position + + if !Self::is_eq_mode(args[i].raw, flag_str) { + // Non-eq mode: the next argument is the value. + // Always tag it — even if it looks like a flag — so that + // the mask reserves it. Validation is the Pickable's job. + if i + 1 < args.len() { + result.push(args[i + 1].raw_idx); + i += 2; // skip flag + value + continue; + } + // No value available: tag just the flag. + i += 1; + continue; + } + // eq mode: value is inline, no extra slot. + i += 1; + continue; + } + i += 1; + } + + result } } + +/// Locate the end-of-options marker (`--`) in the argument list. +fn seek_end_of_options(args: &[MaskedArg], style: &ParserStyle) -> Option<usize> { + args.iter() + .find(|arg| { + if style.case_sensitive { + arg.raw == style.end_of_options + } else { + arg.raw.eq_ignore_ascii_case(style.end_of_options) + } + }) + .map(|arg| arg.raw_idx) +} diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs index 29d52ec..eb53229 100644 --- a/mingling_picker/test/src/test.rs +++ b/mingling_picker/test/src/test.rs @@ -1,3 +1,4 @@ +mod arg_matcher_test; mod basic_test; mod route_test; mod style_test; diff --git a/mingling_picker/test/src/test/arg_matcher_test.rs b/mingling_picker/test/src/test/arg_matcher_test.rs new file mode 100644 index 0000000..3249079 --- /dev/null +++ b/mingling_picker/test/src/test/arg_matcher_test.rs @@ -0,0 +1,277 @@ +use mingling_picker::PickerArgInfo; +use mingling_picker::parselib::{ArgMatcher, MaskedArg, Matcher, POWERSHELL_STYLE, UNIX_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 — Named +// ============================================================ + +#[test] +fn test_match_one_named_basic() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_named_eq_mode() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name=Alice", 0)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_named_no_match() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--other", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_match_one_named_short_flag() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + info.set_short('n'); + let args = make_args(&[("-n", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_named_after_end_of_options() { + // Flags after `--` should not be matched. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--", 0), ("--name", 1), ("Alice", 2)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +// ============================================================ +// on_match_one — Positional +// ============================================================ + +#[test] +fn test_match_one_positional_basic() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("file.txt", 0)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +#[test] +fn test_match_one_positional_takes_first() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, Some(0)); +} + +// ============================================================ +// on_match_all — Named, single occurrence +// ============================================================ + +#[test] +fn test_match_all_named_flag_plus_value() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_match_all_named_eq_mode() { + // --name=Alice: value is inline, only tag the flag position. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name=Alice", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_match_all_named_no_value() { + // Flag at end with no following arg: only tag the flag. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_match_all_named_value_looks_like_flag() { + // The next arg looks like a flag — still tag it. + // Validation is the Pickable's responsibility. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("--other", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// ============================================================ +// on_match_all — Named, multiple occurrences (Single per flag) +// ============================================================ + +#[test] +fn test_match_all_named_two_occurrences() { + // --name Alice --name Bob → each occurrence gets one value. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name", 0), ("Alice", 1), ("--name", 2), ("Bob", 3)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2, 3]); +} + +#[test] +fn test_match_all_named_skips_non_matching_args() { + // --val a b --val d → only pairs (0,1) and (3,4); idx 2 ("b") left free. + let mut info = PickerArgInfo::new(); + info.set_long("val"); + let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--val", 3), ("d", 4)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 3, 4]); +} + +// ============================================================ +// on_match_all — Named, short flag +// ============================================================ + +#[test] +fn test_match_all_named_short_flag() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + info.set_short('n'); + let args = make_args(&[("-n", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// ============================================================ +// on_match_all — Named, eq + non-eq mixed +// ============================================================ + +#[test] +fn test_match_all_named_mixed_eq_and_regular() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--name=Alice", 0), ("--name", 1), ("Bob", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1, 2]); +} + +// ============================================================ +// on_match_all — Named, case insensitive (PowerShell) +// ============================================================ + +#[test] +fn test_match_all_named_powershell_case_insensitive() { + let mut info = PickerArgInfo::new(); + info.set_long("Name"); + let args = make_args(&[("-name", 0), ("Alice", 1)]); + let result = ArgMatcher::on_match_all(&args, &POWERSHELL_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// ============================================================ +// on_match_all — Positional +// ============================================================ + +#[test] +fn test_match_all_positional_single() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("file.txt", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +#[test] +fn test_match_all_positional_multiple() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("a.txt", 0), ("b.txt", 1)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +// ============================================================ +// End-of-options marker (`--`) +// ============================================================ + +#[test] +fn test_match_all_named_stops_at_end_of_options() { + // --name before `--` should match, --name after should not. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[ + ("--name", 0), + ("Alice", 1), + ("--", 2), + ("--name", 3), + ("Bob", 4), + ]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0, 1]); +} + +#[test] +fn test_match_all_positional_stops_at_end_of_options() { + let mut info = PickerArgInfo::new(); + info.set_positional(true); + let args = make_args(&[("before", 0), ("--", 1), ("after", 2)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert_eq!(result, vec![0]); +} + +// ============================================================ +// Empty args +// ============================================================ + +#[test] +fn test_match_one_empty() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = vec![]; + let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info); + assert_eq!(result, None); +} + +#[test] +fn test_match_all_empty() { + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = vec![]; + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} + +// ============================================================ +// Verify that -- itself is never matched as a flag +// ============================================================ + +#[test] +fn test_match_all_end_of_options_not_matched() { + // `--` should neither match as a flag nor take a value. + let mut info = PickerArgInfo::new(); + info.set_long("name"); + let args = make_args(&[("--", 0)]); + let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info); + assert!(result.is_empty()); +} |
