aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--mingling_picker/src/builtin.rs2
-rw-r--r--mingling_picker/src/builtin/pick_numbers.rs20
-rw-r--r--mingling_picker/src/builtin/pick_string.rs11
-rw-r--r--mingling_picker/src/parselib.rs6
-rw-r--r--mingling_picker/src/parselib/arg_matcher.rs36
-rw-r--r--mingling_picker/src/parselib/pos_matcher.rs71
-rw-r--r--mingling_picker/src/parselib/single_matcher.rs39
-rw-r--r--mingling_picker/src/parselib/utils.rs25
-rw-r--r--mingling_picker/src/pickable.rs3
-rw-r--r--mingling_picker/src/pickable/single_pickable.rs69
-rw-r--r--mingling_picker/src/picker.rs16
-rw-r--r--mingling_picker/src/value.rs13
-rw-r--r--mingling_picker/test/src/test.rs2
-rw-r--r--mingling_picker/test/src/test/arg_matcher_test.rs39
-rw-r--r--mingling_picker/test/src/test/pos_matcher_test.rs141
-rw-r--r--mingling_picker/test/src/test/value_string_test.rs171
-rw-r--r--mingling_picker_macros/src/arg.rs9
17 files changed, 654 insertions, 19 deletions
diff --git a/mingling_picker/src/builtin.rs b/mingling_picker/src/builtin.rs
index 00fe1e5..e855b08 100644
--- a/mingling_picker/src/builtin.rs
+++ b/mingling_picker/src/builtin.rs
@@ -1,2 +1,4 @@
mod pick_bool;
mod pick_flag;
+mod pick_numbers;
+mod pick_string;
diff --git a/mingling_picker/src/builtin/pick_numbers.rs b/mingling_picker/src/builtin/pick_numbers.rs
new file mode 100644
index 0000000..6c0f0b7
--- /dev/null
+++ b/mingling_picker/src/builtin/pick_numbers.rs
@@ -0,0 +1,20 @@
+use crate::{SinglePickable, pickable_needed::*};
+
+macro_rules! impl_single_pickable_num {
+ ($($t:ty),+ $(,)?) => {
+ $(impl SinglePickable for $t {
+ fn pick_single(str: Option<&str>) -> PickerArgResult<Self> {
+ match str {
+ Some(s) => s.parse::<$t>().map(PickerArgResult::Parsed).unwrap_or(PickerArgResult::NotFound),
+ None => PickerArgResult::NotFound,
+ }
+ }
+ })+
+ };
+}
+
+impl_single_pickable_num! {
+ i8, i16, i32, i64, i128, isize,
+ u8, u16, u32, u64, u128, usize,
+ f32, f64,
+}
diff --git a/mingling_picker/src/builtin/pick_string.rs b/mingling_picker/src/builtin/pick_string.rs
new file mode 100644
index 0000000..c96f667
--- /dev/null
+++ b/mingling_picker/src/builtin/pick_string.rs
@@ -0,0 +1,11 @@
+use crate::PickerArgResult::NotFound;
+use crate::{SinglePickable, pickable_needed::*};
+
+impl SinglePickable for String {
+ fn pick_single(str: Option<&str>) -> PickerArgResult<Self> {
+ match str {
+ Some(str) => PickerArgResult::Parsed(str.to_string()),
+ None => NotFound,
+ }
+ }
+}
diff --git a/mingling_picker/src/parselib.rs b/mingling_picker/src/parselib.rs
index 49e3cc2..456a34d 100644
--- a/mingling_picker/src/parselib.rs
+++ b/mingling_picker/src/parselib.rs
@@ -4,6 +4,12 @@ pub use flag_matcher::*;
mod arg_matcher;
pub use arg_matcher::*;
+mod pos_matcher;
+pub use pos_matcher::*;
+
+mod single_matcher;
+pub use single_matcher::*;
+
mod style;
pub use style::*;
diff --git a/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs
index 808e4a2..38bb9cc 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,18 +109,21 @@ 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 i + 1 < args.len() {
+ if !Self::is_inline_value(args[i].raw, flag_str, sep) {
+ if i + 1 < args.len()
+ // Don't consume `--` (end-of-options marker) as a value.
+ && end.is_none_or(|e| args[i + 1].raw_idx < e)
+ {
result.push(args[i + 1].raw_idx);
i += 2;
continue;
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/src/parselib/single_matcher.rs b/mingling_picker/src/parselib/single_matcher.rs
new file mode 100644
index 0000000..3606d3a
--- /dev/null
+++ b/mingling_picker/src/parselib/single_matcher.rs
@@ -0,0 +1,39 @@
+use crate::TagPhaseContext;
+use crate::parselib::{ArgMatcher, Matcher, ParserStyle, PositionalMatcher};
+
+/// `SingleMatcher` is a composite matcher for single-value parameters.
+///
+/// It delegates to [`PositionalMatcher`] for positional args and
+/// [`ArgMatcher`] for named args, adding a guard: if a named flag
+/// captures only itself with no inline value (eq mode), the result
+/// is cleared so that [`Pickable::pick`] receives `[]` → `NotFound`.
+///
+/// This is the standard tag implementation for all `Single`-type
+/// `Pickable` implementations (e.g., `String`, `i32`, `u64`).
+pub struct SingleMatcher;
+
+impl SingleMatcher {
+ /// Match a single positional value or a named flag+value pair.
+ ///
+ /// For named args, returns `[]` when the flag has no following
+ /// value and no inline separator — indicating a missing value.
+ #[inline(always)]
+ pub fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ if ctx.arg_info.positional {
+ PositionalMatcher::match_one(ctx.into())
+ .map(|i| vec![i])
+ .unwrap_or_default()
+ } else {
+ let args = ctx.args;
+ let positions = ArgMatcher::match_all(ctx.into());
+ if positions.len() == 1 {
+ let sep = ParserStyle::global_style().value_separator;
+ if let Some(raw) = args.get(positions[0])
+ && !raw.contains(sep) {
+ return vec![];
+ }
+ }
+ positions
+ }
+ }
+}
diff --git a/mingling_picker/src/parselib/utils.rs b/mingling_picker/src/parselib/utils.rs
index ae3c203..4fd34b7 100644
--- a/mingling_picker/src/parselib/utils.rs
+++ b/mingling_picker/src/parselib/utils.rs
@@ -26,6 +26,31 @@ pub fn build_possible_flags(style: &ParserStyle, arg_info: &PickerArgInfo) -> Ve
possible_flags
}
+/// Extract a single value from the raw strings tagged by [`SingleMatcher`].
+///
+/// Returns `None` if no value is available (empty slice),
+/// the inline value after the style separator if present (eq mode),
+/// or the value directly (positional or flag-following).
+///
+/// This is the standard `pick` helper for all `Single`-type
+/// [`Pickable`](crate::Pickable) implementations.
+#[must_use]
+pub fn seek_single<'a>(raw_strs: &'a [&'a str]) -> Option<&'a str> {
+ match raw_strs.len() {
+ 0 => None,
+ 1 => {
+ let s = raw_strs[0];
+ let sep = ParserStyle::global_style().value_separator;
+ if let Some(pos) = s.rfind(sep) {
+ Some(&s[pos + 1..])
+ } else {
+ Some(s)
+ }
+ }
+ _ => Some(raw_strs[1]),
+ }
+}
+
/// Seeks the index of the end-of-options marker (`--`) in the argument list.
///
/// This function searches for the standard end-of-options separator (`--`)
diff --git a/mingling_picker/src/pickable.rs b/mingling_picker/src/pickable.rs
index c88e49b..463edc0 100644
--- a/mingling_picker/src/pickable.rs
+++ b/mingling_picker/src/pickable.rs
@@ -1,5 +1,8 @@
use crate::{PickerArg, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs};
+mod single_pickable;
+pub use single_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/single_pickable.rs b/mingling_picker/src/pickable/single_pickable.rs
new file mode 100644
index 0000000..8a5b3e6
--- /dev/null
+++ b/mingling_picker/src/pickable/single_pickable.rs
@@ -0,0 +1,69 @@
+use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext};
+
+/// `SinglePickable` trait defines how to parse a type from a single command-line argument.
+///
+/// This trait provides a simplified interface for types that consume exactly one argument value.
+/// It is automatically implemented by the blanket `impl` of [`Pickable`], so types implementing
+/// `SinglePickable` will work with the full `Pickable` argument parsing system.
+///
+/// Additionally, `Option<S>` where `S: SinglePickable` also implements [`Pickable`], allowing
+/// optional arguments to be parsed naturally.
+///
+/// # Type Parameters
+///
+/// * `Self` - The type to be parsed from a single argument string.
+pub trait SinglePickable
+where
+ Self: Sized,
+{
+ /// Parse a single optional string value into an instance of `Self`.
+ ///
+ /// # Parameters
+ ///
+ /// * `str` - An `Option<&str>` representing the raw argument value. If `None`,
+ /// it indicates that no argument value was provided (e.g., for flag-like arguments).
+ ///
+ /// # Returns
+ ///
+ /// Returns [`PickerArgResult<Self>`], i.e., the parsed `Self` instance on success,
+ /// or an appropriate error message on failure.
+ fn pick_single(str: Option<&str>) -> PickerArgResult<Self>;
+}
+
+impl<'a, S> Pickable<'a> for S
+where
+ S: SinglePickable,
+{
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_single(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ crate::parselib::SingleMatcher::tag(ctx)
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ Self::pick_single(crate::parselib::seek_single(raw_strs))
+ }
+}
+
+impl<'a, S> Pickable<'a> for Option<S>
+where
+ S: SinglePickable,
+{
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_single(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ crate::parselib::SingleMatcher::tag(ctx)
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ match S::pick(raw_strs) {
+ PickerArgResult::Unparsed => PickerArgResult::Unparsed,
+ PickerArgResult::Parsed(r) => PickerArgResult::Parsed(Some(r)),
+ PickerArgResult::NotFound => PickerArgResult::Parsed(None),
+ }
+ }
+}
diff --git a/mingling_picker/src/picker.rs b/mingling_picker/src/picker.rs
index 4473d76..4efb532 100644
--- a/mingling_picker/src/picker.rs
+++ b/mingling_picker/src/picker.rs
@@ -47,6 +47,22 @@ impl<'a> Picker<'a> {
args: PickerArgs::Owned(args),
}
}
+
+ /// Changes the route (phantom type parameter) of the `Picker`.
+ ///
+ /// This method allows converting a `Picker` from one route type to another,
+ /// while preserving the same underlying arguments. The route type is typically
+ /// used to distinguish different parsing contexts or to carry compile-time
+ /// state information through the picking chain.
+ pub fn with_route<NewRoute>(self) -> Picker<'a, NewRoute>
+ where
+ Self: Sized,
+ {
+ Picker {
+ route_phantom: PhantomData,
+ args: self.args,
+ }
+ }
}
/// Internal arguments of Picker
diff --git a/mingling_picker/src/value.rs b/mingling_picker/src/value.rs
index 7d52442..ee0d6ee 100644
--- a/mingling_picker/src/value.rs
+++ b/mingling_picker/src/value.rs
@@ -1,6 +1,6 @@
use std::{
fmt::{Debug, Display},
- ops::Deref,
+ ops::{Deref, Not},
};
/// Parsed result of a boolean-style command-line flag.
@@ -132,3 +132,14 @@ impl Deref for Flag {
}
}
}
+
+impl Not for Flag {
+ type Output = Flag;
+
+ fn not(self) -> Flag {
+ match self {
+ Flag::Active => Flag::Inactive,
+ Flag::Inactive => Flag::Active,
+ }
+ }
+}
diff --git a/mingling_picker/test/src/test.rs b/mingling_picker/test/src/test.rs
index eb53229..e27615a 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 pos_matcher_test;
mod route_test;
mod style_test;
mod value_flag_test;
+mod value_string_test;
diff --git a/mingling_picker/test/src/test/arg_matcher_test.rs b/mingling_picker/test/src/test/arg_matcher_test.rs
index 947e20f..7b37bc8 100644
--- a/mingling_picker/test/src/test/arg_matcher_test.rs
+++ b/mingling_picker/test/src/test/arg_matcher_test.rs
@@ -248,3 +248,42 @@ fn test_match_all_end_of_options_not_matched() {
let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
assert!(result.is_empty());
}
+
+// `--` should never be consumed as a value by a named flag.
+
+#[test]
+fn test_match_all_named_does_not_consume_end_marker() {
+ // `--flag` before `--`, but the next position IS `--`.
+ // Only tag the flag, don't consume the end-of-options marker.
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag");
+ let args = make_args(&[("--flag", 0), ("--", 1), ("value", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0], "should NOT consume -- as a value");
+}
+
+#[test]
+fn test_match_all_named_does_not_consume_end_marker_flag_before_end() {
+ // Simulates: --flag -- you where -- is end-of-options.
+ // The flag should be tagged but -- should NOT be consumed as its value.
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag");
+
+ let args = make_args(&[("--flag", 0), ("--", 1), ("you", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result,
+ vec![0],
+ "flag before --: only tag flag, leave -- for positional"
+ );
+}
+
+#[test]
+fn test_match_all_flag_after_end_has_value() {
+ // Flag after `--` should not match at all.
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag");
+ let args = make_args(&[("--", 0), ("--flag", 1), ("value", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
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]);
+}
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");
+}
diff --git a/mingling_picker_macros/src/arg.rs b/mingling_picker_macros/src/arg.rs
index e273d23..70b27b0 100644
--- a/mingling_picker_macros/src/arg.rs
+++ b/mingling_picker_macros/src/arg.rs
@@ -161,11 +161,12 @@ fn extract_char(tokens: &[TokenTree]) -> Option<char> {
let cs: Vec<char> = s.chars().collect();
if cs.len() >= 3 && cs[0] == '\'' && cs[cs.len() - 1] == '\'' {
let inner: String = cs[1..cs.len() - 1].iter().collect();
+ // inner is the character between the quotes of a char literal.
+ // For a literal `'n'`, inner is `"n"` (the character n).
+ // For an escape `'\n'`, inner is the actual newline character.
+ // The catch-all handles both literal single chars and escape
+ // sequences (the escaped char IS the actual control character).
match inner.as_str() {
- "n" => Some('\n'),
- "t" => Some('\t'),
- "r" => Some('\r'),
- "0" => Some('\0'),
"\\\\" => Some('\\'),
"\\'" => Some('\''),
_ => inner.chars().next(),