aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-16 19:42:28 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-16 19:42:28 +0800
commit20253556eacdcb068210254b540f47a4522fc6f5 (patch)
treefa9724522de731e2c8d416bddf4c88f092083195
parent2287f5db3c12049afd274496a49b223365fb1e89 (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.rs151
-rw-r--r--mingling_picker/test/src/test.rs1
-rw-r--r--mingling_picker/test/src/test/arg_matcher_test.rs277
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());
+}