aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-07-16 19:58:30 +0800
committer魏曹先生 <1992414357@qq.com>2026-07-16 19:58:30 +0800
commit81049b3abd83115374cfc51b26fb5d5054263f81 (patch)
treebea8e56ebacf96d685b545bc7ea3c8d152910bf9
parent4f4a61d493547660260a5aa1db1af8a6ec42bbb6 (diff)
feat(picker): add string value support with inline value matching
Implement `Pickable` for `String` supporting both positional and named flags, and generalize value matching to use style-specific separators
-rw-r--r--mingling_picker/src/builtin.rs1
-rw-r--r--mingling_picker/src/builtin/pick_string.rs48
-rw-r--r--mingling_picker/src/parselib/arg_matcher.rs31
-rw-r--r--mingling_picker/test/src/test.rs1
-rw-r--r--mingling_picker/test/src/test/value_string_test.rs171
5 files changed, 239 insertions, 13 deletions
diff --git a/mingling_picker/src/builtin.rs b/mingling_picker/src/builtin.rs
index 00fe1e5..d179f86 100644
--- a/mingling_picker/src/builtin.rs
+++ b/mingling_picker/src/builtin.rs
@@ -1,2 +1,3 @@
mod pick_bool;
mod pick_flag;
+mod pick_string;
diff --git a/mingling_picker/src/builtin/pick_string.rs b/mingling_picker/src/builtin/pick_string.rs
new file mode 100644
index 0000000..688ac0a
--- /dev/null
+++ b/mingling_picker/src/builtin/pick_string.rs
@@ -0,0 +1,48 @@
+use crate::parselib::{ArgMatcher, Matcher, ParserStyle};
+use crate::pickable_needed::*;
+
+impl<'a> Pickable<'a> for String {
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_single(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ if ctx.arg_info.positional {
+ // Positional: take the first available position only.
+ ArgMatcher::match_one(ctx.into())
+ .map(|i| vec![i])
+ .unwrap_or_default()
+ } else {
+ // Named: find each flag + its single value.
+ ArgMatcher::match_all(ctx.into())
+ }
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ match raw_strs.len() {
+ 0 => PickerArgResult::NotFound,
+ 1 => {
+ let s = raw_strs[0];
+ let style = ParserStyle::global_style();
+
+ // Inline value via style separator (e.g., --name=Alice, -Name:Alice).
+ if let Some(pos) = s.rfind(style.value_separator) {
+ return PickerArgResult::Parsed(s[pos + 1..].to_string());
+ }
+
+ // If the single element looks like a named flag (starts with a prefix
+ // such as `--`, `-`, or `/`), it's a flag with no value → NotFound.
+ if s.starts_with(style.long_prefix) || s.starts_with(style.short_prefix) {
+ return PickerArgResult::NotFound;
+ }
+
+ // Positional value.
+ PickerArgResult::Parsed(s.to_string())
+ }
+ _ => {
+ // flag + value as separate args: take the second element.
+ PickerArgResult::Parsed(raw_strs[1].to_string())
+ }
+ }
+ }
+}
diff --git a/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs
index 808e4a2..d4b8793 100644
--- a/mingling_picker/src/parselib/arg_matcher.rs
+++ b/mingling_picker/src/parselib/arg_matcher.rs
@@ -27,25 +27,28 @@ use crate::{
pub struct ArgMatcher;
impl ArgMatcher {
- /// Check whether `raw` matches `flag_str` (exact or eq-separated).
+ /// Check whether `raw` matches `flag_str`, optionally with an inline value
+ /// separated by the style's value separator (`=` for Unix, `:` for PowerShell).
#[inline(always)]
- fn matches(raw: &str, flag_str: &str, case_sensitive: bool) -> bool {
+ fn matches(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool {
+ let eq_match =
+ |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) && raw.as_bytes().get(flag_str.len()) == Some(&b'=')
+ raw == flag_str || (raw.starts_with(flag_str) && eq_match(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()] == b'=')
+ && raw.as_bytes()[flag_str.len()] == sep as u8)
}
}
- /// Check whether the argument at the given position (in the masked slice)
- /// contains its value inline (eq mode), so no extra slot is needed.
+ /// Check whether the argument contains its value inline via the style's
+ /// value separator (eq mode), so no extra mask 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'=')
+ fn is_inline_value(raw: &str, flag_str: &str, sep: char) -> bool {
+ raw.len() > flag_str.len() && raw.as_bytes().get(flag_str.len()) == Some(&(sep as u8))
}
}
@@ -61,6 +64,7 @@ impl Matcher for ArgMatcher {
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) {
@@ -69,7 +73,7 @@ impl Matcher for ArgMatcher {
let matched = possible_flags
.iter()
- .any(|f| Self::matches(arg.raw, f, style.case_sensitive));
+ .any(|f| Self::matches(arg.raw, f, style.case_sensitive, sep));
if matched {
return Some(arg.raw_idx);
}
@@ -94,6 +98,7 @@ impl Matcher for ArgMatcher {
let possible_flags = build_possible_flags(style, arg_info);
let end = seek_end_of_options(args, style);
+ let sep = style.value_separator;
let mut result = Vec::new();
let mut i = 0;
@@ -104,17 +109,17 @@ impl Matcher for ArgMatcher {
let matched = possible_flags
.iter()
- .any(|f| Self::matches(args[i].raw, f, style.case_sensitive));
+ .any(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep));
if matched {
let flag_str = possible_flags
.iter()
- .find(|f| Self::matches(args[i].raw, f, style.case_sensitive))
+ .find(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep))
.expect("already matched");
result.push(args[i].raw_idx);
- if !Self::is_eq_mode(args[i].raw, flag_str) {
+ if !Self::is_inline_value(args[i].raw, flag_str, sep) {
if i + 1 < args.len() {
result.push(args[i + 1].raw_idx);
i += 2;
diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs
index eb53229..dc251dd 100644
--- a/mingling_picker/test/src/test.rs
+++ b/mingling_picker/test/src/test.rs
@@ -3,3 +3,4 @@ mod basic_test;
mod route_test;
mod style_test;
mod value_flag_test;
+mod value_string_test;
diff --git a/mingling_picker/test/src/test/value_string_test.rs b/mingling_picker/test/src/test/value_string_test.rs
new file mode 100644
index 0000000..b1e5c0b
--- /dev/null
+++ b/mingling_picker/test/src/test/value_string_test.rs
@@ -0,0 +1,171 @@
+use mingling_picker::{IntoPicker, macros::arg};
+
+// Basic named String — present / absent
+
+#[test]
+fn test_string_named_present() {
+ let val: String = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "Alice");
+}
+
+#[test]
+fn test_string_named_absent_uses_default() {
+ let val: String = Vec::<&str>::new()
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// Named String — eq mode
+
+#[test]
+fn test_string_named_eq_mode() {
+ let val: String = vec!["--name=Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "Alice");
+}
+
+// Named String — short flag
+
+#[test]
+fn test_string_named_short_flag() {
+ let val: String = vec!["-n", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String, 'n'])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "Alice");
+}
+
+// Named String — no value after flag
+
+#[test]
+fn test_string_named_missing_value_triggers_default() {
+ // --name at end with no following arg → pick returns NotFound → or_default gives ""
+ let val: String = vec!["--name"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// Positional String
+
+#[test]
+fn test_string_positional() {
+ let val: String = vec!["file.txt"]
+ .to_picker()
+ .pick(&arg![String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "file.txt");
+}
+
+#[test]
+fn test_string_positional_takes_first() {
+ let val: String = vec!["first", "second"]
+ .to_picker()
+ .pick(&arg![String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "first");
+}
+
+// Multiple occurrences (Single only tags one occurrence per Parseable)
+
+#[test]
+fn test_string_two_named_flags() {
+ let (a, b): (String, String) = vec!["--name", "Alice", "--greeting", "Hello"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .pick(&arg![greeting: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, "Alice");
+ assert_eq!(b, "Hello");
+}
+
+// Mixed named + positional
+
+#[test]
+fn test_string_named_and_positional() {
+ let (name, file): (String, String) = vec!["--name", "Alice", "file.txt"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .pick(&arg![String])
+ .or_default()
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(file, "file.txt");
+}
+
+// After `--` (end-of-options)
+
+#[test]
+fn test_string_named_after_end_of_options() {
+ // Named arg after `--` should not be matched → default
+ let val: String = vec!["--", "--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// Unrelated flag should not match → default
+
+#[test]
+fn test_string_unrelated_flag() {
+ let val: String = vec!["--other", "value"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// to_result / to_option
+
+#[test]
+fn test_string_to_result() {
+ let result: Result<String, ()> = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .to_result();
+ assert_eq!(result, Ok("Alice".to_string()));
+}
+
+#[test]
+fn test_string_to_option() {
+ let opt: Option<String> = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .to_option();
+ assert_eq!(opt, Some("Alice".to_string()));
+}
+
+// Custom default via .or()
+
+#[test]
+fn test_string_custom_default() {
+ let val: String = Vec::<&str>::new()
+ .to_picker()
+ .pick(&arg![name: String])
+ .or(|| "default_name".to_string())
+ .unwrap();
+ assert_eq!(val, "default_name");
+}