aboutsummaryrefslogtreecommitdiff
path: root/mingling_picker/src
diff options
context:
space:
mode:
Diffstat (limited to 'mingling_picker/src')
-rw-r--r--mingling_picker/src/arg.rs299
-rw-r--r--mingling_picker/src/builtin.rs4
-rw-r--r--mingling_picker/src/builtin/pick_bool.rs22
-rw-r--r--mingling_picker/src/builtin/pick_flag.rs21
-rw-r--r--mingling_picker/src/builtin/pick_numbers.rs80
-rw-r--r--mingling_picker/src/builtin/pick_string.rs11
-rw-r--r--mingling_picker/src/corebind.rs1
-rw-r--r--mingling_picker/src/infos.rs446
-rw-r--r--mingling_picker/src/lib.rs44
-rw-r--r--mingling_picker/src/parselib.rs144
-rw-r--r--mingling_picker/src/parselib/arg_matcher.rs142
-rw-r--r--mingling_picker/src/parselib/flag_matcher.rs82
-rw-r--r--mingling_picker/src/parselib/multi_arg_matcher.rs141
-rw-r--r--mingling_picker/src/parselib/pos_matcher.rs71
-rw-r--r--mingling_picker/src/parselib/single_matcher.rs59
-rw-r--r--mingling_picker/src/parselib/style.rs225
-rw-r--r--mingling_picker/src/parselib/utils.rs271
-rw-r--r--mingling_picker/src/pickable.rs91
-rw-r--r--mingling_picker/src/pickable/multi_pickable.rs77
-rw-r--r--mingling_picker/src/pickable/single_pickable.rs69
-rw-r--r--mingling_picker/src/picker.rs360
-rw-r--r--mingling_picker/src/picker/parse.rs177
-rw-r--r--mingling_picker/src/picker/patterns.rs199
-rw-r--r--mingling_picker/src/picker/result.rs56
-rw-r--r--mingling_picker/src/value.rs5
-rw-r--r--mingling_picker/src/value/flag.rs145
-rw-r--r--mingling_picker/src/value/vec_until.rs134
27 files changed, 3375 insertions, 1 deletions
diff --git a/mingling_picker/src/arg.rs b/mingling_picker/src/arg.rs
new file mode 100644
index 0000000..78ad539
--- /dev/null
+++ b/mingling_picker/src/arg.rs
@@ -0,0 +1,299 @@
+use crate::Pickable;
+use std::marker::PhantomData;
+
+/// Represents a constraint definition for a parameter selection.
+///
+/// This structure describes the constraints that a command-line parameter (Picker parameter item)
+/// should satisfy, including its full name list (with aliases), short name form, and whether it is
+/// positional.
+///
+/// # Field Descriptions
+///
+/// - `full`: Full name or alias list. For example, `["config", "cfg"]` means the parameter can be
+/// matched with either `--config` or `--cfg`. Must contain at least one non-empty string.
+///
+/// - `short`: Short name (single character). For example, `Some('c')` means it can be passed using
+/// the `-c` form. If set to `None`, the short name form is not supported.
+///
+/// - `positional`: Whether the parameter is positional (i.e., an argument without a flag).
+/// - `true`: The parameter is positional; it is matched by its position in the command line rather
+/// than by a `--name` or `-n` flag.
+/// - `false`: The parameter is a named (flag-based) parameter.
+///
+/// - `_type`: PhantomData to hold the type parameter.
+#[derive(Default, Clone, Copy)]
+pub struct PickerArg<'a, Type>
+where
+ Type: Pickable<'a>,
+{
+ /// Full name, may include variant names (aliases), e.g., `["config", "cfg"]`.
+ pub full: &'a [&'a str],
+
+ /// Short name, e.g., `'c'`.
+ pub short: Option<char>,
+
+ /// Whether the parameter is positional (no flag, matched by position).
+ pub positional: bool,
+
+ /// PhantomData to hold the type parameter.
+ pub internal_type: PhantomData<Type>,
+}
+
+impl<'a, Type> From<&'a PickerArg<'a, Type>> for PickerArg<'a, Type>
+where
+ Type: Pickable<'a>,
+{
+ fn from(value: &'a PickerArg<'a, Type>) -> Self {
+ PickerArg {
+ full: value.full,
+ short: value.short,
+ positional: value.positional,
+ internal_type: PhantomData,
+ }
+ }
+}
+
+impl<'a, Type> PickerArg<'a, Type>
+where
+ Type: Pickable<'a>,
+{
+ /// Creates a new `PickerArg` with the provided parameters.
+ pub fn new(full: &'a [&'a str], short: Option<char>, positional: bool) -> Self {
+ Self {
+ full,
+ short,
+ positional,
+ internal_type: PhantomData,
+ }
+ }
+
+ /// Returns the full name list (including aliases).
+ pub fn full(&self) -> &'a [&'a str] {
+ self.full
+ }
+
+ /// Returns the short name, if any.
+ pub fn short(&self) -> Option<char> {
+ self.short
+ }
+
+ /// Returns whether the parameter is positional.
+ ///
+ /// If `full` is empty or `short` is `None`, the parameter is considered positional
+ /// regardless of the stored value.
+ pub fn is_positional(&self) -> bool {
+ if self.full.is_empty() && self.short.is_none() {
+ true
+ } else {
+ self.positional
+ }
+ }
+
+ /// Sets the full name list.
+ pub fn set_full(&mut self, full: &'a [&'a str]) {
+ self.full = full;
+ }
+
+ /// Sets the short name.
+ pub fn set_short(&mut self, short: Option<char>) {
+ self.short = short;
+ }
+
+ /// Sets whether the parameter is positional.
+ pub fn set_positional(&mut self, positional: bool) {
+ self.positional = positional;
+ }
+
+ /// Sets the full name list and returns self.
+ pub fn with_full(mut self, full: &'a [&'a str]) -> Self {
+ self.full = full;
+ self
+ }
+
+ /// Clears the full name list (sets it to an empty slice) and returns self.
+ pub fn without_full(mut self) -> Self {
+ self.full = &[];
+ self
+ }
+
+ /// Sets the short name to the given character and returns self.
+ pub fn with_short(mut self, short: char) -> Self {
+ self.short = Some(short);
+ self
+ }
+
+ /// Clears the short name (sets it to None) and returns self.
+ pub fn without_short(mut self) -> Self {
+ self.short = None;
+ self
+ }
+
+ /// Sets whether the parameter is positional and returns self.
+ pub fn with_positional(mut self, positional: bool) -> Self {
+ self.positional = positional;
+ self
+ }
+}
+
+/// Describes the attribute (behavior) of a command-line parameter.
+///
+/// The ordering reflects parse priority (higher = parsed first):
+/// `PositionalMulti < Positional < Flag < Single < Multi`
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub enum PickerArgAttr {
+ /// Positional argument that accepts multiple values (e.g., multiple input files).
+ PositionalMulti,
+
+ /// Positional argument matched by its position (e.g., an input file).
+ #[default]
+ Positional,
+
+ /// Boolean flag with no associated value (e.g., `--verbose`).
+ Flag,
+
+ /// Accepts a single value (e.g., `--name Alice`).
+ Single,
+
+ /// Accepts multiple values (e.g., `--file a.txt --file b.txt`).
+ Multi,
+}
+
+impl PickerArgAttr {
+ /// Determines if the given `PickerArg` represents a positional parameter.
+ ///
+ /// If the flag is positional (determined by `flag.is_positional()`), returns
+ /// `PickerArgAttr::Positional`. Otherwise, invokes the `other` closure to
+ /// produce and return a `PickerArgAttr`.
+ ///
+ /// # Parameters
+ ///
+ /// - `flag`: A reference to the [`PickerArg`] to evaluate.
+ /// - `other`: A closure that returns a [`PickerArgAttr`] when the flag is
+ /// **not** positional.
+ #[inline(always)]
+ pub fn positional_or_else<'a, T>(
+ flag: &PickerArg<'a, T>,
+ other: fn() -> PickerArgAttr,
+ ) -> PickerArgAttr
+ where
+ T: Pickable<'a>,
+ {
+ if flag.is_positional() {
+ PickerArgAttr::Positional
+ } else {
+ other()
+ }
+ }
+
+ /// Determines if the given `PickerArg` represents a positional parameter and returns
+ /// `PickerArgAttr::Positional` if so. Otherwise, returns the provided `default` attribute.
+ ///
+ /// # Parameters
+ ///
+ /// - `flag`: A reference to the [`PickerArg`] to evaluate.
+ /// - `default`: The [`PickerArgAttr`] to return if the flag is not positional.
+ #[inline(always)]
+ pub fn positional_or<'a, T>(flag: &PickerArg<'a, T>, default: PickerArgAttr) -> PickerArgAttr
+ where
+ T: Pickable<'a>,
+ {
+ if flag.is_positional() {
+ PickerArgAttr::Positional
+ } else {
+ default
+ }
+ }
+
+ /// Determines if the given `PickerArg` represents a positional parameter and returns
+ /// `PickerArgAttr::Positional` if so. Otherwise, returns `PickerArgAttr::Single`.
+ ///
+ /// # Parameters
+ ///
+ /// - `flag`: A reference to the [`PickerArg`] to evaluate.
+ #[inline(always)]
+ pub fn positional_or_single<'a, T>(flag: &PickerArg<'a, T>) -> PickerArgAttr
+ where
+ T: Pickable<'a>,
+ {
+ if flag.is_positional() {
+ PickerArgAttr::Positional
+ } else {
+ PickerArgAttr::Single
+ }
+ }
+
+ /// Determines if the given `PickerArg` represents a positional parameter and returns
+ /// `PickerArgAttr::PositionalMulti` if so. Otherwise, returns `PickerArgAttr::Multi`.
+ ///
+ /// # Parameters
+ ///
+ /// - `flag`: A reference to the [`PickerArg`] to evaluate.
+ #[inline(always)]
+ pub fn positional_or_multi<'a, T>(flag: &PickerArg<'a, T>) -> PickerArgAttr
+ where
+ T: Pickable<'a>,
+ {
+ if flag.is_positional() {
+ PickerArgAttr::PositionalMulti
+ } else {
+ PickerArgAttr::Multi
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_picker_flag_attr_ordering() {
+ // Multi > Single > Flag > Positional > PositionalMulti
+ assert!(PickerArgAttr::Multi > PickerArgAttr::Single);
+ assert!(PickerArgAttr::Multi > PickerArgAttr::Flag);
+ assert!(PickerArgAttr::Multi > PickerArgAttr::Positional);
+ assert!(PickerArgAttr::Multi > PickerArgAttr::PositionalMulti);
+
+ assert!(PickerArgAttr::Single > PickerArgAttr::Flag);
+ assert!(PickerArgAttr::Single > PickerArgAttr::Positional);
+ assert!(PickerArgAttr::Single > PickerArgAttr::PositionalMulti);
+
+ assert!(PickerArgAttr::Flag > PickerArgAttr::Positional);
+ assert!(PickerArgAttr::Flag > PickerArgAttr::PositionalMulti);
+
+ assert!(PickerArgAttr::Positional > PickerArgAttr::PositionalMulti);
+
+ // PartialOrd
+ assert!(PickerArgAttr::Multi >= PickerArgAttr::Single);
+ assert!(PickerArgAttr::Single >= PickerArgAttr::Flag);
+ assert!(PickerArgAttr::Flag >= PickerArgAttr::Positional);
+ assert!(PickerArgAttr::Positional >= PickerArgAttr::PositionalMulti);
+
+ assert!(PickerArgAttr::PositionalMulti < PickerArgAttr::Positional);
+ assert!(PickerArgAttr::Positional < PickerArgAttr::Flag);
+ assert!(PickerArgAttr::Flag < PickerArgAttr::Single);
+ assert!(PickerArgAttr::Single < PickerArgAttr::Multi);
+ }
+
+ #[test]
+ fn test_picker_flag_attr_sorting() {
+ // Sort
+ let mut values = vec![
+ PickerArgAttr::Flag,
+ PickerArgAttr::Single,
+ PickerArgAttr::Positional,
+ PickerArgAttr::Multi,
+ PickerArgAttr::PositionalMulti,
+ ];
+ values.sort();
+ assert_eq!(
+ values,
+ vec![
+ PickerArgAttr::PositionalMulti,
+ PickerArgAttr::Positional,
+ PickerArgAttr::Flag,
+ PickerArgAttr::Single,
+ PickerArgAttr::Multi,
+ ]
+ );
+ }
+}
diff --git a/mingling_picker/src/builtin.rs b/mingling_picker/src/builtin.rs
new file mode 100644
index 0000000..e855b08
--- /dev/null
+++ b/mingling_picker/src/builtin.rs
@@ -0,0 +1,4 @@
+mod pick_bool;
+mod pick_flag;
+mod pick_numbers;
+mod pick_string;
diff --git a/mingling_picker/src/builtin/pick_bool.rs b/mingling_picker/src/builtin/pick_bool.rs
new file mode 100644
index 0000000..ccc4424
--- /dev/null
+++ b/mingling_picker/src/builtin/pick_bool.rs
@@ -0,0 +1,22 @@
+use crate::parselib::{FlagMatcher, Matcher};
+use crate::pickable_needed::*;
+
+impl<'a> Pickable<'a> for bool {
+ fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::Flag
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ FlagMatcher::match_all(ctx.into())
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ if raw_strs.is_empty() {
+ // No matching flag found — signal NotFound so the fallback chain
+ // (default → route) gets a chance to run.
+ PickerArgResult::NotFound
+ } else {
+ PickerArgResult::Parsed(true)
+ }
+ }
+}
diff --git a/mingling_picker/src/builtin/pick_flag.rs b/mingling_picker/src/builtin/pick_flag.rs
new file mode 100644
index 0000000..b642a9a
--- /dev/null
+++ b/mingling_picker/src/builtin/pick_flag.rs
@@ -0,0 +1,21 @@
+use crate::parselib::{FlagMatcher, Matcher};
+use crate::pickable_needed::*;
+use crate::value::Flag;
+
+impl<'a> Pickable<'a> for Flag {
+ fn get_attr(_: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::Flag
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ FlagMatcher::match_all(ctx.into())
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ if raw_strs.is_empty() {
+ PickerArgResult::Parsed(Flag::Inactive)
+ } else {
+ PickerArgResult::Parsed(Flag::Active)
+ }
+ }
+}
diff --git a/mingling_picker/src/builtin/pick_numbers.rs b/mingling_picker/src/builtin/pick_numbers.rs
new file mode 100644
index 0000000..a5ab0a9
--- /dev/null
+++ b/mingling_picker/src/builtin/pick_numbers.rs
@@ -0,0 +1,80 @@
+use crate::{BoundaryCheck, 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,
+ }
+ }
+ })+
+ };
+}
+
+/// Returns `true` if `raw` looks like an integer (digits, optional `+`/`-` prefix,
+/// no decimal point or exponent).
+fn is_int_like(raw: &str) -> bool {
+ let s = raw.trim();
+ let bytes = s.as_bytes();
+ if bytes.is_empty() {
+ return false;
+ }
+ let mut i = 0;
+ if bytes[0] == b'-' || bytes[0] == b'+' {
+ i = 1;
+ }
+ if i >= bytes.len() {
+ return false;
+ }
+ for &b in &bytes[i..] {
+ if !b.is_ascii_digit() {
+ return false;
+ }
+ }
+ true
+}
+
+/// Returns `true` if `raw` looks like a float (contains `.`, `e`, or `E`).
+fn is_float_like(raw: &str) -> bool {
+ let s = raw.trim();
+ s.contains('.') || s.contains('e') || s.contains('E')
+}
+
+impl_single_pickable_num! {
+ i8, i16, i32, i64, i128, isize,
+ u8, u16, u32, u64, u128, usize,
+ f32, f64,
+}
+
+// Integer boundary: only accept strings that look like integers.
+// Float-like strings trigger a boundary.
+macro_rules! impl_boundary_check_int {
+ ($($t:ty),+ $(,)?) => {
+ $(impl BoundaryCheck for $t {
+ fn check_boundary(raw: &str) -> bool {
+ !is_int_like(raw)
+ }
+ })+
+ };
+}
+
+impl_boundary_check_int! {
+ i8, i16, i32, i64, i128, isize,
+ u8, u16, u32, u64, u128, usize,
+}
+
+// Float boundary: only accept strings that look like floats.
+// Integer-like strings trigger a boundary.
+impl BoundaryCheck for f32 {
+ fn check_boundary(raw: &str) -> bool {
+ !is_float_like(raw) || raw.parse::<f32>().is_err()
+ }
+}
+
+impl BoundaryCheck for f64 {
+ fn check_boundary(raw: &str) -> bool {
+ !is_float_like(raw) || raw.parse::<f64>().is_err()
+ }
+}
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/corebind.rs b/mingling_picker/src/corebind.rs
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/mingling_picker/src/corebind.rs
@@ -0,0 +1 @@
+
diff --git a/mingling_picker/src/infos.rs b/mingling_picker/src/infos.rs
new file mode 100644
index 0000000..d2a0fce
--- /dev/null
+++ b/mingling_picker/src/infos.rs
@@ -0,0 +1,446 @@
+use crate::{Pickable, PickerArg};
+
+/// Represents the result of parsing or looking up a value.
+///
+/// This enum is generic over the type being parsed. It models three possible outcomes:
+/// - [`Unparsed`](PickerArgResult::Unparsed): The value has not yet been parsed (default).
+/// - [`Parsed`](PickerArgResult::Parsed): The value was successfully parsed into `Type`.
+/// - [`NotFound`](PickerArgResult::NotFound): The requested value could not be found.
+#[derive(Default)]
+pub enum PickerArgResult<Type> {
+ /// The value has not yet been parsed (default).
+ #[default]
+ Unparsed,
+
+ /// The value was successfully parsed into `Type`.
+ Parsed(Type),
+
+ /// The requested value could not be found.
+ NotFound,
+}
+
+impl<Type, E> From<Result<Type, E>> for PickerArgResult<Type> {
+ /// Converts a `Result<Type, E>` into a `PickerArgResult<Type>`.
+ ///
+ /// - `Ok(value)` maps to [`Parsed(value)`](PickerArgResult::Parsed).
+ /// - `Err(_)` maps to [`NotFound`](PickerArgResult::NotFound).
+ fn from(result: Result<Type, E>) -> Self {
+ match result {
+ Ok(value) => PickerArgResult::Parsed(value),
+ Err(_) => PickerArgResult::NotFound,
+ }
+ }
+}
+
+impl<Type> From<Option<Type>> for PickerArgResult<Type> {
+ /// Converts an `Option<Type>` into a `PickerArgResult<Type>`.
+ ///
+ /// - `Some(value)` maps to [`Parsed(value)`](PickerArgResult::Parsed).
+ /// - `None` maps to [`NotFound`](PickerArgResult::NotFound).
+ fn from(option: Option<Type>) -> Self {
+ match option {
+ Some(value) => PickerArgResult::Parsed(value),
+ None => PickerArgResult::NotFound,
+ }
+ }
+}
+
+impl<Type> PickerArgResult<Type> {
+ /// Returns `true` if the result is [`Parsed`](PickerArgResult::Parsed).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert!(result.is_parsed());
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert!(!result.is_parsed());
+ /// ```
+ pub fn is_parsed(&self) -> bool {
+ matches!(self, PickerArgResult::Parsed(_))
+ }
+
+ /// Returns `true` if the result is [`Parsed`](PickerArgResult::Parsed) or [`NotFound`](PickerArgResult::NotFound).
+ /// i.e., the value exists (was either found or not yet parsed).
+ /// Typically indicates the value was "found" in some sense.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert!(result.is_found());
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert!(result.is_found());
+ /// ```
+ pub fn is_found(&self) -> bool {
+ matches!(self, PickerArgResult::Parsed(_) | PickerArgResult::NotFound)
+ }
+
+ /// Returns `true` if the result is [`Unparsed`](PickerArgResult::Unparsed) or [`NotFound`](PickerArgResult::NotFound).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Unparsed;
+ /// assert!(result.is_err());
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(10);
+ /// assert!(!result.is_err());
+ /// ```
+ pub fn is_err(&self) -> bool {
+ !matches!(self, PickerArgResult::Parsed(_))
+ }
+
+ /// Returns `Some(&Type)` if [`Parsed`](PickerArgResult::Parsed), otherwise `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.parsed(), Some(&42));
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert_eq!(result.parsed(), None);
+ /// ```
+ pub fn parsed(&self) -> Option<&Type> {
+ if let PickerArgResult::Parsed(value) = self {
+ Some(value)
+ } else {
+ None
+ }
+ }
+
+ /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or panics with a given message.
+ ///
+ /// # Panics
+ /// Panics if the value is not [`Parsed`](PickerArgResult::Parsed), with a message including the provided `msg`.
+ ///
+ /// # Examples
+ ///
+ /// ```should_panic
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// result.expect("expected a parsed value");
+ /// ```
+ pub fn expect(self, msg: &str) -> Type {
+ match self {
+ PickerArgResult::Parsed(value) => value,
+ _ => panic!("{}", msg),
+ }
+ }
+
+ /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or panics.
+ ///
+ /// # Panics
+ /// Panics if the value is not [`Parsed`](PickerArgResult::Parsed).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.unwrap(), 42);
+ /// ```
+ ///
+ /// ```should_panic
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// result.unwrap();
+ /// ```
+ pub fn unwrap(self) -> Type {
+ match self {
+ PickerArgResult::Parsed(value) => value,
+ PickerArgResult::Unparsed => {
+ panic!("called `PickerArgResult::unwrap()` on an `Unparsed` value")
+ }
+ PickerArgResult::NotFound => {
+ panic!("called `PickerArgResult::unwrap()` on a `NotFound` value")
+ }
+ }
+ }
+
+ /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or a provided `default`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.unwrap_or(0), 42);
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert_eq!(result.unwrap_or(0), 0);
+ /// ```
+ pub fn unwrap_or(self, default: Type) -> Type {
+ match self {
+ PickerArgResult::Parsed(value) => value,
+ _ => default,
+ }
+ }
+
+ /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or computes it from a closure.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.unwrap_or_else(|| 0), 42);
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert_eq!(result.unwrap_or_else(|| 0), 0);
+ /// ```
+ pub fn unwrap_or_else<F: FnOnce() -> Type>(self, f: F) -> Type {
+ match self {
+ PickerArgResult::Parsed(value) => value,
+ _ => f(),
+ }
+ }
+
+ /// Returns the contained [`Parsed`](PickerArgResult::Parsed) value or the default value of `Type`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.unwrap_or_default(), 42);
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert_eq!(result.unwrap_or_default(), 0);
+ /// ```
+ pub fn unwrap_or_default(self) -> Type
+ where
+ Type: Default,
+ {
+ match self {
+ PickerArgResult::Parsed(value) => value,
+ _ => Type::default(),
+ }
+ }
+
+ /// Converts `PickerArgResult<Type>` into `Option<Type>`.
+ ///
+ /// Returns `Some(Type)` if [`Parsed`](PickerArgResult::Parsed), otherwise `None`.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.to_option(), Some(42));
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::NotFound;
+ /// assert_eq!(result.to_option(), None);
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Unparsed;
+ /// assert_eq!(result.to_option(), None);
+ /// ```
+ pub fn to_option(self) -> Option<Type> {
+ match self {
+ PickerArgResult::Parsed(value) => Some(value),
+ _ => None,
+ }
+ }
+}
+
+/// Represents metadata about a command-line argument or flag.
+///
+/// This struct stores all relevant information about a tag/argument that can be used
+/// for parsing command-line inputs. It includes the short form (e.g., `-n`), long form
+/// (e.g., `--name`), aliases, and various flags that control parsing behavior.
+pub struct PickerArgInfo<'a> {
+ /// The short form of the tag, e.g. `'n'` for `-n`.
+ pub short: Option<char>,
+ /// The long form of the tag, e.g. `"name"` for `--name`.
+ pub long: Option<&'a str>,
+ /// Alternative names for the tag, e.g. `["-N", "--nickname"]`.
+ pub alias: Option<Vec<&'a str>>,
+ /// Whether this tag is a positional argument (no `-` or `--` prefix).
+ pub positional: bool,
+ /// Whether this tag is optional or required.
+ pub optional: bool,
+ /// Whether this tag can accept multiple values.
+ pub multi: bool,
+ /// Whether this tag participates in parsing after a `--` separator.
+ pub is_flag: bool,
+}
+
+impl<'a, T> From<PickerArg<'a, T>> for PickerArgInfo<'a>
+where
+ T: Pickable<'a>,
+{
+ fn from(value: PickerArg<'a, T>) -> Self {
+ let (long, alias) = match value.full.len() {
+ 0 => (None, None),
+ _ => {
+ let long = Some(value.full[0]);
+ let alias = if value.full.len() > 1 {
+ Some(value.full[1..].to_vec())
+ } else {
+ None
+ };
+ (long, alias)
+ }
+ };
+
+ Self {
+ short: value.short,
+ long,
+ alias,
+ positional: value.positional,
+ optional: false,
+ multi: false,
+ is_flag: false,
+ }
+ }
+}
+
+impl<'a, T: Pickable<'a>> From<&'a PickerArg<'a, T>> for PickerArgInfo<'a> {
+ fn from(value: &'a PickerArg<'a, T>) -> Self {
+ let (long, alias) = match value.full.len() {
+ 0 => (None, None),
+ _ => {
+ let long = Some(value.full[0]);
+ let alias = if value.full.len() > 1 {
+ Some(value.full[1..].to_vec())
+ } else {
+ None
+ };
+ (long, alias)
+ }
+ };
+
+ Self {
+ short: value.short,
+ long,
+ alias,
+ positional: value.positional,
+ optional: false,
+ multi: false,
+ is_flag: false,
+ }
+ }
+}
+
+impl<'a> PickerArgInfo<'a> {
+ /// Create a new `PickerTag` with default values.
+ pub fn new() -> Self {
+ Self {
+ short: None,
+ long: None,
+ alias: None,
+ positional: false,
+ optional: false,
+ multi: false,
+ is_flag: false,
+ }
+ }
+
+ /// Set the short flag (e.g., `'n'` for `-n`).
+ pub fn with_short(mut self, short: char) -> Self {
+ self.short = Some(short);
+ self
+ }
+
+ /// Set the long flag (e.g., `"name"` for `--name`).
+ pub fn with_long(mut self, long: &'a str) -> Self {
+ self.long = Some(long);
+ self
+ }
+
+ /// Set aliases for the tag.
+ pub fn with_alias(mut self, alias: Vec<&'a str>) -> Self {
+ self.alias = Some(alias);
+ self
+ }
+
+ /// Mark the tag as positional.
+ pub fn with_positional(mut self, positional: bool) -> Self {
+ self.positional = positional;
+ self
+ }
+
+ /// Mark the tag as optional.
+ pub fn with_optional(mut self, optional: bool) -> Self {
+ self.optional = optional;
+ self
+ }
+
+ /// Mark the tag as multi-value.
+ pub fn with_multi(mut self, multi: bool) -> Self {
+ self.multi = multi;
+ self
+ }
+
+ /// Mark the tag as a flag that participates in parsing after `--`.
+ pub fn with_is_flag(mut self, is_flag: bool) -> Self {
+ self.is_flag = is_flag;
+ self
+ }
+
+ /// Set the short flag (e.g., `'n'` for `-n`).
+ pub fn set_short(&mut self, short: char) -> &mut Self {
+ self.short = Some(short);
+ self
+ }
+
+ /// Set the long flag (e.g., `"name"` for `--name`).
+ pub fn set_long(&mut self, long: &'a str) -> &mut Self {
+ self.long = Some(long);
+ self
+ }
+
+ /// Set aliases for the tag.
+ pub fn set_alias(&mut self, alias: Vec<&'a str>) -> &mut Self {
+ self.alias = Some(alias);
+ self
+ }
+
+ /// Set whether this tag is positional.
+ pub fn set_positional(&mut self, positional: bool) -> &mut Self {
+ self.positional = positional;
+ self
+ }
+
+ /// Set whether this tag is optional.
+ pub fn set_optional(&mut self, optional: bool) -> &mut Self {
+ self.optional = optional;
+ self
+ }
+
+ /// Set whether this tag accepts multiple values.
+ pub fn set_multi(&mut self, multi: bool) -> &mut Self {
+ self.multi = multi;
+ self
+ }
+
+ /// Set whether this tag participates in parsing after a `--` separator.
+ pub fn set_is_flag(&mut self, is_flag: bool) -> &mut Self {
+ self.is_flag = is_flag;
+ self
+ }
+}
+
+impl<'a> Default for PickerArgInfo<'a> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
diff --git a/mingling_picker/src/lib.rs b/mingling_picker/src/lib.rs
index 3c3251d..afc7aca 100644
--- a/mingling_picker/src/lib.rs
+++ b/mingling_picker/src/lib.rs
@@ -1 +1,43 @@
-pub mod prelude {}
+mod builtin;
+
+mod picker;
+pub use picker::*;
+
+mod pickable;
+pub use pickable::*;
+
+mod arg;
+pub use arg::*;
+
+mod infos;
+pub use infos::*;
+
+pub mod parselib;
+
+pub mod value;
+
+pub mod prelude {
+ pub use crate::IntoPicker;
+}
+
+pub mod macros {
+ pub use mingling_picker_macros::*;
+}
+
+/// Provides the types necessary for implementing the `Pickable` trait
+pub mod pickable_needed {
+ pub use crate::{Pickable, PickerArg, PickerArgAttr, PickerArgResult, TagPhaseContext};
+}
+
+/// Provides the types necessary for implementing the `Matcher` trait
+pub mod matcher_needed {
+ pub use crate::PickerArgInfo;
+ pub use crate::parselib::{MaskedArg, Matcher, ParserStyle};
+}
+
+#[cfg(feature = "mingling_support")]
+mod corebind;
+
+#[allow(unused_imports)]
+#[cfg(feature = "mingling_support")]
+pub use corebind::*;
diff --git a/mingling_picker/src/parselib.rs b/mingling_picker/src/parselib.rs
new file mode 100644
index 0000000..7fbd606
--- /dev/null
+++ b/mingling_picker/src/parselib.rs
@@ -0,0 +1,144 @@
+mod flag_matcher;
+pub use flag_matcher::*;
+
+mod arg_matcher;
+pub use arg_matcher::*;
+
+mod multi_arg_matcher;
+pub use multi_arg_matcher::*;
+
+mod pos_matcher;
+pub use pos_matcher::*;
+
+mod single_matcher;
+pub use single_matcher::*;
+
+mod style;
+pub use style::*;
+
+mod utils;
+pub use utils::*;
+
+use crate::{PickerArgInfo, PickerArgs};
+
+/// Represents a single argument with its original raw string and index.
+///
+/// This is used during pattern matching to provide context about
+/// which argument is being processed.
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct MaskedArg<'a> {
+ /// The raw string value of the argument.
+ pub raw: &'a str,
+ /// The original index of the argument in the full argument list.
+ pub raw_idx: usize,
+}
+
+/// Trait for defining matching logic against masked arguments.
+///
+/// Implementors can define custom strategies for matching one or all
+/// arguments that pass through a mask filter.
+pub trait Matcher {
+ /// Called when only one match is needed.
+ ///
+ /// Returns the index of the first matched argument, or `None` if no match.
+ fn on_match_one(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Option<usize>;
+
+ /// Called when all matches are needed.
+ ///
+ /// Returns a vector of indices of all matched arguments.
+ fn on_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Vec<usize>;
+
+ /// Convenience method that builds masked arguments from `PickerArgs` and a mask,
+ /// then calls `on_match_one`.
+ fn match_one<'a>(ctx: MatcherContext<'a>) -> Option<usize> {
+ let masked_args = build_masked_args(ctx.args, ctx.mask);
+ Self::on_match_one(masked_args.as_slice(), ctx.style, ctx.arg_info)
+ }
+
+ /// Convenience method that builds masked arguments from `PickerArgs` and a mask,
+ /// then calls `on_match_all`.
+ fn match_all<'a>(ctx: MatcherContext<'a>) -> Vec<usize> {
+ let masked_args = build_masked_args(ctx.args, ctx.mask);
+ Self::on_match_all(masked_args.as_slice(), ctx.style, ctx.arg_info)
+ }
+}
+
+/// Context for matcher operations
+///
+/// This struct bundles together the key pieces of data needed during matching:
+/// - `args`: The full set of parsed arguments.
+/// - `mask`: A byte mask indicating which arguments are currently active (non-zero = active).
+/// - `style`: The parsing style configuration.
+pub struct MatcherContext<'a> {
+ /// The full set of parsed arguments.
+ pub args: &'a PickerArgs<'a>,
+
+ /// A byte mask where non-zero values indicate the argument at that position is active/should be matched.
+ pub mask: &'a [u8],
+
+ /// The parsing style configuration.
+ pub style: &'a ParserStyle<'a>,
+
+ /// Metadata about the command-line argument/flag being processed.
+ ///
+ /// Contains information such as short form (`-n`), long form (`--name`),
+ /// aliases, and parsing flags (positional, optional, multi, is_flag).
+ /// Used by matchers to make decisions based on argument characteristics.
+ pub arg_info: &'a PickerArgInfo<'a>,
+}
+
+impl<'a> From<&'a crate::TagPhaseContext<'a>> for MatcherContext<'a> {
+ fn from(ctx: &'a crate::TagPhaseContext<'a>) -> Self {
+ MatcherContext {
+ args: ctx.args,
+ mask: ctx.mask,
+ style: ParserStyle::global_style(),
+ arg_info: ctx.arg_info,
+ }
+ }
+}
+
+impl<'a> From<crate::TagPhaseContext<'a>> for MatcherContext<'a> {
+ fn from(ctx: crate::TagPhaseContext<'a>) -> Self {
+ MatcherContext {
+ args: ctx.args,
+ mask: ctx.mask,
+ style: ParserStyle::global_style(),
+ arg_info: ctx.arg_info,
+ }
+ }
+}
+
+#[inline(always)]
+fn is_masked(mask: &[u8], idx: usize) -> bool {
+ idx < mask.len() && mask[idx] != 0
+}
+
+#[inline(always)]
+fn build_masked_args<'a>(args: &'a PickerArgs, mask: &'a [u8]) -> Vec<MaskedArg<'a>> {
+ let mut cidx = 0;
+ args.iter()
+ .filter_map(|r| {
+ let idx = cidx;
+ cidx += 1;
+ // Include args where mask is 0 (available/not yet claimed).
+ // mask[i] = 0 means available; mask[i] != 0 means already claimed.
+ if !is_masked(mask, idx) {
+ Some(MaskedArg {
+ raw: r,
+ raw_idx: idx,
+ })
+ } else {
+ None
+ }
+ })
+ .collect()
+}
diff --git a/mingling_picker/src/parselib/arg_matcher.rs b/mingling_picker/src/parselib/arg_matcher.rs
new file mode 100644
index 0000000..38bb9cc
--- /dev/null
+++ b/mingling_picker/src/parselib/arg_matcher.rs
@@ -0,0 +1,142 @@
+use crate::{
+ matcher_needed::*,
+ parselib::{build_possible_flags, seek_end_of_options},
+};
+
+/// `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`, 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, 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) && 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()] == sep as u8)
+ }
+ }
+
+ /// 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_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))
+ }
+}
+
+impl Matcher for ArgMatcher {
+ fn on_match_one(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Option<usize> {
+ if arg_info.positional {
+ 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);
+ let sep = style.value_separator;
+
+ for arg in args {
+ 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, sep));
+ if matched {
+ return Some(arg.raw_idx);
+ }
+ }
+
+ None
+ }
+
+ fn on_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Vec<usize> {
+ if arg_info.positional {
+ 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 sep = style.value_separator;
+
+ let mut result = Vec::new();
+ let mut i = 0;
+ while i < args.len() {
+ 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, sep));
+
+ if matched {
+ let flag_str = possible_flags
+ .iter()
+ .find(|f| Self::matches(args[i].raw, f, style.case_sensitive, sep))
+ .expect("already matched");
+
+ result.push(args[i].raw_idx);
+
+ 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;
+ }
+ i += 1;
+ continue;
+ }
+ i += 1;
+ continue;
+ }
+ i += 1;
+ }
+
+ result
+ }
+}
diff --git a/mingling_picker/src/parselib/flag_matcher.rs b/mingling_picker/src/parselib/flag_matcher.rs
new file mode 100644
index 0000000..e93d35a
--- /dev/null
+++ b/mingling_picker/src/parselib/flag_matcher.rs
@@ -0,0 +1,82 @@
+use crate::{
+ matcher_needed::*,
+ parselib::{build_possible_flags, get_seeked_first, multi_seek_eq, seek_end_of_options},
+};
+
+/// `FlagMatcher` is used to match flags in command-line arguments.
+///
+/// Flags typically start with `-` or `--` (e.g., `-h`, `--help`),
+/// and do not carry additional values. This matcher is responsible for finding
+/// these flags in the argument list, taking into account that flags after `--`
+/// (end-of-options marker) should not be matched.
+pub struct FlagMatcher;
+
+impl Matcher for FlagMatcher {
+ fn on_match_one(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Option<usize> {
+ let possible_flags = build_possible_flags(style, arg_info);
+ let flag_refs: Vec<&str> = possible_flags.iter().map(|s| s.as_str()).collect();
+ let end_of_options = seek_end_of_options(args, style);
+
+ let result = get_seeked_first(multi_seek_eq(args, &flag_refs, style.case_sensitive));
+
+ match (end_of_options, result) {
+ (Some(end), Some(current)) if current > end => None,
+ _ => result,
+ }
+ }
+
+ fn on_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Vec<usize> {
+ let possible_flags = build_possible_flags(style, arg_info);
+ single_pass_match_all(args, style, &possible_flags)
+ }
+}
+
+/// Single-pass match: finds the `--` marker and matching flags in one iteration.
+fn single_pass_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ possible_flags: &[String],
+) -> Vec<usize> {
+ let flag_refs: Vec<&str> = possible_flags.iter().map(|s| s.as_str()).collect();
+ let eoo = style.end_of_options;
+ let case_sensitive = style.case_sensitive;
+
+ let mut end_pos: Option<usize> = None;
+ let mut matches: Vec<usize> = Vec::new();
+
+ for arg in args {
+ if end_pos.is_none() {
+ let is_eoo = if case_sensitive {
+ arg.raw == eoo
+ } else {
+ arg.raw.eq_ignore_ascii_case(eoo)
+ };
+ if is_eoo {
+ end_pos = Some(arg.raw_idx);
+ continue;
+ }
+ }
+
+ // Only match flags before the end-of-options marker.
+ if end_pos.is_none() {
+ let matched = if case_sensitive {
+ flag_refs.contains(&arg.raw)
+ } else {
+ flag_refs.iter().any(|s| arg.raw.eq_ignore_ascii_case(s))
+ };
+ if matched {
+ matches.push(arg.raw_idx);
+ }
+ }
+ }
+
+ matches
+}
diff --git a/mingling_picker/src/parselib/multi_arg_matcher.rs b/mingling_picker/src/parselib/multi_arg_matcher.rs
new file mode 100644
index 0000000..e8bdb71
--- /dev/null
+++ b/mingling_picker/src/parselib/multi_arg_matcher.rs
@@ -0,0 +1,141 @@
+use crate::{
+ matcher_needed::*,
+ parselib::{build_possible_flags, seek_end_of_options},
+};
+
+/// `MultiArgMatcher` matches a named flag and **all** consecutive arguments
+/// that follow it, stopping at the next flag, the `--` marker, or the end
+/// of the argument list.
+///
+/// This is the tag implementation for `Multi` and `GreedyMulti` types
+/// such as `Vec<String>` (`--files a.txt b.txt`).
+///
+/// # Behavior
+///
+/// | Input | `on_match_all` |
+/// |-------|----------------|
+/// | `--val a b --val d e` | `[0, 1, 2, 5, 6]` (two groups) |
+/// | `--val=1 2` | `[0, 1]` (eq mode + one extra value) |
+///
+/// Args after `--` are ignored.
+pub struct MultiArgMatcher;
+
+impl Matcher for MultiArgMatcher {
+ fn on_match_one(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Option<usize> {
+ if arg_info.positional {
+ 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);
+ let sep = style.value_separator;
+
+ for arg in args {
+ if end.is_some_and(|e| arg.raw_idx >= e) {
+ break;
+ }
+ let matched = possible_flags
+ .iter()
+ .any(|f| Self::flag_match(arg.raw, f, style.case_sensitive, sep));
+ if matched {
+ return Some(arg.raw_idx);
+ }
+ }
+ None
+ }
+
+ fn on_match_all(
+ args: &[MaskedArg],
+ style: &ParserStyle,
+ arg_info: &PickerArgInfo,
+ ) -> Vec<usize> {
+ if arg_info.positional {
+ 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 sep = style.value_separator;
+ let is_flag = |raw: &str| {
+ raw.starts_with(style.long_prefix) || raw.starts_with(style.short_prefix)
+ };
+ let is_our_flag = |raw: &str| {
+ possible_flags
+ .iter()
+ .any(|f| Self::flag_match(raw, f, style.case_sensitive, sep))
+ };
+
+ let mut result = Vec::new();
+ let mut i = 0;
+ while i < args.len() {
+ if end.is_some_and(|e| args[i].raw_idx >= e) {
+ break;
+ }
+
+ let matched = is_our_flag(args[i].raw);
+
+ if matched {
+ result.push(args[i].raw_idx);
+
+ if Self::is_eq_match(args[i].raw, &possible_flags, style.case_sensitive, sep) {
+ i += 1;
+ while i < args.len()
+ && end.is_none_or(|e| args[i].raw_idx < e)
+ && !is_flag(args[i].raw)
+ {
+ result.push(args[i].raw_idx);
+ i += 1;
+ }
+ continue;
+ }
+
+ i += 1;
+ while i < args.len()
+ && end.is_none_or(|e| args[i].raw_idx < e)
+ && !is_flag(args[i].raw)
+ {
+ result.push(args[i].raw_idx);
+ i += 1;
+ }
+ continue;
+ }
+ i += 1;
+ }
+
+ result
+ }
+}
+
+impl MultiArgMatcher {
+ #[inline(always)]
+ fn flag_match(raw: &str, flag_str: &str, case_sensitive: bool, sep: char) -> bool {
+ let eq = |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) && eq(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()] == sep as u8)
+ }
+ }
+
+ #[inline(always)]
+ fn is_eq_match(raw: &str, flags: &[String], case_sensitive: bool, sep: char) -> bool {
+ flags.iter().any(|f| {
+ Self::flag_match(raw, f, case_sensitive, sep)
+ && raw.len() > f.len()
+ && raw.as_bytes().get(f.len()) == Some(&(sep as u8))
+ })
+ }
+}
diff --git a/mingling_picker/src/parselib/pos_matcher.rs b/mingling_picker/src/parselib/pos_matcher.rs
new file mode 100644
index 0000000..279e01e
--- /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`](crate::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..25c4741
--- /dev/null
+++ b/mingling_picker/src/parselib/single_matcher.rs
@@ -0,0 +1,59 @@
+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`](crate::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, only complete pairs (flag + value) are kept.
+ /// Flag occurrences without a following value or inline separator
+ /// are dropped so they remain available for other matchers.
+ #[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());
+ let sep = ParserStyle::global_style().value_separator;
+
+ // Walk pairs: [flag, value, flag, value, ...]
+ // Drop any flag that has no following value and no inline separator.
+ let mut i = 0;
+ let mut result = Vec::with_capacity(positions.len());
+ while i < positions.len() {
+ let flag_idx = positions[i];
+ if let Some(raw) = args.get(flag_idx)
+ && raw.contains(sep)
+ {
+ // Eq mode: value is inline, keep just the flag.
+ result.push(flag_idx);
+ i += 1;
+ continue;
+ }
+ if i + 1 < positions.len() {
+ // Pair: flag + value.
+ result.push(flag_idx);
+ result.push(positions[i + 1]);
+ i += 2;
+ } else {
+ // Flag without value — drop it.
+ i += 1;
+ }
+ }
+ result
+ }
+ }
+}
diff --git a/mingling_picker/src/parselib/style.rs b/mingling_picker/src/parselib/style.rs
new file mode 100644
index 0000000..4ea161f
--- /dev/null
+++ b/mingling_picker/src/parselib/style.rs
@@ -0,0 +1,225 @@
+use std::sync::OnceLock;
+use std::sync::atomic::{AtomicBool, Ordering};
+
+use crate::parselib::ParserStyleNamingCase::{Kebab, Pascal};
+
+/// Defines the style of command-line argument parsing (prefixes, separators, etc.).
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub struct ParserStyle<'a> {
+ /// End-of-options marker (e.g., `--`)
+ pub end_of_options: &'a str,
+
+ /// Prefix for long options (e.g., `--` or `/`)
+ pub long_prefix: &'a str,
+
+ /// Prefix for short options (e.g., `-` or `/`)
+ pub short_prefix: &'a str,
+
+ /// Prefix for combined short flags (e.g., `-abc`)
+ pub combine_prefix: &'a str,
+
+ /// Separator between name and value (e.g., `=` or `:`)
+ pub value_separator: char,
+
+ /// Whether option names are case-sensitive
+ pub case_sensitive: bool,
+
+ /// Whether combining short flags is allowed (e.g., `-abc` for `-a -b -c`)
+ pub allow_combine: bool,
+
+ /// Naming case
+ pub naming_case: ParserStyleNamingCase,
+}
+
+impl<'a> ParserStyle<'a> {
+ /// Formats a flag (short or long) into a full command-line option string.
+ ///
+ /// This method takes any type that can be converted into a `FlagStr` and produces
+ /// a complete option string by prepending the appropriate prefix.
+ ///
+ /// # Examples
+ ///
+ /// ```ignore
+ /// use mingling_picker::parselib::{ParserStyle, FlagStr, UNIX_STYLE};
+ /// let style = &UNIX_STYLE;
+ ///
+ /// assert_eq!(style.flag_string('v'), "-v");
+ /// assert_eq!(style.flag_string("verbose"), "--verbose");
+ /// ```
+ ///
+ /// # Parameters
+ ///
+ /// * `flag` - A value that can be converted to `FlagStr`, either a `char` for short flags
+ /// or a `&str` for long flags.
+ ///
+ /// # Returns
+ ///
+ /// A `String` with the prefix and the flag name combined.
+ #[must_use]
+ #[inline(always)]
+ pub fn flag_string<F>(&self, flag: F) -> String
+ where
+ F: Into<FlagStr<'a>>,
+ {
+ match flag.into() {
+ FlagStr::Short(short) => format!("{}{}", self.short_prefix, short),
+ FlagStr::Long(long) => format!("{}{}", self.long_prefix, long),
+ }
+ }
+}
+
+/// Represents a flag name for command-line argument parsing.
+///
+/// This enum can hold either a short flag (a single character, e.g., `'v'` for `-v`)
+/// or a long flag (a string, e.g., `"verbose"` for `--verbose`).
+///
+/// # Examples
+///
+/// ```
+/// use mingling_picker::parselib::FlagStr;
+///
+/// let short: FlagStr = 'v'.into();
+/// let long: FlagStr = "verbose".into();
+/// ```
+pub enum FlagStr<'a> {
+ /// A short flag represented by a single character.
+ Short(char),
+ /// A long flag represented by a string slice.
+ Long(&'a str),
+}
+
+impl<'a> From<char> for FlagStr<'a> {
+ /// Converts a single character into a `FlagStr::Short`.
+ fn from(c: char) -> Self {
+ FlagStr::Short(c)
+ }
+}
+
+impl<'a> From<&'a str> for FlagStr<'a> {
+ /// Converts a string slice into a `FlagStr::Long`.
+ fn from(s: &'a str) -> Self {
+ FlagStr::Long(s)
+ }
+}
+
+impl<'a> From<&'a String> for FlagStr<'a> {
+ /// Converts a reference to a `String` into a `FlagStr::Long`.
+ fn from(s: &'a String) -> Self {
+ FlagStr::Long(s.as_str())
+ }
+}
+
+#[repr(u8)]
+#[derive(Default, Clone, Copy, PartialEq, Eq)]
+pub enum ParserStyleNamingCase {
+ /// snake_case format (e.g., `brew_coffee`)
+ #[default]
+ Snake,
+ /// camelCase format (e.g., `brewCoffee`)
+ Camel,
+ /// PascalCase format (e.g., `BrewCoffee`)
+ Pascal,
+ /// kebab-case format (e.g., `brew-coffee`)
+ Kebab,
+ /// dot.case format (e.g., `brew.coffee`)
+ Dot,
+ /// Title Case format (e.g., `Brew Coffee`)
+ Title,
+ /// lower case format (e.g., `brew coffee`)
+ Lower,
+ /// UPPER CASE format (e.g., `BREW COFFEE`)
+ Upper,
+}
+
+impl ParserStyleNamingCase {
+ /// Converts the input string `s` to the naming case represented by this variant.
+ ///
+ /// This method takes any type `S` that can be converted into a `String` and
+ /// produced from a `String`, applies the corresponding case transformation,
+ /// and returns the result.
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::parselib::ParserStyleNamingCase;
+ ///
+ /// let camel = ParserStyleNamingCase::Camel;
+ /// assert_eq!(camel.convert("brew_coffee".to_string()), "brewCoffee");
+ ///
+ /// let kebab = ParserStyleNamingCase::Kebab;
+ /// assert_eq!(kebab.convert("BrewCoffee".to_string()), "brew-coffee");
+ /// ```
+ pub fn convert<S>(&self, s: S) -> S
+ where
+ S: Into<String> + From<String>,
+ {
+ match self {
+ ParserStyleNamingCase::Camel => just_fmt::camel_case!(s.into()).into(),
+ ParserStyleNamingCase::Pascal => just_fmt::pascal_case!(s.into()).into(),
+ ParserStyleNamingCase::Kebab => just_fmt::kebab_case!(s.into()).into(),
+ ParserStyleNamingCase::Snake => just_fmt::snake_case!(s.into()).into(),
+ ParserStyleNamingCase::Dot => just_fmt::dot_case!(s.into()).into(),
+ ParserStyleNamingCase::Title => just_fmt::title_case!(s.into()).into(),
+ ParserStyleNamingCase::Lower => just_fmt::lower_case!(s.into()).into(),
+ ParserStyleNamingCase::Upper => just_fmt::upper_case!(s.into()).into(),
+ }
+ }
+}
+
+/// Unix-like style (e.g., `--verbose`, `-v`, `--name=value`)
+pub const UNIX_STYLE: ParserStyle = ParserStyle {
+ end_of_options: "--",
+ long_prefix: "--",
+ short_prefix: "-",
+ combine_prefix: "-",
+ value_separator: '=',
+ case_sensitive: true,
+ allow_combine: true,
+ naming_case: Kebab,
+};
+
+/// PowerShell style (e.g., `-Verbose`, `-Name:value`)
+pub const POWERSHELL_STYLE: ParserStyle = ParserStyle {
+ end_of_options: "--",
+ long_prefix: "-",
+ short_prefix: "-",
+ combine_prefix: "-",
+ value_separator: ':',
+ case_sensitive: false,
+ allow_combine: false,
+ naming_case: Pascal,
+};
+
+/// Windows-style command-line (e.g., `/Verbose`, `/Name:value`)
+pub const WINDOWS_STYLE: ParserStyle = ParserStyle {
+ end_of_options: "--",
+ long_prefix: "/",
+ short_prefix: "/",
+ combine_prefix: "/",
+ value_separator: ':',
+ case_sensitive: false,
+ allow_combine: false,
+ naming_case: Pascal,
+};
+
+static GLOBAL_STYLE: OnceLock<ParserStyle<'static>> = OnceLock::new();
+static GLOBAL_STYLE_SET: AtomicBool = AtomicBool::new(false);
+
+impl<'a> ParserStyle<'a> {
+ /// Sets the global parser style.
+ ///
+ /// This function can only be called once. Subsequent calls will have no effect.
+ /// The style is stored as a static reference; the provided style must be a static
+ /// constant (e.g., `&'static ParserStyle`). Use the built-in constants like
+ /// `UNIX_STYLE`, `POWERSHELL_STYLE`, or `WINDOWS_STYLE`.
+ pub fn set_global_style(style: &'static ParserStyle<'static>) {
+ if !GLOBAL_STYLE_SET.load(Ordering::Acquire) && GLOBAL_STYLE.set(*style).is_ok() {
+ GLOBAL_STYLE_SET.store(true, Ordering::Release);
+ }
+ }
+
+ /// Returns the global parser style, falling back to `UNIX_STYLE` if not set.
+ pub fn global_style() -> &'static ParserStyle<'static> {
+ GLOBAL_STYLE.get().unwrap_or(&UNIX_STYLE)
+ }
+}
diff --git a/mingling_picker/src/parselib/utils.rs b/mingling_picker/src/parselib/utils.rs
new file mode 100644
index 0000000..726346c
--- /dev/null
+++ b/mingling_picker/src/parselib/utils.rs
@@ -0,0 +1,271 @@
+use crate::{
+ PickerArgInfo,
+ parselib::{MaskedArg, ParserStyle},
+};
+
+#[inline(always)]
+pub fn build_possible_flags(style: &ParserStyle, arg_info: &PickerArgInfo) -> Vec<String> {
+ let mut possible_flags = vec![];
+
+ if let Some(short) = arg_info.short {
+ possible_flags.push(style.flag_string(short));
+ }
+
+ if let Some(long) = arg_info.long {
+ let converted = style.naming_case.convert(long.to_string());
+ possible_flags.push(style.flag_string(&converted));
+ }
+
+ if let Some(aliases) = &arg_info.alias {
+ for alias in aliases {
+ let converted = style.naming_case.convert(alias.to_string());
+ possible_flags.push(style.flag_string(&converted));
+ }
+ }
+
+ possible_flags
+}
+
+/// Extract a single value from the raw strings tagged by [`SingleMatcher`](crate::parselib::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 (`--`)
+/// in the given argument list, respecting the parser's style settings
+/// (e.g., case sensitivity). The end-of-options marker indicates that all
+/// subsequent arguments should be treated as positional arguments, not flags.
+#[must_use]
+pub 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)
+}
+
+/// Seeks arguments in `args` that are exactly equal to the given `string`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn seek_eq(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ arg.raw == string
+ } else {
+ arg.raw.eq_ignore_ascii_case(string)
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that contain the given `string` as a substring.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn seek_contains(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ arg.raw.contains(string)
+ } else {
+ arg.raw.to_lowercase().contains(&string.to_lowercase())
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that start with the given `string`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn seek_start_with(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ arg.raw.starts_with(string)
+ } else {
+ arg.raw.to_lowercase().starts_with(&string.to_lowercase())
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that end with the given `string`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn seek_end_with(args: &[MaskedArg], string: &str, case_sensitive: bool) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ arg.raw.ends_with(string)
+ } else {
+ arg.raw.to_lowercase().ends_with(&string.to_lowercase())
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that are exactly equal to any of the given `strings`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn multi_seek_eq(args: &[MaskedArg], strings: &[&str], case_sensitive: bool) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ strings.contains(&arg.raw)
+ } else {
+ strings.iter().any(|s| arg.raw.eq_ignore_ascii_case(s))
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that contain any of the given `strings` as a substring.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn multi_seek_contains(
+ args: &[MaskedArg],
+ strings: &[&str],
+ case_sensitive: bool,
+) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ strings.iter().any(|s| arg.raw.contains(s))
+ } else {
+ let lower_raw = arg.raw.to_lowercase();
+ strings
+ .iter()
+ .any(|s| lower_raw.contains(&s.to_lowercase()))
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that start with any of the given `strings`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn multi_seek_start_with(
+ args: &[MaskedArg],
+ strings: &[&str],
+ case_sensitive: bool,
+) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ strings.iter().any(|s| arg.raw.starts_with(s))
+ } else {
+ let lower_raw = arg.raw.to_lowercase();
+ strings
+ .iter()
+ .any(|s| lower_raw.starts_with(&s.to_lowercase()))
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Seeks arguments in `args` that end with any of the given `strings`.
+///
+/// Returns the indices of matching arguments.
+#[must_use]
+#[inline(always)]
+pub fn multi_seek_end_with(
+ args: &[MaskedArg],
+ strings: &[&str],
+ case_sensitive: bool,
+) -> Vec<usize> {
+ args.iter()
+ .filter(|arg| {
+ if case_sensitive {
+ strings.iter().any(|s| arg.raw.ends_with(s))
+ } else {
+ let lower_raw = arg.raw.to_lowercase();
+ strings
+ .iter()
+ .any(|s| lower_raw.ends_with(&s.to_lowercase()))
+ }
+ })
+ .map(|arg| arg.raw_idx)
+ .collect()
+}
+
+/// Converts a `&Vec<String>` into a `Vec<&str>` by borrowing each string's slice.
+///
+/// This is useful for converting owned `String` vectors into borrowed `&str` slices
+/// for functions that take `&[&str]` or similar parameters.
+#[must_use]
+#[inline(always)]
+#[doc(hidden)]
+pub fn vec_string_to_vec_str(input: &[String]) -> Vec<&str> {
+ input.iter().map(|s| s.as_str()).collect()
+}
+
+/// Converts a `&Vec<String>` into a `Vec<&str>` by borrowing each string's slice.
+///
+/// This is useful for converting owned `String` vectors into borrowed `&str` slices
+/// for functions that take `&[&str]` or similar parameters.
+#[macro_export]
+#[doc(hidden)]
+macro_rules! vec_string_slice {
+ ($v:expr) => {
+ $v.iter()
+ .map(|s| s.as_str())
+ .collect::<Vec<&str>>()
+ .as_slice()
+ };
+}
+
+/// Gets the first element from a vector of seek results, if any.
+///
+/// Returns `Some(index)` if the vector is non-empty, otherwise `None`.
+#[must_use]
+#[inline(always)]
+pub fn get_seeked_first(seeked: Vec<usize>) -> Option<usize> {
+ seeked.into_iter().next()
+}
diff --git a/mingling_picker/src/pickable.rs b/mingling_picker/src/pickable.rs
new file mode 100644
index 0000000..758ae9a
--- /dev/null
+++ b/mingling_picker/src/pickable.rs
@@ -0,0 +1,91 @@
+use crate::{PickerArg, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs};
+
+mod single_pickable;
+pub use single_pickable::*;
+
+mod multi_pickable;
+pub use multi_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
+/// parsing process into two phases:
+///
+/// 1. **Tag phase ([`Pickable::tag`])**: Determines which argument positions the `Pickable` needs to handle.
+/// 2. **Pick phase ([`Pickable::pick`])**: Converts the raw strings at the tagged positions into the actual type.
+///
+/// Types implementing this trait must also implement [`Default`], so that a default value
+/// can be used as a fallback when parsing fails.
+///
+/// # Type Parameters
+///
+/// * `'a` - Lifetime parameter, used to associate references in [`PickerArg`].
+pub trait Pickable<'a>
+where
+ Self: Sized,
+{
+ /// Returns the parse-order attribute of this flag.
+ ///
+ /// This attribute is used to inform the parser about the parse order
+ /// between different `Pickable` types.
+ /// See [`PickerArgAttr`] for specific ordering definitions.
+ ///
+ /// # Parameters
+ ///
+ /// * `flag` - The current flag instance, which contains a reference to `Self`.
+ ///
+ /// # Returns
+ ///
+ /// Returns a [`PickerArgAttr`] describing the parse-order attribute of this flag.
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr;
+
+ /// Tag phase: Determines which argument positions the `Pickable` needs to handle.
+ ///
+ /// This function receives a [`TagPhaseContext`] containing argument context information.
+ /// During this phase, the parser invokes each `Pickable` and collects the position indices
+ /// they return, in order to determine which arguments to parse later.
+ ///
+ /// # Parameters
+ ///
+ /// * `ctx` - The tag phase context, containing argument information, all parameters of the
+ /// current Picker, and an availability mask.
+ ///
+ /// # Returns
+ ///
+ /// Returns a `Vec<usize>` representing the indices of the arguments in the argument list
+ /// that this `Pickable` needs to handle.
+ fn tag(ctx: TagPhaseContext) -> Vec<usize>;
+
+ /// Pick phase: Converts the raw string arguments tagged during the `tag` phase into
+ /// the actual expected type.
+ ///
+ /// This function receives a slice of the raw strings that were tagged in the `tag` step
+ /// and converts them into an instance of `Self`.
+ ///
+ /// # Parameters
+ ///
+ /// * `raw_strs` - A slice of strings containing the raw argument values to parse.
+ ///
+ /// # Returns
+ ///
+ /// Returns [`PickerArgResult<Self>`], i.e., the `Self` instance on success, or an appropriate
+ /// error message on failure.
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self>;
+}
+
+/// Tag phase context, providing the necessary argument and state information for
+/// [`Pickable::tag`].
+pub struct TagPhaseContext<'a> {
+ /// Argument information describing the structure and metadata of the argument
+ /// to be parsed.
+ pub arg_info: &'a PickerArgInfo<'a>,
+
+ /// A read-only list of all arguments in the current [`Picker`](crate::Picker).
+ pub args: &'a PickerArgs<'a>,
+
+ /// Mask indicating which argument positions have already been claimed.
+ ///
+ /// For example, if the mask is `[0, 0, 1, 0]`, then the argument at index `2`
+ /// has already been tagged by another `Pickable`.
+ pub mask: &'a [u8],
+}
diff --git a/mingling_picker/src/pickable/multi_pickable.rs b/mingling_picker/src/pickable/multi_pickable.rs
new file mode 100644
index 0000000..0ab0508
--- /dev/null
+++ b/mingling_picker/src/pickable/multi_pickable.rs
@@ -0,0 +1,77 @@
+use crate::{
+ matcher_needed::Matcher,
+ parselib::{MultiArgMatcher, ParserStyle},
+ Pickable, PickerArg, PickerArgAttr, PickerArgResult,
+ SinglePickable, TagPhaseContext,
+};
+
+/// Boundary check for multi-value positional parameters.
+pub trait BoundaryCheck {
+ fn check_boundary(raw: &str) -> bool;
+}
+
+/// Trait for multi-value parameters.
+pub trait MultiPickableWithBoundary: Sized {
+ type Checker: BoundaryCheck;
+ fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self>;
+}
+
+/// Marker: unit type that always accepts — no boundary.
+pub struct NoBoundary;
+
+impl BoundaryCheck for NoBoundary {
+ #[inline(always)]
+ fn check_boundary(_raw: &str) -> bool {
+ false
+ }
+}
+
+/// `Vec<T>` is greedy — it takes everything with `NoBoundary`.
+impl<T: SinglePickable> MultiPickableWithBoundary for Vec<T> {
+ type Checker = NoBoundary;
+
+ fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self> {
+ let mut result = Vec::with_capacity(raw.len());
+ for s in &raw {
+ match T::pick_single(Some(s)) {
+ PickerArgResult::Parsed(v) => result.push(v),
+ PickerArgResult::NotFound => return PickerArgResult::NotFound,
+ PickerArgResult::Unparsed => {}
+ }
+ }
+ PickerArgResult::Parsed(result)
+ }
+}
+
+/// If the first raw string looks like a named flag (starts with the
+/// style's long or short prefix), strip it — it's the flag, not a value.
+fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] {
+ if let Some(first) = raw_strs.first() {
+ let style = ParserStyle::global_style();
+ if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) {
+ return &raw_strs[1..];
+ }
+ }
+ raw_strs
+}
+
+// Pickable impl for Vec<T>
+
+impl<'a, T> Pickable<'a> for Vec<T>
+where
+ T: SinglePickable,
+{
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_multi(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ MultiArgMatcher::match_all(ctx.into())
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ let strs = strip_flag(raw_strs);
+ let owned: Vec<String> = strs.iter().map(|&s| s.to_string()).collect();
+ <Vec<T> as MultiPickableWithBoundary>::pick_multi(owned)
+ }
+}
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
new file mode 100644
index 0000000..4efb532
--- /dev/null
+++ b/mingling_picker/src/picker.rs
@@ -0,0 +1,360 @@
+use std::{marker::PhantomData, ops::Index};
+
+mod parse;
+
+mod patterns;
+pub use patterns::*;
+
+mod result;
+pub use result::*;
+
+use crate::{Pickable, PickerArg, PickerArgResult};
+
+/// Picker, used to record all states of a parameter parsing
+///
+/// Includes the following:
+///
+/// - Basic arguments
+/// - Parsing states
+/// - Parsing results
+pub struct Picker<'a, Route = ()> {
+ route_phantom: PhantomData<Route>,
+
+ /// Internal arguments of Picker
+ args: PickerArgs<'a>,
+}
+
+impl<'a> Picker<'a> {
+ /// Creates a new `Picker` from the command-line arguments (excluding the program name).
+ ///
+ /// This is equivalent to calling `std::env::args().skip(1)`, which
+ /// collects all arguments passed to the program except the first one
+ /// (the executable path).
+ pub fn from_args() -> Picker<'a, ()> {
+ Self::from_args_skip(1)
+ }
+
+ /// Creates a new `Picker` from the command-line arguments, skipping the
+ /// first `skip` entries.
+ ///
+ /// This method is useful when you want more control over which arguments
+ /// are included. For example, pass `skip = 2` to skip both the program
+ /// name and the first argument.
+ pub fn from_args_skip(skip: usize) -> Picker<'a, ()> {
+ let args = std::env::args().skip(skip).collect::<Vec<String>>();
+ Picker {
+ route_phantom: PhantomData,
+ 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
+///
+/// - `Slice` - borrowed slice of string slices
+/// - `Vec` - owned vector of borrowed string slices
+/// - `Owned` - owned vector of owned strings
+pub enum PickerArgs<'a> {
+ /// Borrowed slice of string slices
+ Slice(&'a [&'a str]),
+ /// Owned vector of borrowed string slices
+ Vec(Vec<&'a str>),
+ /// Owned vector of owned strings
+ Owned(Vec<String>),
+}
+
+impl<'a> Default for PickerArgs<'a> {
+ fn default() -> Self {
+ Self::Vec(vec![])
+ }
+}
+
+impl<'a> PickerArgs<'a> {
+ /// Returns the number of arguments.
+ pub fn len(&self) -> usize {
+ match self {
+ PickerArgs::Slice(items) => items.len(),
+ PickerArgs::Vec(items) => items.len(),
+ PickerArgs::Owned(items) => items.len(),
+ }
+ }
+
+ /// Returns `true` if there are no arguments.
+ pub fn is_empty(&self) -> bool {
+ self.len() == 0
+ }
+
+ /// Returns an iterator over the arguments, yielding `&str` values.
+ pub fn iter(&'a self) -> PickerIter<'a> {
+ match self {
+ PickerArgs::Slice(items) => PickerIter::Slice(items.iter()),
+ PickerArgs::Vec(items) => PickerIter::Vec(items.iter()),
+ PickerArgs::Owned(items) => PickerIter::Owned(items.iter()),
+ }
+ }
+
+ /// Returns a reference to the argument at `index`, if it exists.
+ pub fn get(&self, index: usize) -> Option<&str> {
+ match self {
+ PickerArgs::Slice(items) => items.get(index).copied(),
+ PickerArgs::Vec(items) => items.get(index).copied(),
+ PickerArgs::Owned(items) => items.get(index).map(|s| s.as_str()),
+ }
+ }
+}
+
+impl<'a> Index<usize> for PickerArgs<'a> {
+ type Output = str;
+
+ fn index(&self, index: usize) -> &Self::Output {
+ match self {
+ PickerArgs::Slice(items) => items[index],
+ PickerArgs::Vec(items) => items[index],
+ PickerArgs::Owned(items) => &items[index],
+ }
+ }
+}
+
+impl<'a> IntoIterator for &'a PickerArgs<'a> {
+ type Item = &'a str;
+ type IntoIter = PickerIter<'a>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ match self {
+ PickerArgs::Slice(items) => PickerIter::Slice(items.iter()),
+ PickerArgs::Vec(items) => PickerIter::Vec(items.iter()),
+ PickerArgs::Owned(items) => PickerIter::Owned(items.iter()),
+ }
+ }
+}
+
+impl<'a, Route> From<&'a [&'a str]> for Picker<'a, Route> {
+ fn from(value: &'a [&'a str]) -> Self {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Slice(value),
+ }
+ }
+}
+
+impl<'a, Route> From<Vec<&'a str>> for Picker<'a, Route> {
+ fn from(value: Vec<&'a str>) -> Self {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Vec(value),
+ }
+ }
+}
+
+impl<'a, Route> From<Vec<String>> for Picker<'a, Route> {
+ fn from(value: Vec<String>) -> Self {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Owned(value),
+ }
+ }
+}
+
+impl<'a, Route> Picker<'a, Route> {
+ /// Returns a reference to the internal `PickerArgs`.
+ pub fn args(&self) -> &PickerArgs<'a> {
+ &self.args
+ }
+
+ /// Returns a mutable reference to the internal `PickerArgs`.
+ pub fn args_mut(&mut self) -> &mut PickerArgs<'a> {
+ &mut self.args
+ }
+
+ /// Consumes `self` and returns the internal `PickerArgs`.
+ pub fn into_args(self) -> PickerArgs<'a> {
+ self.args
+ }
+
+ /// Returns the number of arguments.
+ pub fn len(&self) -> usize {
+ self.args.len()
+ }
+
+ /// Returns `true` if there are no arguments.
+ pub fn is_empty(&self) -> bool {
+ self.args.is_empty()
+ }
+
+ /// Returns an iterator over the arguments, yielding `&str` values.
+ pub fn iter(&'a self) -> PickerIter<'a> {
+ self.args.iter()
+ }
+}
+
+impl<'a, Route> Index<usize> for Picker<'a, Route> {
+ type Output = str;
+
+ fn index(&self, index: usize) -> &Self::Output {
+ &self.args[index]
+ }
+}
+
+impl<'a, Route> Index<usize> for &Picker<'a, Route> {
+ type Output = str;
+
+ fn index(&self, index: usize) -> &Self::Output {
+ &self.args[index]
+ }
+}
+
+impl<'a, Route> IntoIterator for &'a Picker<'a, Route> {
+ type Item = &'a str;
+ type IntoIter = PickerIter<'a>;
+
+ fn into_iter(self) -> Self::IntoIter {
+ self.args.iter()
+ }
+}
+
+/// Iterator for `Picker` (and `PickerArgs`), yielding `&'a str` values.
+pub enum PickerIter<'a> {
+ /// Iterates over a borrowed slice (`&[&str]`)
+ Slice(std::slice::Iter<'a, &'a str>),
+ /// Iterates over an owned vector of borrowed string slices (`Vec<&str>`)
+ Vec(std::slice::Iter<'a, &'a str>),
+ /// Iterates over an owned vector of owned strings (`Vec<String>`)
+ Owned(std::slice::Iter<'a, String>),
+}
+
+impl<'a> Iterator for PickerIter<'a> {
+ type Item = &'a str;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ match self {
+ PickerIter::Slice(iter) => iter.next().copied(),
+ PickerIter::Vec(iter) => iter.next().copied(),
+ PickerIter::Owned(iter) => iter.next().map(|s| s.as_str()),
+ }
+ }
+
+ fn size_hint(&self) -> (usize, Option<usize>) {
+ match self {
+ PickerIter::Slice(iter) => iter.size_hint(),
+ PickerIter::Vec(iter) => iter.size_hint(),
+ PickerIter::Owned(iter) => iter.size_hint(),
+ }
+ }
+}
+
+impl<'a> ExactSizeIterator for PickerIter<'a> {}
+
+/// Trait for converting types into a `Picker`
+///
+/// Implemented for:
+/// - `&[&str]` (borrowed slice)
+/// - `&[String]` (borrowed slice of owned strings)
+/// - `Vec<&str>` (owned vector of borrowed strings)
+/// - `Vec<String>` (owned vector of owned strings)
+pub trait IntoPicker<'a> {
+ /// Converts the value into a `Picker`
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use mingling_picker::{IntoPicker, Picker};
+ ///
+ /// let args: Picker = (&["hello", "world"][..]).to_picker();
+ /// assert_eq!(args.len(), 2);
+ ///
+ /// let args: Picker = vec!["foo", "bar"].to_picker();
+ /// assert_eq!(args.len(), 2);
+ ///
+ /// let args: Picker = vec!["a".to_string(), "b".to_string()].to_picker();
+ /// assert_eq!(args.len(), 2);
+ /// ```
+ fn to_picker(self) -> Picker<'a, ()>;
+
+ /// Creates a `PickerPattern1` from the given arg for the `pick` method.
+ ///
+ /// This method converts the value into a `Picker` and starts a parameter
+ /// picking chain with one arg. The result is initially `Unparsed`.
+ fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, ()>
+ where
+ Self: Sized,
+ N: Pickable<'a> + Default + Sized,
+ {
+ PickerPattern1 {
+ args: self.to_picker().args,
+ arg_1: arg.into(),
+ result_1: PickerArgResult::Unparsed,
+ default_1: None,
+ route_1: None,
+ post_1: None,
+ error_route: None,
+ }
+ }
+
+ /// Converts the value into a `Picker` with a specified route type.
+ ///
+ /// This method allows changing the route (phantom type parameter) of the picker.
+ /// The route type is typically used to distinguish different parsing contexts or
+ /// to carry compile-time state information through the picking chain.
+ fn with_route<NewRoute>(self) -> Picker<'a, NewRoute>
+ where
+ Self: Sized,
+ {
+ Picker {
+ route_phantom: PhantomData,
+ args: self.to_picker().args,
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for &'a [&'a str] {
+ fn to_picker(self) -> Picker<'a, ()> {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Slice(self),
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for &'a [String] {
+ fn to_picker(self) -> Picker<'a, ()> {
+ let vec: Vec<&str> = self.iter().map(|s| s.as_str()).collect();
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Vec(vec),
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for Vec<&'a str> {
+ fn to_picker(self) -> Picker<'a, ()> {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Vec(self),
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for Vec<String> {
+ fn to_picker(self) -> Picker<'a, ()> {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Owned(self),
+ }
+ }
+}
diff --git a/mingling_picker/src/picker/parse.rs b/mingling_picker/src/picker/parse.rs
new file mode 100644
index 0000000..8f7d514
--- /dev/null
+++ b/mingling_picker/src/picker/parse.rs
@@ -0,0 +1,177 @@
+// --------------------------------------------------------------------------------------------
+// I have to say, the code generated by this `internal_repeat!` macro is really UGLY.
+//
+// But I have to admit, this is a **trade-off**. To achieve the syntax of `pick().pick().pick()`
+// while ensuring type safety, this is the best approach I could think of.
+//
+// P.S. If there's a better way, please let me know. Thanks!
+// --------------------------------------------------------------------------------------------
+//
+// Then, I must disable `clippy::type_complexity` — this guy is way too noisy.
+#![allow(clippy::type_complexity)]
+
+use crate::{Pickable, PickerArgAttr, PickerArgInfo, PickerArgResult, PickerArgs, TagPhaseContext};
+use mingling_picker_macros::internal_repeat;
+
+internal_repeat!(1..=32 => {
+ use crate::PickerPattern$;
+ use crate::PickerResult$;
+});
+
+internal_repeat!(1..=32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+) {
+ /// Unwraps the result, panicking if a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if a route was selected.
+ pub fn unwrap(self) -> ((T$,+)) {
+ let p = self.parse();
+ ((p.v$.unwrap(),+))
+ }
+
+ /// Returns the individual option values without checking the route.
+ pub fn unpack(self) -> ((Option<T$>,+)) {
+ let p = self.parse();
+ ((p.v$,+))
+ }
+
+ /// Converts to a `Result`, returning `Err(route)` if a route was selected,
+ /// or `Ok(values)` otherwise.
+ pub fn to_result(self) -> Result<((T$,+)), Route> {
+ let p = self.parse();
+ if let Some(r) = p.route {
+ return Err(r);
+ }
+ Ok(p.unwrap())
+ }
+
+ /// Converts to an `Option`, returning `None` if a route was selected,
+ /// or `Some(values)` otherwise.
+ pub fn to_option(self) -> Option<((T$,+))> {
+ let p = self.parse();
+ if p.route.is_some() {
+ return None;
+ }
+ Some(p.unwrap())
+ }
+ }
+});
+
+internal_repeat!(1..=32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ pub fn parse(mut self) -> PickerResult$<(T$,+), Route> {
+ // ArgInfos
+ let arg_infos: [PickerArgInfo; $] = [
+ (
+ PickerArgInfo::from(self.arg_$),
+ +)
+ ];
+
+ let mut bundle: [
+ (
+ // Arg Attr
+ PickerArgAttr,
+
+ // Tag Func
+ Box<dyn FnOnce(&PickerArgs<'a>, &[u8]) -> Vec<usize>>,
+
+ // Pick Func
+ Box<dyn FnOnce(&[&str], &mut Option<Route>)>,
+
+ // Index
+ usize
+ )
+ ; $] = [
+ (
+ (
+ // Arg Attr
+ T$::get_attr(self.arg_$),
+
+ // Tag Func
+ Box::new(|args, mask| {
+ let ctx = TagPhaseContext {
+ arg_info: &arg_infos[$-],
+ args,
+ mask
+ };
+ T$::tag(ctx)
+ }),
+
+ // Pick Func
+ Box::new(|args, error_route| {
+ self.result_$ = match T$::pick(args) {
+ PickerArgResult::Parsed(mut value) => {
+ // Postprocess
+ if let Some(post) = self.post_$ {
+ value = post(value);
+ }
+ PickerArgResult::Parsed(value)
+ },
+ other => {
+ if let Some(get_default) = self.default_$ {
+ let mut value = get_default();
+
+ // Postprocess
+ if let Some(post) = self.post_$ {
+ value = post(value);
+ }
+
+ PickerArgResult::Parsed(value)
+ } else {
+ if error_route.is_none() {
+ if let Some(get_route) = self.route_$ {
+ *error_route = Some(get_route());
+ }
+ }
+ other
+ }
+ },
+
+ }
+ }),
+
+ // Index
+ $
+ ),
+ +)
+ ];
+
+ // Sort by Bundle Ord (descending)
+ bundle.sort_by(|a, b| b.0.cmp(&a.0));
+
+ // Mask — size = number of args (not args), so use args length
+ let mut mask: Vec<u8> = vec![0u8; self.args.len()];
+
+ // Parsing
+ for (_, tag_func, pick_func, _idx) in bundle {
+
+ // Tag phase
+ let tagged = tag_func(&self.args, mask.as_slice());
+ let mut args_to_pick: Vec<&str> = vec![];
+
+ for i in tagged {
+ mask[i] = 1;
+
+ // Update args to pick
+ args_to_pick.push(self.args.get(i).unwrap_or_default());
+ }
+
+ // Pick phase
+ pick_func(args_to_pick.as_slice(), &mut self.error_route);
+ }
+
+ // Combine Result
+ let result: PickerResult$<(T$,+), Route> = PickerResult$ {
+ route: self.error_route,
+ (
+ v$: self.result_$.to_option(),
+ +)
+ };
+ result
+ }
+ }
+});
diff --git a/mingling_picker/src/picker/patterns.rs b/mingling_picker/src/picker/patterns.rs
new file mode 100644
index 0000000..3c6e73f
--- /dev/null
+++ b/mingling_picker/src/picker/patterns.rs
@@ -0,0 +1,199 @@
+use mingling_picker_macros::internal_repeat;
+
+use crate::{Pickable, Picker, PickerArg, PickerArgResult, PickerArgs};
+
+internal_repeat!(1..=32 => {
+ #[doc(hidden)]
+ pub struct PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ pub args: PickerArgs<'a>,
+ pub error_route: Option<Route>,
+ (
+ pub arg_$: &'a PickerArg<'a, T$>,
+ pub result_$: PickerArgResult<T$>,
+ pub default_$: Option<Box<dyn FnOnce() -> T$>>,
+ pub route_$: Option<Box<dyn FnOnce() -> Route>>,
+ pub post_$: Option<Box<dyn FnOnce(T$) -> T$>>,
+ +)
+ }
+});
+
+internal_repeat!(1..=32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ /// Sets a default value provider for this arg.
+ ///
+ /// If the arg is not provided by the user at runtime, the given closure will be
+ /// called to produce a default value. The closure is expected to return `T$`.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .or(|| 42);
+ /// ```
+ #[allow(clippy::type_complexity)]
+ pub fn or<F>(mut self, func: F) -> Self
+ where
+ F: FnMut() -> T$,
+ F: 'static,
+ {
+ self.default_$ = Some(Box::new(func));
+ self
+ }
+
+ /// Uses the default value for this arg's type if the arg is not provided.
+ ///
+ /// If the arg is not provided by the user at runtime, the default value for `T$`
+ /// (as defined by the `Default` trait) will be used.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .or_default();
+ /// ```
+ #[allow(clippy::type_complexity)]
+ pub fn or_default(mut self) -> Self
+ where
+ T$: Default,
+ {
+ self.default_$ = Some(Box::new(|| T$::default()));
+ self
+ }
+
+ /// Sets a route for when the arg is not provided.
+ ///
+ /// If the arg is not provided by the user at runtime, the given closure will be
+ /// called to produce a route value that will be returned early.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .or_route(|| Redirect::home());
+ /// ```
+ pub fn or_route<F>(mut self, func: F) -> Self
+ where
+ F: FnMut() -> Route,
+ F: 'static,
+ {
+ self.route_$ = Some(Box::new(func));
+ self
+ }
+
+
+ /// Resets the route for this picker pattern, allowing a different route type.
+ ///
+ /// This method converts the current `PickerPattern` into a new one with a different
+ /// route type `NewRoute`. All existing arg configurations, defaults, and post-
+ /// processing functions are preserved, but the `error_route` and individual
+ /// `route_$` fields are cleared (set to `None`).
+ ///
+ /// This is useful when you want to change the error/redirect route type mid-chain,
+ /// for example when composing patterns from different contexts that use different
+ /// route enums.
+ #[allow(clippy::type_complexity)]
+ pub fn with_route<NewRoute>(self) -> PickerPattern$<'a, (T$,+), NewRoute> {
+ PickerPattern$ {
+ args: self.args,
+ error_route: None,
+ (
+ arg_$: self.arg_$,
+ result_$: self.result_$,
+ default_$: self.default_$,
+ route_$: None,
+ post_$: self.post_$,
+ +)
+ }
+ }
+
+ /// Attaches a post-processing function to this arg.
+ ///
+ /// After the arg's value is parsed (or defaulted), the given closure will be
+ /// invoked with the parsed value and its return value will be used as the final
+ /// result. This allows transforming or validating the parsed value.
+ ///
+ /// # Example
+ ///
+ /// ```ignore
+ /// let pattern = picker
+ /// .pick(&my_arg)
+ /// .post(|val| val * 2);
+ /// ```
+ #[allow(clippy::type_complexity)]
+ pub fn post<F>(mut self, func: F) -> Self
+ where
+ F: FnMut(T$) -> T$,
+ F: 'static,
+ {
+ self.post_$ = Some(Box::new(func));
+ self
+ }
+ }
+});
+
+internal_repeat!(1..32 => {
+ impl<'a, (T$,+), Route> PickerPattern$<'a, (T$,+), Route>
+ where (T$: Pickable<'a>,+)
+ {
+ #[allow(clippy::type_complexity)]
+ /// Adds a new arg to the picking chain, returning a new `PickerPattern` with one more type parameter.
+ ///
+ /// This method extends the current picking pattern by appending an additional arg.
+ /// The previous args and their results are preserved as part of the new pattern.
+ /// The new arg's result is initially `Unparsed`.
+ pub fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern$+<'a, (T$,+), N, Route>
+ where
+ N: Pickable<'a>,
+ {
+ PickerPattern$+ {
+ // Args
+ args: self.args,
+ error_route: self.error_route,
+
+ // Current
+ arg_$+: arg.into(),
+ result_$+: PickerArgResult::Unparsed,
+ default_$+: None,
+ route_$+: None,
+ post_$+: None,
+
+ // Prev
+ (
+ arg_$: self.arg_$,
+ result_$: self.result_$,
+ default_$: self.default_$,
+ route_$: self.route_$,
+ post_$: self.post_$,
+ +)
+ }
+ }
+ }
+});
+
+impl<'a, Route> Picker<'a, Route> {
+ /// Creates a `PickerPattern1` from the given arg to start a picking chain.
+ ///
+ /// This method initiates a parameter picking chain with one arg.
+ /// The result is initially `Unparsed`.
+ pub fn pick<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern1<'a, N, Route>
+ where
+ N: Pickable<'a>,
+ {
+ PickerPattern1 {
+ args: self.args,
+ error_route: None::<Route>,
+ arg_1: arg.into(),
+ result_1: PickerArgResult::Unparsed,
+ route_1: None,
+ default_1: None,
+ post_1: None,
+ }
+ }
+}
diff --git a/mingling_picker/src/picker/result.rs b/mingling_picker/src/picker/result.rs
new file mode 100644
index 0000000..9cd78ae
--- /dev/null
+++ b/mingling_picker/src/picker/result.rs
@@ -0,0 +1,56 @@
+#![allow(clippy::type_complexity)] // Aha, Type Gymnastics!
+
+use mingling_picker_macros::internal_repeat;
+
+internal_repeat!(1..=32 => {
+ #[doc(hidden)]
+ pub struct PickerResult$<(T$,+), Route> {
+ /// The route selected by the picker, if any.
+ /// If this is `Some`, the picker chose to follow a route instead of selecting values,
+ /// and all value fields (`v1`, `v2`, ...) will be `None`.
+ ///
+ /// Note: "route" here refers to an alternative path/choice, not a network route.
+ pub route: Option<Route>,
+
+ (
+ #[doc = concat!("The optional value for the ", $, "th type parameter.")]
+ pub v$: Option<T$>,
+ +)
+ }
+});
+
+internal_repeat!(1..=32 => {
+ impl<(T$,+), Route> PickerResult$<(T$,+), Route> {
+ /// Unwraps the result, panicking if a route was selected.
+ ///
+ /// # Panics
+ ///
+ /// Panics if `self.route` is `Some(...)`.
+ pub fn unwrap(self) -> ((T$,+)) {
+ ((self.v$.unwrap(),+))
+ }
+
+ /// Returns the individual option values without checking the route.
+ pub fn unpack(self) -> ((Option<T$>,+)) {
+ ((self.v$,+))
+ }
+
+ /// Converts to a `Result`, returning `Err(route)` if a route was selected,
+ /// or `Ok(values)` otherwise.
+ pub fn to_result(self) -> Result<((T$,+)), Route> {
+ if let Some(r) = self.route {
+ return Err(r);
+ }
+ Ok(self.unwrap())
+ }
+
+ /// Converts to an `Option`, returning `None` if a route was selected,
+ /// or `Some(values)` otherwise.
+ pub fn to_option(self) -> Option<((T$,+))> {
+ if let Some(_) = self.route {
+ return None;
+ }
+ Some(self.unwrap())
+ }
+ }
+});
diff --git a/mingling_picker/src/value.rs b/mingling_picker/src/value.rs
new file mode 100644
index 0000000..995aa00
--- /dev/null
+++ b/mingling_picker/src/value.rs
@@ -0,0 +1,5 @@
+mod flag;
+pub use flag::*;
+
+mod vec_until;
+pub use vec_until::*;
diff --git a/mingling_picker/src/value/flag.rs b/mingling_picker/src/value/flag.rs
new file mode 100644
index 0000000..ee0d6ee
--- /dev/null
+++ b/mingling_picker/src/value/flag.rs
@@ -0,0 +1,145 @@
+use std::{
+ fmt::{Debug, Display},
+ ops::{Deref, Not},
+};
+
+/// Parsed result of a boolean-style command-line flag.
+///
+/// `Flag` is a **value type** that can be declared in [`PickerArg`].
+/// When the user passes `--verbose` on the command line, the parsed result is `Flag::Active`;
+/// when the flag is absent, the result is `Flag::Inactive`.
+///
+/// # Why not just `bool`?
+///
+/// Unlike a raw `bool`, `Flag` carries **explicit semantics** about whether
+/// the flag was actually provided by the user (`Active`) or simply omitted
+/// (`Inactive`). This distinction matters when you want to distinguish
+/// "the user intentionally omitted the flag" from "the flag was processed but
+/// resolved to false" — the `Pickable` implementation for `Flag` always
+/// returns `Parsed(Flag::Inactive)` when no matching argument is found,
+/// rather than `NotFound`, making it always succeed with a meaningful default.
+///
+/// # Conversions
+///
+/// `Flag` interoperates seamlessly with `bool`: `Flag::Active` is `true`,
+/// `Flag::Inactive` is `false`. The [`Deref`] impl allows using a `Flag`
+/// directly in boolean contexts:
+///
+/// ```
+/// # use mingling_picker::value::Flag;
+/// let flag = Flag::Active;
+/// if *flag { /* runs */ }
+/// ```
+///
+/// [`PickerArg`]: crate::PickerArg
+#[derive(Default, Clone, Copy, PartialEq, Eq)]
+pub enum Flag {
+ /// The flag was **not** present on the command line.
+ ///
+ /// This is the default state, equivalent to `false`.
+ #[default]
+ Inactive,
+
+ /// The flag **was** present on the command line.
+ ///
+ /// Equivalent to `true`.
+ Active,
+}
+
+impl Debug for Flag {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Inactive => write!(f, "inactive"),
+ Self::Active => write!(f, "active"),
+ }
+ }
+}
+
+impl Display for Flag {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::Inactive => write!(f, "inactive"),
+ Self::Active => write!(f, "active"),
+ }
+ }
+}
+
+impl Flag {
+ /// Converts this `Flag` into a `bool`.
+ ///
+ /// Returns `true` if the flag is [`Active`], `false` if [`Inactive`].
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use mingling_picker::value::Flag;
+ /// assert!(Flag::Active.bool());
+ /// assert!(!Flag::Inactive.bool());
+ /// ```
+ ///
+ /// [`Active`]: Flag::Active
+ /// [`Inactive`]: Flag::Inactive
+ #[must_use]
+ #[inline(always)]
+ pub fn bool(&self) -> bool {
+ *self == Flag::Active
+ }
+}
+
+impl PartialEq<bool> for Flag {
+ fn eq(&self, other: &bool) -> bool {
+ self.bool() == *other
+ }
+}
+
+/// Compares `bool` with `Flag` using `==`.
+impl PartialEq<Flag> for bool {
+ fn eq(&self, other: &Flag) -> bool {
+ *self == other.bool()
+ }
+}
+
+impl From<bool> for Flag {
+ fn from(value: bool) -> Self {
+ if value { Flag::Active } else { Flag::Inactive }
+ }
+}
+
+impl From<Flag> for bool {
+ fn from(val: Flag) -> Self {
+ val == Flag::Active
+ }
+}
+
+/// Allows `Flag` to be used in boolean contexts via `*flag`.
+///
+/// # Examples
+///
+/// ```
+/// # use mingling_picker::value::Flag;
+/// let flag = Flag::Active;
+/// if *flag {
+/// println!("flag is set");
+/// }
+/// ```
+impl Deref for Flag {
+ type Target = bool;
+
+ fn deref(&self) -> &bool {
+ match self {
+ Flag::Active => &true,
+ Flag::Inactive => &false,
+ }
+ }
+}
+
+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/src/value/vec_until.rs b/mingling_picker/src/value/vec_until.rs
new file mode 100644
index 0000000..1b79641
--- /dev/null
+++ b/mingling_picker/src/value/vec_until.rs
@@ -0,0 +1,134 @@
+use std::marker::PhantomData;
+use std::ops::{Deref, DerefMut};
+
+use crate::{
+ BoundaryCheck, MultiPickableWithBoundary, Pickable, PickerArg, PickerArgAttr, PickerArgResult,
+ SinglePickable, TagPhaseContext,
+ matcher_needed::Matcher,
+ parselib::{MultiArgMatcher, ParserStyle},
+};
+
+/// A `Vec`-like container that stops collecting when [`BoundaryCheck`]
+/// returns `true`.
+///
+/// This type exists to signal "I know what I'm doing with boundaries"
+/// at the type level (as opposed to `Vec<T>` which greedily takes
+/// everything).
+#[derive(Debug, Clone, PartialEq, Eq, Default)]
+pub struct VecUntil<T> {
+ pub(crate) inner: Vec<T>,
+ _marker: PhantomData<T>,
+}
+
+impl<T> VecUntil<T> {
+ pub fn into_inner(self) -> Vec<T> {
+ self.inner
+ }
+}
+
+impl<T> From<Vec<T>> for VecUntil<T> {
+ fn from(v: Vec<T>) -> Self {
+ VecUntil {
+ inner: v,
+ _marker: PhantomData,
+ }
+ }
+}
+
+impl<T> From<VecUntil<T>> for Vec<T> {
+ fn from(v: VecUntil<T>) -> Self {
+ v.inner
+ }
+}
+
+impl<T> Deref for VecUntil<T> {
+ type Target = Vec<T>;
+ fn deref(&self) -> &Vec<T> {
+ &self.inner
+ }
+}
+
+impl<T> DerefMut for VecUntil<T> {
+ fn deref_mut(&mut self) -> &mut Vec<T> {
+ &mut self.inner
+ }
+}
+
+// MultiPickableWithBoundary impl
+
+impl<T> MultiPickableWithBoundary for VecUntil<T>
+where
+ T: SinglePickable + BoundaryCheck,
+{
+ type Checker = T;
+
+ fn pick_multi(raw: Vec<String>) -> PickerArgResult<Self> {
+ let mut inner = Vec::with_capacity(raw.len());
+ for s in &raw {
+ match T::pick_single(Some(s)) {
+ PickerArgResult::Parsed(v) => inner.push(v),
+ PickerArgResult::NotFound => return PickerArgResult::NotFound,
+ PickerArgResult::Unparsed => {}
+ }
+ }
+ PickerArgResult::Parsed(VecUntil {
+ inner,
+ _marker: PhantomData,
+ })
+ }
+}
+
+// Pickable impl
+
+impl<'a, T> Pickable<'a> for VecUntil<T>
+where
+ T: SinglePickable + BoundaryCheck,
+{
+ fn get_attr(flag: &'a PickerArg<'a, Self>) -> PickerArgAttr {
+ PickerArgAttr::positional_or_multi(flag)
+ }
+
+ fn tag(ctx: TagPhaseContext) -> Vec<usize> {
+ let args = ctx.args;
+ let is_positional = ctx.arg_info.positional;
+ let positions = MultiArgMatcher::match_all(ctx.into());
+ if positions.is_empty() {
+ return positions;
+ }
+
+ let start = if is_positional { 0 } else { 1 };
+ if start >= positions.len() {
+ return positions;
+ }
+
+ let mut cut = start;
+ for &idx in &positions[start..] {
+ if let Some(raw) = args.get(idx)
+ && T::check_boundary(raw)
+ {
+ break;
+ }
+ cut += 1;
+ }
+
+ positions[..cut].to_vec()
+ }
+
+ fn pick(raw_strs: &[&str]) -> PickerArgResult<Self> {
+ let strs = strip_flag(raw_strs);
+ let owned: Vec<String> = strs.iter().map(|&s| s.to_string()).collect();
+ <VecUntil<T> as MultiPickableWithBoundary>::pick_multi(owned)
+ }
+}
+
+/// If the first raw string looks like a named flag (starts with the
+/// style's long or short prefix), strip it — it's the flag, not a value.
+fn strip_flag<'a>(raw_strs: &'a [&'a str]) -> &'a [&'a str] {
+ if let Some(first) = raw_strs.first() {
+ let style = ParserStyle::global_style();
+ if first.starts_with(style.long_prefix) || first.starts_with(style.short_prefix) {
+ return &raw_strs[1..];
+ }
+ }
+ raw_strs
+}