aboutsummaryrefslogtreecommitdiff
path: root/arg_picker
diff options
context:
space:
mode:
Diffstat (limited to 'arg_picker')
-rw-r--r--arg_picker/Cargo.toml17
-rw-r--r--arg_picker/README.md47
-rw-r--r--arg_picker/src/arg.rs299
-rw-r--r--arg_picker/src/builtin.rs4
-rw-r--r--arg_picker/src/builtin/pick_bool.rs22
-rw-r--r--arg_picker/src/builtin/pick_flag.rs21
-rw-r--r--arg_picker/src/builtin/pick_numbers.rs80
-rw-r--r--arg_picker/src/builtin/pick_string.rs11
-rw-r--r--arg_picker/src/corebind.rs2
-rw-r--r--arg_picker/src/corebind/entry_picker.rs131
-rw-r--r--arg_picker/src/infos.rs446
-rw-r--r--arg_picker/src/lib.rs62
-rw-r--r--arg_picker/src/parselib.rs144
-rw-r--r--arg_picker/src/parselib/arg_matcher.rs142
-rw-r--r--arg_picker/src/parselib/flag_matcher.rs82
-rw-r--r--arg_picker/src/parselib/multi_arg_matcher.rs141
-rw-r--r--arg_picker/src/parselib/pos_matcher.rs71
-rw-r--r--arg_picker/src/parselib/single_matcher.rs59
-rw-r--r--arg_picker/src/parselib/style.rs258
-rw-r--r--arg_picker/src/parselib/utils.rs276
-rw-r--r--arg_picker/src/pickable.rs91
-rw-r--r--arg_picker/src/pickable/multi_pickable.rs76
-rw-r--r--arg_picker/src/pickable/single_pickable.rs69
-rw-r--r--arg_picker/src/picker.rs432
-rw-r--r--arg_picker/src/picker/parse.rs177
-rw-r--r--arg_picker/src/picker/patterns.rs205
-rw-r--r--arg_picker/src/picker/result.rs56
-rw-r--r--arg_picker/src/value.rs5
-rw-r--r--arg_picker/src/value/flag.rs145
-rw-r--r--arg_picker/src/value/vec_until.rs134
-rw-r--r--arg_picker/test/Cargo.lock68
-rw-r--r--arg_picker/test/Cargo.toml9
-rw-r--r--arg_picker/test/src/lib.rs23
-rw-r--r--arg_picker/test/src/test.rs10
-rw-r--r--arg_picker/test/src/test/arg_matcher_test.rs289
-rw-r--r--arg_picker/test/src/test/basic_test.rs223
-rw-r--r--arg_picker/test/src/test/multi_arg_test.rs186
-rw-r--r--arg_picker/test/src/test/multi_value_test.rs66
-rw-r--r--arg_picker/test/src/test/pos_matcher_test.rs141
-rw-r--r--arg_picker/test/src/test/priority_test.rs85
-rw-r--r--arg_picker/test/src/test/route_test.rs200
-rw-r--r--arg_picker/test/src/test/style_test.rs300
-rw-r--r--arg_picker/test/src/test/value_flag_test.rs174
-rw-r--r--arg_picker/test/src/test/value_string_test.rs171
44 files changed, 5650 insertions, 0 deletions
diff --git a/arg_picker/Cargo.toml b/arg_picker/Cargo.toml
new file mode 100644
index 0000000..f7672c1
--- /dev/null
+++ b/arg_picker/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "arg-picker"
+version = "0.1.0"
+edition = "2024"
+license = "MIT OR Apache-2.0"
+repository = "https://github.com/mingling-rs/mingling/tree/main/arg-picker"
+authors = ["Weicao-CatilGrass"]
+readme = "README.md"
+description = "A lightweight, type-safe CLI argument parser"
+
+[features]
+mingling_support = ["dep:mingling_core", "arg-picker-macros/mingling_support"]
+
+[dependencies]
+mingling_core = { workspace = true, optional = true }
+arg-picker-macros.workspace = true
+just_fmt.workspace = true
diff --git a/arg_picker/README.md b/arg_picker/README.md
new file mode 100644
index 0000000..14051f3
--- /dev/null
+++ b/arg_picker/README.md
@@ -0,0 +1,47 @@
+# Argument Picker
+
+A command-line argument parser for [Mingling](https://github.com/mingling-rs/mingling), enabled by the `mingling/picker` feature.
+
+```toml
+[dependencies.mingling]
+version = "0.3.0"
+features = [
+ "picker"
+]
+```
+
+Of course, you can also use it as a standalone crate by replacing `mingling::picker` with `arg_picker`:
+
+```toml
+[dependencies]
+arg-picker = "0.1.0"
+```
+
+## Chained Argument Parser
+
+Provides a clean chained-call API for declaring arguments to parse:
+
+```rust
+use arg_picker::prelude::*;
+
+let args: Vec<&str> = vec!["--name", "Bob", "--age", "24"];
+
+let (name, age) = args
+ .pick(&arg![name: String])
+ .or(|| "Alice".to_string())
+ .pick(&arg![age: i32])
+ .or(|| 24)
+ .post(|num| num.clamp(0, 120))
+ .unwrap();
+
+assert_eq!(name, "Bob".to_string());
+assert_eq!(age, 24);
+```
+
+## Parsing Function Library
+
+Provides a pure function library `parselib` for analyzing the structure of command-line arguments.
+
+```rust
+use arg_picker::parselib::*;
+```
diff --git a/arg_picker/src/arg.rs b/arg_picker/src/arg.rs
new file mode 100644
index 0000000..78ad539
--- /dev/null
+++ b/arg_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/arg_picker/src/builtin.rs b/arg_picker/src/builtin.rs
new file mode 100644
index 0000000..e855b08
--- /dev/null
+++ b/arg_picker/src/builtin.rs
@@ -0,0 +1,4 @@
+mod pick_bool;
+mod pick_flag;
+mod pick_numbers;
+mod pick_string;
diff --git a/arg_picker/src/builtin/pick_bool.rs b/arg_picker/src/builtin/pick_bool.rs
new file mode 100644
index 0000000..ccc4424
--- /dev/null
+++ b/arg_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/arg_picker/src/builtin/pick_flag.rs b/arg_picker/src/builtin/pick_flag.rs
new file mode 100644
index 0000000..b642a9a
--- /dev/null
+++ b/arg_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/arg_picker/src/builtin/pick_numbers.rs b/arg_picker/src/builtin/pick_numbers.rs
new file mode 100644
index 0000000..a5ab0a9
--- /dev/null
+++ b/arg_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/arg_picker/src/builtin/pick_string.rs b/arg_picker/src/builtin/pick_string.rs
new file mode 100644
index 0000000..c96f667
--- /dev/null
+++ b/arg_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/arg_picker/src/corebind.rs b/arg_picker/src/corebind.rs
new file mode 100644
index 0000000..3581871
--- /dev/null
+++ b/arg_picker/src/corebind.rs
@@ -0,0 +1,2 @@
+mod entry_picker;
+pub use entry_picker::*;
diff --git a/arg_picker/src/corebind/entry_picker.rs b/arg_picker/src/corebind/entry_picker.rs
new file mode 100644
index 0000000..69bc4d8
--- /dev/null
+++ b/arg_picker/src/corebind/entry_picker.rs
@@ -0,0 +1,131 @@
+use std::marker::PhantomData;
+
+use mingling_core::{ChainProcess, Groupped, ProgramCollect};
+
+use crate::{Pickable, Picker, PickerArg, PickerArgs, PickerPattern1};
+
+/// Trait for converting Mingling entry types (types that implement `Groupped<R>` and `Into<Vec<String>>`)
+/// into [`Picker`] instances for a given route type `R`.
+///
+/// This trait provides a bridge between entry definitions created with the `mingling` framework
+/// and the picker argument system used for CLI argument parsing and routing.
+///
+/// # Type Parameters
+///
+/// * `'a` — The lifetime of the picker and its references to argument definitions.
+/// * `This` — The program type used for dispatching runtime routes; must implement [`ProgramCollect`].
+/// * `Route` — The route type used for dispatching; must implement [`Groupped<This>`].
+pub trait EntryPicker<'a, This> {
+ /// Converts `self` into a [`Picker`] for the given route type `Route`.
+ fn to_picker(self) -> Picker<'a, ChainProcess<This>>;
+
+ /// Starts building a picker pattern with the first argument.
+ ///
+ /// Returns a [`PickerPattern1`] that can be further chained with additional
+ /// arguments, defaults, routes, and post-processing.
+ ///
+ /// # Type Parameters
+ ///
+ /// * `Next` — The nominal type of the first argument; must implement [`Pickable`].
+ ///
+ /// # Parameters
+ ///
+ /// * `arg` — The argument definition, typically obtained from [`crate::arg`].
+ fn pick<Next>(
+ self,
+ arg: impl Into<&'a PickerArg<'a, Next>>,
+ ) -> PickerPattern1<'a, Next, ChainProcess<This>>
+ where
+ Self: Sized,
+ Next: Pickable<'a> + Default + Sized,
+ {
+ let picker = Self::to_picker(self);
+ Picker::build_pattern1(picker.args, arg.into(), None)
+ }
+
+ /// Starts building a picker pattern with the first argument, using a default value provider.
+ ///
+ /// This is a shorthand for calling `.pick(arg).or(func)`.
+ ///
+ /// # Type Parameters
+ ///
+ /// * `Next` — The nominal type of the first argument; must implement [`Pickable`].
+ ///
+ /// # Parameters
+ ///
+ /// * `arg` — The argument definition, typically obtained from [`crate::arg`].
+ /// * `func` — A closure that provides a default value if the arg is not provided by the user.
+ fn pick_or<Next, F>(
+ self,
+ arg: impl Into<&'a PickerArg<'a, Next>>,
+ func: F,
+ ) -> PickerPattern1<'a, Next, ChainProcess<This>>
+ where
+ Self: Sized,
+ Next: Pickable<'a> + Default + Sized,
+ F: FnMut() -> Next + 'static,
+ {
+ self.pick(arg).or(func)
+ }
+
+ /// Starts building a picker pattern with the first argument, using a default value.
+ ///
+ /// This is a shorthand for calling `.pick(arg).or_default()`.
+ ///
+ /// # Type Parameters
+ ///
+ /// * `Next` — The nominal type of the first argument; must implement [`Pickable`] and [`Default`].
+ ///
+ /// # Parameters
+ ///
+ /// * `arg` — The argument definition, typically obtained from [`crate::arg`].
+ fn pick_or_default<Next>(
+ self,
+ arg: impl Into<&'a PickerArg<'a, Next>>,
+ ) -> PickerPattern1<'a, Next, ChainProcess<This>>
+ where
+ Self: Sized,
+ Next: Pickable<'a> + Default + Sized,
+ {
+ self.pick(arg).or_default()
+ }
+
+ /// Starts building a picker pattern with the first argument, using a route if the arg is not provided.
+ ///
+ /// This is a shorthand for calling `.pick(arg).or_route(func)`.
+ ///
+ /// # Type Parameters
+ ///
+ /// * `Next` — The nominal type of the first argument; must implement [`Pickable`].
+ ///
+ /// # Parameters
+ ///
+ /// * `arg` — The argument definition, typically obtained from [`crate::arg`].
+ /// * `func` — A closure that produces a route value if the arg is not provided by the user.
+ fn pick_or_route<Next, F>(
+ self,
+ arg: impl Into<&'a PickerArg<'a, Next>>,
+ func: F,
+ ) -> PickerPattern1<'a, Next, ChainProcess<This>>
+ where
+ Self: Sized,
+ Next: Pickable<'a> + Default + Sized,
+ F: FnMut() -> ChainProcess<This> + 'static,
+ {
+ self.pick(arg).or_route(func)
+ }
+}
+
+impl<'a, This, Bind> EntryPicker<'a, This> for Bind
+where
+ This: ProgramCollect<Enum = This>,
+ Bind: Groupped<This> + Into<Vec<String>>,
+{
+ fn to_picker(self) -> Picker<'a, ChainProcess<This>> {
+ let args = self.into();
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Owned(args),
+ }
+ }
+}
diff --git a/arg_picker/src/infos.rs b/arg_picker/src/infos.rs
new file mode 100644
index 0000000..074539a
--- /dev/null
+++ b/arg_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 arg_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 arg_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 arg_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 arg_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 arg_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 arg_picker::PickerArgResult;
+ ///
+ /// let result: PickerArgResult<i32> = PickerArgResult::Parsed(42);
+ /// assert_eq!(result.unwrap(), 42);
+ /// ```
+ ///
+ /// ```should_panic
+ /// use arg_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 arg_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 arg_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 arg_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 arg_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/arg_picker/src/lib.rs b/arg_picker/src/lib.rs
new file mode 100644
index 0000000..21a0d35
--- /dev/null
+++ b/arg_picker/src/lib.rs
@@ -0,0 +1,62 @@
+#![doc = include_str!("../README.md")]
+
+mod builtin;
+
+mod picker;
+pub use picker::*;
+
+mod pickable;
+pub use pickable::*;
+
+mod arg;
+pub use arg::*;
+
+mod infos;
+pub use infos::*;
+
+/// Provides the specific parsing logic for command-line arguments and common utilities,
+/// as well as customization of command-line argument styles.
+pub mod parselib;
+
+/// Parser-provided parseable command-line types
+pub mod value;
+
+/// The prelude module, which re-exports the most commonly used traits and types.
+///
+/// This module is intended to be imported with a wildcard import:
+///
+/// ```
+/// use arg_picker::prelude::*;
+/// ```
+pub mod prelude {
+ pub use crate::macros::arg;
+
+ #[cfg(not(feature = "mingling_support"))]
+ pub use crate::IntoPicker;
+
+ #[cfg(feature = "mingling_support")]
+ pub use crate::corebind::EntryPicker;
+}
+
+/// Re-export of the `arg_picker_macros` crate
+pub mod macros {
+ pub use arg_picker_macros::arg;
+}
+
+/// 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/arg_picker/src/parselib.rs b/arg_picker/src/parselib.rs
new file mode 100644
index 0000000..7fbd606
--- /dev/null
+++ b/arg_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/arg_picker/src/parselib/arg_matcher.rs b/arg_picker/src/parselib/arg_matcher.rs
new file mode 100644
index 0000000..38bb9cc
--- /dev/null
+++ b/arg_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/arg_picker/src/parselib/flag_matcher.rs b/arg_picker/src/parselib/flag_matcher.rs
new file mode 100644
index 0000000..e93d35a
--- /dev/null
+++ b/arg_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/arg_picker/src/parselib/multi_arg_matcher.rs b/arg_picker/src/parselib/multi_arg_matcher.rs
new file mode 100644
index 0000000..748b1be
--- /dev/null
+++ b/arg_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/arg_picker/src/parselib/pos_matcher.rs b/arg_picker/src/parselib/pos_matcher.rs
new file mode 100644
index 0000000..279e01e
--- /dev/null
+++ b/arg_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/arg_picker/src/parselib/single_matcher.rs b/arg_picker/src/parselib/single_matcher.rs
new file mode 100644
index 0000000..25c4741
--- /dev/null
+++ b/arg_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/arg_picker/src/parselib/style.rs b/arg_picker/src/parselib/style.rs
new file mode 100644
index 0000000..36ba8f0
--- /dev/null
+++ b/arg_picker/src/parselib/style.rs
@@ -0,0 +1,258 @@
+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
+ ///
+ /// ```
+ /// # use arg_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 arg_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())
+ }
+}
+
+/// Defines the naming convention for command-line option names.
+///
+/// Each variant represents a different case format that can be applied
+/// to option names (e.g., long option names) during parsing or generation.
+///
+/// # Examples
+///
+/// ```
+/// # use arg_picker::IntoPicker;
+/// use arg_picker::parselib::ParserStyleNamingCase;
+///
+/// let case = ParserStyleNamingCase::Kebab;
+/// assert_eq!(
+/// case.convert("brew_coffee".to_string()),
+/// "brew-coffee".to_string()
+/// );
+/// ```
+#[repr(u8)]
+#[derive(Default, Clone, Copy, PartialEq, Eq)]
+pub enum ParserStyleNamingCase {
+ /// snake_case format: words are separated by underscores, all lowercase.
+ ///
+ /// Example: `brew_coffee`
+ #[default]
+ Snake,
+ /// camelCase format: first word is lowercase, subsequent words are capitalized.
+ ///
+ /// Example: `brewCoffee`
+ Camel,
+ /// PascalCase format: every word starts with an uppercase letter.
+ ///
+ /// Example: `BrewCoffee`
+ Pascal,
+ /// kebab-case format: words are separated by hyphens, all lowercase.
+ ///
+ /// Example: `brew-coffee`
+ Kebab,
+ /// dot.case format: words are separated by dots, all lowercase.
+ ///
+ /// Example: `brew.coffee`
+ Dot,
+ /// Title Case format: words are separated by spaces, each word capitalized.
+ ///
+ /// Example: `Brew Coffee`
+ Title,
+ /// lower case format: words are separated by spaces, all lowercase.
+ ///
+ /// Example: `brew coffee`
+ Lower,
+ /// UPPER CASE format: words are separated by spaces, all uppercase.
+ ///
+ /// Example: `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 arg_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/arg_picker/src/parselib/utils.rs b/arg_picker/src/parselib/utils.rs
new file mode 100644
index 0000000..47c5b55
--- /dev/null
+++ b/arg_picker/src/parselib/utils.rs
@@ -0,0 +1,276 @@
+use crate::{
+ PickerArgInfo,
+ parselib::{MaskedArg, ParserStyle},
+};
+
+/// Builds a list of possible flag strings for the given argument info
+///
+/// This function generates formatted flag strings (e.g., `-h`, `--help`) from the short flag,
+/// long flag, and any aliases defined in the argument info. The long flag and alias names
+/// are converted according to the style's naming case convention before being formatted.
+#[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/arg_picker/src/pickable.rs b/arg_picker/src/pickable.rs
new file mode 100644
index 0000000..758ae9a
--- /dev/null
+++ b/arg_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/arg_picker/src/pickable/multi_pickable.rs b/arg_picker/src/pickable/multi_pickable.rs
new file mode 100644
index 0000000..84a8068
--- /dev/null
+++ b/arg_picker/src/pickable/multi_pickable.rs
@@ -0,0 +1,76 @@
+use crate::{
+ Pickable, PickerArg, PickerArgAttr, PickerArgResult, SinglePickable, TagPhaseContext,
+ matcher_needed::Matcher,
+ parselib::{MultiArgMatcher, ParserStyle},
+};
+
+/// 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/arg_picker/src/pickable/single_pickable.rs b/arg_picker/src/pickable/single_pickable.rs
new file mode 100644
index 0000000..8a5b3e6
--- /dev/null
+++ b/arg_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/arg_picker/src/picker.rs b/arg_picker/src/picker.rs
new file mode 100644
index 0000000..ecab648
--- /dev/null
+++ b/arg_picker/src/picker.rs
@@ -0,0 +1,432 @@
+use std::{marker::PhantomData, ops::Index};
+
+mod parse;
+
+mod patterns;
+pub use patterns::*;
+
+mod result;
+pub use result::*;
+
+use crate::{Pickable, PickerArg, PickerArgResult};
+
+#[doc = include_str!("../README.md")]
+pub struct Picker<'a, Route = ()> {
+ pub(crate) route_phantom: PhantomData<Route>,
+
+ /// Internal arguments of Picker
+ pub(crate) 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> {}
+
+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>,
+ {
+ Self::build_pattern1(self.args, arg.into(), None::<Route>)
+ }
+
+ /// Creates a `PickerPattern1` from the given arg.
+ /// If parsing fails, attempts the fallback arg.
+ pub fn pick_or<N, F>(
+ self,
+ arg: impl Into<&'a PickerArg<'a, N>>,
+ or_arg: F,
+ ) -> PickerPattern1<'a, N, Route>
+ where
+ N: Pickable<'a>,
+ F: FnMut() -> N + 'static,
+ {
+ self.pick(arg).or(or_arg)
+ }
+
+ /// Creates a `PickerPattern1` from the given arg.
+ /// If parsing fails, uses the provided default value.
+ pub fn pick_or_default<N>(
+ self,
+ arg: impl Into<&'a PickerArg<'a, N>>,
+ ) -> PickerPattern1<'a, N, Route>
+ where
+ N: Pickable<'a> + Default,
+ {
+ self.pick(arg).or_default()
+ }
+
+ /// Creates a `PickerPattern1` from the given arg.
+ /// If parsing fails, switches to the given error route.
+ pub fn pick_or_route<N, F>(
+ self,
+ arg: impl Into<&'a PickerArg<'a, N>>,
+ error_route: F,
+ ) -> PickerPattern1<'a, N, Route>
+ where
+ N: Pickable<'a>,
+ F: FnMut() -> Route + 'static,
+ {
+ self.pick(arg).or_route(error_route)
+ }
+}
+
+/// 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 arg_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,
+ {
+ Picker::build_pattern1(self.to_picker().args, arg.into(), 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 &'a Vec<String> {
+ fn to_picker(self) -> Picker<'a, ()> {
+ let slice: Vec<&str> = self.iter().map(|s| s.as_str()).collect();
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Vec(slice),
+ }
+ }
+}
+
+impl<'a> IntoPicker<'a> for Vec<String> {
+ fn to_picker(self) -> Picker<'a, ()> {
+ Picker {
+ route_phantom: PhantomData,
+ args: PickerArgs::Owned(self),
+ }
+ }
+}
+
+// Private helper: shared construction logic for `PickerPattern1`.
+// Both `Picker::pick` and `IntoPicker::pick` delegate to this.
+impl<'a, Route> Picker<'a, Route> {
+ pub(crate) fn build_pattern1<N>(
+ args: PickerArgs<'a>,
+ arg: &'a PickerArg<'a, N>,
+ error_route: Option<Route>,
+ ) -> PickerPattern1<'a, N, Route>
+ where
+ N: Pickable<'a>,
+ {
+ PickerPattern1 {
+ args,
+ error_route,
+ arg_1: arg,
+ result_1: PickerArgResult::Unparsed,
+ route_1: None,
+ default_1: None,
+ post_1: None,
+ }
+ }
+}
diff --git a/arg_picker/src/picker/parse.rs b/arg_picker/src/picker/parse.rs
new file mode 100644
index 0000000..366028b
--- /dev/null
+++ b/arg_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 arg_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().into());
+ }
+ }
+ 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/arg_picker/src/picker/patterns.rs b/arg_picker/src/picker/patterns.rs
new file mode 100644
index 0000000..5806605
--- /dev/null
+++ b/arg_picker/src/picker/patterns.rs
@@ -0,0 +1,205 @@
+use arg_picker_macros::internal_repeat;
+
+use crate::{Pickable, 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(crate) arg_$: &'a PickerArg<'a, T$>,
+ pub(crate) result_$: PickerArgResult<T$>,
+ pub(crate) default_$: Option<Box<dyn FnOnce() -> T$>>,
+ pub(crate) route_$: Option<Box<dyn FnOnce() -> Route>>,
+ pub(crate) 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
+ ///
+ #[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
+ ///
+ #[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
+ ///
+ 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
+ ///
+ #[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_$,
+ +)
+ }
+ }
+
+ /// Picks a new arg with a default value provider in a single call.
+ ///
+ /// This is a shorthand for calling `.pick(arg).or(func)`.
+ ///
+ /// # Example
+ ///
+ #[allow(clippy::type_complexity)]
+ pub fn pick_or<N, F>(self, arg: impl Into<&'a PickerArg<'a, N>>, func: F) -> PickerPattern$+<'a, (T$,+), N, Route>
+ where
+ N: Pickable<'a>,
+ F: FnMut() -> N + 'static,
+ F: 'static,
+ {
+ self.pick(arg).or(func)
+ }
+
+ /// Picks a new arg with a default value in a single call.
+ ///
+ /// This is a shorthand for calling `.pick(arg).or_default()`.
+ ///
+ /// # Example
+ ///
+ #[allow(clippy::type_complexity)]
+ pub fn pick_or_default<N>(self, arg: impl Into<&'a PickerArg<'a, N>>) -> PickerPattern$+<'a, (T$,+), N, Route>
+ where
+ N: Pickable<'a> + Default,
+ {
+ self.pick(arg).or_default()
+ }
+
+ /// Picks a new arg with a route in a single call.
+ ///
+ /// This is a shorthand for calling `.pick(arg).or_route(func)`.
+ ///
+ /// # Example
+ ///
+ #[allow(clippy::type_complexity)]
+ pub fn pick_or_route<N, F>(self, arg: impl Into<&'a PickerArg<'a, N>>, func: F) -> PickerPattern$+<'a, (T$,+), N, Route>
+ where
+ N: Pickable<'a>,
+ F: FnMut() -> Route + 'static,
+ F: 'static,
+ {
+ self.pick(arg).or_route(func)
+ }
+ }
+});
diff --git a/arg_picker/src/picker/result.rs b/arg_picker/src/picker/result.rs
new file mode 100644
index 0000000..83ca2cd
--- /dev/null
+++ b/arg_picker/src/picker/result.rs
@@ -0,0 +1,56 @@
+#![allow(clippy::type_complexity)] // Aha, Type Gymnastics!
+
+use arg_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/arg_picker/src/value.rs b/arg_picker/src/value.rs
new file mode 100644
index 0000000..995aa00
--- /dev/null
+++ b/arg_picker/src/value.rs
@@ -0,0 +1,5 @@
+mod flag;
+pub use flag::*;
+
+mod vec_until;
+pub use vec_until::*;
diff --git a/arg_picker/src/value/flag.rs b/arg_picker/src/value/flag.rs
new file mode 100644
index 0000000..c0673bd
--- /dev/null
+++ b/arg_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 arg_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 arg_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 arg_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/arg_picker/src/value/vec_until.rs b/arg_picker/src/value/vec_until.rs
new file mode 100644
index 0000000..1b79641
--- /dev/null
+++ b/arg_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
+}
diff --git a/arg_picker/test/Cargo.lock b/arg_picker/test/Cargo.lock
new file mode 100644
index 0000000..5d44838
--- /dev/null
+++ b/arg_picker/test/Cargo.lock
@@ -0,0 +1,68 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "arg-picker"
+version = "0.1.0"
+dependencies = [
+ "arg-picker-macros",
+ "just_fmt",
+]
+
+[[package]]
+name = "arg-picker-macros"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "arg-picker-test"
+version = "0.1.0"
+dependencies = [
+ "arg-picker",
+]
+
+[[package]]
+name = "just_fmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6170dccbc3ea15dfb7f2da964097f814aba1dd8f746d4ffc56f33245c38e6d96"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "syn"
+version = "2.0.119"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
diff --git a/arg_picker/test/Cargo.toml b/arg_picker/test/Cargo.toml
new file mode 100644
index 0000000..2c366c7
--- /dev/null
+++ b/arg_picker/test/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "arg-picker-test"
+version = "0.1.0"
+edition = "2024"
+
+[workspace]
+
+[dependencies]
+arg-picker = { path = "../" }
diff --git a/arg_picker/test/src/lib.rs b/arg_picker/test/src/lib.rs
new file mode 100644
index 0000000..0cfa575
--- /dev/null
+++ b/arg_picker/test/src/lib.rs
@@ -0,0 +1,23 @@
+// Using `assert_eq!(x, true)` is clearer than `assert!(x)` for expressing expected values
+//
+// BECAUSE `assert!` only checks if the boolean value is true,
+// while `assert_eq!` explicitly shows the expected value
+#![allow(clippy::bool_assert_comparison)]
+
+#[cfg(test)]
+mod test;
+
+use arg_picker::parselib::MaskedArg;
+
+/// Create a single `MaskedArg` from a raw string and its original index.
+pub fn make_masked(raw: &str, idx: usize) -> MaskedArg<'_> {
+ MaskedArg { raw, raw_idx: idx }
+}
+
+/// Create a `Vec<MaskedArg>` from an array of `(raw, raw_idx)` pairs.
+pub fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec<MaskedArg<'a>> {
+ pairs
+ .iter()
+ .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx })
+ .collect()
+}
diff --git a/arg_picker/test/src/test.rs b/arg_picker/test/src/test.rs
new file mode 100644
index 0000000..9c53514
--- /dev/null
+++ b/arg_picker/test/src/test.rs
@@ -0,0 +1,10 @@
+mod arg_matcher_test;
+mod basic_test;
+mod multi_arg_test;
+mod multi_value_test;
+mod pos_matcher_test;
+mod priority_test;
+mod route_test;
+mod style_test;
+mod value_flag_test;
+mod value_string_test;
diff --git a/arg_picker/test/src/test/arg_matcher_test.rs b/arg_picker/test/src/test/arg_matcher_test.rs
new file mode 100644
index 0000000..d1d363e
--- /dev/null
+++ b/arg_picker/test/src/test/arg_matcher_test.rs
@@ -0,0 +1,289 @@
+use arg_picker::parselib::{ArgMatcher, Matcher, POWERSHELL_STYLE, UNIX_STYLE};
+use arg_picker::PickerArgInfo;
+
+use crate::make_args;
+
+// on_match_one — Named
+
+#[test]
+fn test_match_one_named_basic() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_match_one_named_eq_mode() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name=Alice", 0)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_match_one_named_no_match() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--other", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+#[test]
+fn test_match_one_named_short_flag() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ info.set_short('n');
+ let args = make_args(&[("-n", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_match_one_named_after_end_of_options() {
+ // Flags after `--` should not be matched.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--", 0), ("--name", 1), ("Alice", 2)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+// on_match_one — Positional
+
+#[test]
+fn test_match_one_positional_basic() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("file.txt", 0)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_match_one_positional_takes_first() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("a.txt", 0), ("b.txt", 1)]);
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+// on_match_all — Named, single occurrence
+
+#[test]
+fn test_match_all_named_flag_plus_value() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_match_all_named_eq_mode() {
+ // --name=Alice: value is inline, only tag the flag position.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name=Alice", 0)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_match_all_named_no_value() {
+ // Flag at end with no following arg: only tag the flag.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name", 0)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_match_all_named_value_looks_like_flag() {
+ // The next arg looks like a flag — still tag it.
+ // Validation is the Pickable's responsibility.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name", 0), ("--other", 1)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+// on_match_all — Named, multiple occurrences (Single per flag)
+
+#[test]
+fn test_match_all_named_two_occurrences() {
+ // --name Alice --name Bob → each occurrence gets one value.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name", 0), ("Alice", 1), ("--name", 2), ("Bob", 3)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3]);
+}
+
+#[test]
+fn test_match_all_named_skips_non_matching_args() {
+ // --val a b --val d → only pairs (0,1) and (3,4); idx 2 ("b") left free.
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--val", 3), ("d", 4)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 3, 4]);
+}
+
+// on_match_all — Named, short flag
+
+#[test]
+fn test_match_all_named_short_flag() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ info.set_short('n');
+ let args = make_args(&[("-n", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+// on_match_all — Named, eq + non-eq mixed
+
+#[test]
+fn test_match_all_named_mixed_eq_and_regular() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--name=Alice", 0), ("--name", 1), ("Bob", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2]);
+}
+
+// on_match_all — Named, case insensitive (PowerShell)
+
+#[test]
+fn test_match_all_named_powershell_case_insensitive() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("Name");
+ let args = make_args(&[("-name", 0), ("Alice", 1)]);
+ let result = ArgMatcher::on_match_all(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+// on_match_all — Positional
+
+#[test]
+fn test_match_all_positional_single() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("file.txt", 0)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_match_all_positional_multiple() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("a.txt", 0), ("b.txt", 1)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+// End-of-options marker (`--`)
+
+#[test]
+fn test_match_all_named_stops_at_end_of_options() {
+ // --name before `--` should match, --name after should not.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[
+ ("--name", 0),
+ ("Alice", 1),
+ ("--", 2),
+ ("--name", 3),
+ ("Bob", 4),
+ ]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_match_all_positional_stops_at_end_of_options() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("before", 0), ("--", 1), ("after", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+// Empty args
+
+#[test]
+fn test_match_one_empty() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = vec![];
+ let result = ArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+#[test]
+fn test_match_all_empty() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = vec![];
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// Verify that -- itself is never matched as a flag
+
+#[test]
+fn test_match_all_end_of_options_not_matched() {
+ // `--` should neither match as a flag nor take a value.
+ let mut info = PickerArgInfo::new();
+ info.set_long("name");
+ let args = make_args(&[("--", 0)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// `--` should never be consumed as a value by a named flag.
+
+#[test]
+fn test_match_all_named_does_not_consume_end_marker() {
+ // `--flag` before `--`, but the next position IS `--`.
+ // Only tag the flag, don't consume the end-of-options marker.
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag");
+ let args = make_args(&[("--flag", 0), ("--", 1), ("value", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0], "should NOT consume -- as a value");
+}
+
+#[test]
+fn test_match_all_named_does_not_consume_end_marker_flag_before_end() {
+ // Simulates: --flag -- you where -- is end-of-options.
+ // The flag should be tagged but -- should NOT be consumed as its value.
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag");
+
+ let args = make_args(&[("--flag", 0), ("--", 1), ("you", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result,
+ vec![0],
+ "flag before --: only tag flag, leave -- for positional"
+ );
+}
+
+#[test]
+fn test_match_all_flag_after_end_has_value() {
+ // Flag after `--` should not match at all.
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag");
+ let args = make_args(&[("--", 0), ("--flag", 1), ("value", 2)]);
+ let result = ArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
diff --git a/arg_picker/test/src/test/basic_test.rs b/arg_picker/test/src/test/basic_test.rs
new file mode 100644
index 0000000..b01adb6
--- /dev/null
+++ b/arg_picker/test/src/test/basic_test.rs
@@ -0,0 +1,223 @@
+use arg_picker::{macros::arg, IntoPicker};
+
+// Basic bool flag — present / absent
+
+#[test]
+fn test_bool_flag_present() {
+ let args = vec!["--verbose"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+#[test]
+fn test_bool_flag_absent() {
+ let args: Vec<&str> = vec![];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, false);
+}
+
+// Short flag — '-v'
+
+#[test]
+fn test_bool_short_flag_present() {
+ let args = vec!["-v"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool, 'v'])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+// Multiple bool flags at once
+
+#[test]
+fn test_two_bool_flags_both_present() {
+ // UNIX_STYLE now uses Kebab naming: `flag_a` → "flag-a" → --flag-a
+ let args = vec!["--flag-a", "--flag-b"];
+ let (a, b) = args
+ .to_picker()
+ .pick(&arg![flag_a: bool])
+ .or_default()
+ .pick(&arg![flag_b: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, true);
+ assert_eq!(b, true);
+}
+
+#[test]
+fn test_two_bool_flags_one_present() {
+ let args = vec!["--flag-a"];
+ let (a, b) = args
+ .to_picker()
+ .pick(&arg![flag_a: bool])
+ .or_default()
+ .pick(&arg![flag_b: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, true);
+ assert_eq!(b, false);
+}
+
+#[test]
+fn test_two_bool_flags_neither_present() {
+ let args: Vec<&str> = vec![];
+ let (a, b) = args
+ .to_picker()
+ .pick(&arg![flag_a: bool])
+ .or_default()
+ .pick(&arg![flag_b: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, false);
+ assert_eq!(b, false);
+}
+
+// Mixed short and long flags
+
+#[test]
+fn test_short_and_long_flags() {
+ let args = vec!["-a", "--long-b"];
+ let (a, b) = args
+ .to_picker()
+ .pick(&arg![flag_a: bool, 'a'])
+ .or_default()
+ .pick(&arg![long_b: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, true);
+ assert_eq!(b, true);
+}
+
+// Flags after `--` (end-of-options marker) should not be parsed.
+
+#[test]
+fn test_flag_after_end_of_options() {
+ let args = vec!["--", "--verbose"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, false);
+}
+
+// Alias matching for bool flags
+
+#[test]
+fn test_bool_flag_with_alias() {
+ let args = vec!["--cfg"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![config: bool, "cfg"])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+#[test]
+fn test_bool_flag_primary_name() {
+ let args = vec!["--config"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![config: bool, "cfg"])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+// Short flag + alias for bool flag
+
+#[test]
+fn test_bool_flag_short_and_alias() {
+ let args = vec!["-v"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool, 'v', "cfg"])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+// Default values: .or() / .or_default()
+
+#[test]
+fn test_or_default_without_args() {
+ let args: Vec<&str> = vec![];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, false);
+}
+
+#[test]
+fn test_or_custom_default() {
+ let args: Vec<&str> = vec![];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or(|| true)
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+// to_result / to_option interface
+
+#[test]
+fn test_to_result_ok() {
+ let args = vec!["--verbose"];
+ let result = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .to_result();
+ assert_eq!(result, Ok(true));
+}
+
+#[test]
+fn test_to_option_some() {
+ let args = vec!["--verbose"];
+ let opt = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .to_option();
+ assert_eq!(opt, Some(true));
+}
+
+// Chain with_route passthrough
+
+#[test]
+fn test_with_route_chain() {
+ let args = vec!["--flag"];
+ let parsed = args
+ .with_route::<String>()
+ .pick(&arg![flag: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, true);
+}
+
+// Unrelated flag should not match
+
+#[test]
+fn test_unrelated_flag_does_not_match() {
+ let args = vec!["--other"];
+ let parsed = args
+ .to_picker()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .unwrap();
+ assert_eq!(parsed, false);
+}
diff --git a/arg_picker/test/src/test/multi_arg_test.rs b/arg_picker/test/src/test/multi_arg_test.rs
new file mode 100644
index 0000000..49517f1
--- /dev/null
+++ b/arg_picker/test/src/test/multi_arg_test.rs
@@ -0,0 +1,186 @@
+use arg_picker::parselib::{Matcher, MultiArgMatcher, UNIX_STYLE};
+use arg_picker::PickerArgInfo;
+
+use crate::make_args;
+
+// on_match_one — basic
+
+#[test]
+fn test_multi_one_finds_first_flag() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--other", 0), ("--val", 1), ("a", 2), ("b", 3)]);
+ let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(1));
+}
+
+#[test]
+fn test_multi_one_no_match() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--other", 0)]);
+ let result = MultiArgMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+// on_match_all — named, basic multi-value
+
+#[test]
+fn test_multi_all_flag_takes_all_values_until_next_flag() {
+ // --val a b c → all three values belong to --val
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("c", 3)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3]);
+}
+
+#[test]
+fn test_multi_all_stops_at_next_flag() {
+ // --val a b --other c d → only a,b belong to --val
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("b", 2), ("--other", 3), ("c", 4)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2]);
+}
+
+#[test]
+fn test_multi_all_flag_no_values() {
+ // --val at end with no values → just the flag
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_multi_all_empty() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = vec![];
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// on_match_all — named, multiple occurrences of same flag
+
+#[test]
+fn test_multi_all_two_occurrences() {
+ // --val a b --val c d → two groups
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[
+ ("--val", 0),
+ ("a", 1),
+ ("b", 2),
+ ("--val", 3),
+ ("c", 4),
+ ("d", 5),
+ ]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3, 4, 5]);
+}
+
+#[test]
+fn test_multi_all_skips_non_matching_args() {
+ // --val a --other b --val c → only --val groups
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[
+ ("--val", 0),
+ ("a", 1),
+ ("--other", 2),
+ ("b", 3),
+ ("--val", 4),
+ ("c", 5),
+ ]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 4, 5]);
+}
+
+// on_match_all — named, eq mode
+
+#[test]
+fn test_multi_all_eq_mode() {
+ // --val=a b → eq mode + one extra value
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val=a", 0), ("b", 1)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_multi_all_eq_mode_no_extra() {
+ // --val=a alone → just the eq flag
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val=a", 0)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
+
+#[test]
+fn test_multi_all_mixed_eq_and_regular() {
+ // --val=a b --val c d
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val=a", 0), ("b", 1), ("--val", 2), ("c", 3), ("d", 4)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2, 3, 4]);
+}
+
+// on_match_all — short flag
+
+#[test]
+fn test_multi_all_short_flag() {
+ let mut info = PickerArgInfo::new();
+ info.set_short('v');
+ info.set_long("val");
+ let args = make_args(&[("-v", 0), ("a", 1), ("b", 2)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1, 2]);
+}
+
+// on_match_all — end-of-options marker
+
+#[test]
+fn test_multi_all_stops_at_end_of_options() {
+ // --val a -- b → stops before `--`
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--val", 0), ("a", 1), ("--", 2), ("b", 3)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_multi_all_flag_after_end_ignored() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("val");
+ let args = make_args(&[("--", 0), ("--val", 1), ("a", 2)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// on_match_all — positional
+
+#[test]
+fn test_multi_all_positional() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("a.txt", 0), ("b.txt", 1)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0, 1]);
+}
+
+#[test]
+fn test_multi_all_positional_stops_at_end_of_options() {
+ let mut info = PickerArgInfo::new();
+ info.set_positional(true);
+ let args = make_args(&[("a.txt", 0), ("--", 1), ("b.txt", 2)]);
+ let result = MultiArgMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![0]);
+}
diff --git a/arg_picker/test/src/test/multi_value_test.rs b/arg_picker/test/src/test/multi_value_test.rs
new file mode 100644
index 0000000..cbae645
--- /dev/null
+++ b/arg_picker/test/src/test/multi_value_test.rs
@@ -0,0 +1,66 @@
+use arg_picker::value::{Flag, VecUntil};
+use arg_picker::{macros::arg, IntoPicker};
+
+#[test]
+fn test_vec_until_i16_named() {
+ let (nums, rest): (VecUntil<i16>, Flag) = vec!["--nums", "1", "2", "3", "abc"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .pick(&arg![rest: Flag])
+ .unwrap();
+ assert_eq!(*nums, vec![1i16, 2, 3]);
+ assert_eq!(rest, Flag::Inactive);
+}
+
+#[test]
+fn test_vec_until_i16_stops_at_non_number() {
+ let nums: VecUntil<i16> = vec!["--nums", "42", "abc", "100"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*nums, vec![42i16]);
+}
+
+#[test]
+fn test_vec_until_i16_empty() {
+ let nums: VecUntil<i16> = vec!["--nums"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert!(nums.is_empty());
+}
+
+#[test]
+fn test_vec_until_i16_stops_at_next_flag() {
+ let nums: VecUntil<i16> = vec!["--nums", "1", "2", "--other", "3"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*nums, vec![1i16, 2]);
+}
+
+#[test]
+fn test_vec_until_i16_stops_at_end_of_options() {
+ let nums: VecUntil<i16> = vec!["--nums", "1", "2", "--", "3"]
+ .to_picker()
+ .pick(&arg![nums: VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*nums, vec![1i16, 2]);
+}
+
+// Two VecUntil<T> with different boundary behaviours
+
+#[test]
+fn test_vec_until_f64_and_i16_positional() {
+ // Both are positional VecUntil. f64 takes all valid floats,
+ // i16 takes what's left. But note: "1", "2", "3" are also valid
+ // f64 values, so f64's check_boundary never fires — it consumes
+ // everything, and i16 gets nothing.
+ let (floats, ints): (VecUntil<f64>, VecUntil<i16>) = vec!["1.5", "2.5", "3.5", "1", "2", "3"]
+ .to_picker()
+ .pick(&arg![VecUntil<f64>])
+ .pick(&arg![VecUntil<i16>])
+ .unwrap();
+ assert_eq!(*floats, vec![1.5, 2.5, 3.5]);
+ assert_eq!(*ints, vec![1i16, 2, 3]);
+}
diff --git a/arg_picker/test/src/test/pos_matcher_test.rs b/arg_picker/test/src/test/pos_matcher_test.rs
new file mode 100644
index 0000000..0d4f87e
--- /dev/null
+++ b/arg_picker/test/src/test/pos_matcher_test.rs
@@ -0,0 +1,141 @@
+use arg_picker::parselib::{MaskedArg, Matcher, PositionalMatcher, UNIX_STYLE, WINDOWS_STYLE};
+use arg_picker::PickerArgInfo;
+
+fn make_args<'a>(pairs: &'a [(&'a str, usize)]) -> Vec<MaskedArg<'a>> {
+ pairs
+ .iter()
+ .map(|&(raw, idx)| MaskedArg { raw, raw_idx: idx })
+ .collect()
+}
+
+// on_match_one — basic
+
+#[test]
+fn test_pos_one_takes_first_non_flag() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--verbose", 0), ("file.txt", 1)]);
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(1));
+}
+
+#[test]
+fn test_pos_one_takes_first_if_no_flag() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("file.txt", 0)]);
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_pos_one_all_flags_returns_none() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--verbose", 0), ("--name", 1)]);
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+#[test]
+fn test_pos_one_empty_returns_none() {
+ let info = PickerArgInfo::new();
+ let args = vec![];
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+// on_match_one — after `--`
+
+#[test]
+fn test_pos_one_after_end_takes_even_flag_like() {
+ // After `--`, accept everything including `--verbose`.
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--", 0), ("--verbose", 1), ("file.txt", 2)]);
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, Some(1));
+}
+
+#[test]
+fn test_pos_one_only_end_returns_none() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--", 0)]);
+ let result = PositionalMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+// on_match_one — Windows style prefix
+
+#[test]
+fn test_pos_one_windows_skips_slash_prefix() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("/Verbose", 0), ("file.txt", 1)]);
+ let result = PositionalMatcher::on_match_one(&args, &WINDOWS_STYLE, &info);
+ assert_eq!(result, Some(1));
+}
+
+// on_match_all — basic
+
+#[test]
+fn test_pos_all_collects_non_flags() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--verbose", 0), ("a.txt", 1), ("--name", 2), ("b.txt", 3)]);
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![1, 3]);
+}
+
+#[test]
+fn test_pos_all_only_flags_returns_empty() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--a", 0), ("--b", 1)]);
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+#[test]
+fn test_pos_all_empty() {
+ let info = PickerArgInfo::new();
+ let args = vec![];
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert!(result.is_empty());
+}
+
+// on_match_all — after `--`
+
+#[test]
+fn test_pos_all_after_end_accepts_everything() {
+ // After `--`, even `--verbose` is accepted as positional.
+ let info = PickerArgInfo::new();
+ let args = make_args(&[
+ ("--verbose", 0),
+ ("a.txt", 1),
+ ("--", 2),
+ ("--flag", 3),
+ ("b.txt", 4),
+ ]);
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![1, 3, 4]);
+}
+
+#[test]
+fn test_pos_all_only_after_end() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[("--", 0), ("arg1", 1), ("--arg2", 2)]);
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, vec![1, 2]);
+}
+
+// on_match_all — mixed before/after `--`
+
+#[test]
+fn test_pos_all_mixed() {
+ let info = PickerArgInfo::new();
+ let args = make_args(&[
+ ("infile", 0),
+ ("--verbose", 1),
+ ("--", 2),
+ ("outfile", 3),
+ ("--extra", 4),
+ ]);
+ let result = PositionalMatcher::on_match_all(&args, &UNIX_STYLE, &info);
+ // Before `--`: "infile" (skip --verbose).
+ // After `--`: everything — "outfile", "--extra".
+ assert_eq!(result, vec![0, 3, 4]);
+}
diff --git a/arg_picker/test/src/test/priority_test.rs b/arg_picker/test/src/test/priority_test.rs
new file mode 100644
index 0000000..8f0a452
--- /dev/null
+++ b/arg_picker/test/src/test/priority_test.rs
@@ -0,0 +1,85 @@
+use arg_picker::value::Flag;
+use arg_picker::{macros::arg, IntoPicker};
+
+// Same flag name, different Pickable types
+//
+// PickerArgAttr priority: Flag < Single
+// So Single and Multi should parse BEFORE Flag when sharing
+// the same flag name.
+
+#[test]
+fn test_single_takes_flag_when_sharing_name() {
+ // --name Alice: String consumes it, Flag sees nothing.
+ let (name, verbose): (String, Flag) = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String]) // Single, parsed first
+ .pick(&arg![name: Flag]) // Flag, parsed second, nothing left
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(verbose, Flag::Inactive);
+}
+
+#[test]
+fn test_flag_only_triggers_when_single_missing() {
+ // --verbose: only Flag matches, String gets default.
+ let (name, verbose): (String, Flag) = vec!["--verbose"]
+ .to_picker()
+ .pick(&arg![name: String]) // Single, no match → default ""
+ .or_default()
+ .pick(&arg![name: Flag]) // Flag, no --name in args → Inactive
+ .unwrap();
+ assert_eq!(name, "");
+ assert_eq!(verbose, Flag::Inactive);
+}
+
+#[test]
+fn test_flag_gets_leftovers_after_single_consumes_value() {
+ // --name Alice --name: String takes "--name Alice", Flag takes "--name".
+ let (name, verbose): (String, Flag) = vec!["--name", "Alice", "--name"]
+ .to_picker()
+ .pick(&arg![name: String]) // Single: tag [0, 1], consumes --name Alice
+ .pick(&arg![name: Flag]) // Flag: tags position 2 (--name)
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(
+ verbose,
+ Flag::Active,
+ "Flag should see --name at position 2 after String consumed positions 0-1"
+ );
+}
+
+#[test]
+fn test_short_flag_sharing_same_letter() {
+ // -n Alice: String takes it, Flag misses.
+ let (name, verbose): (String, Flag) = vec!["-n", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String, 'n']) // Single, parsed first
+ .pick(&arg![name: Flag, 'n']) // Flag, parsed second
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(verbose, Flag::Inactive);
+}
+
+#[test]
+fn test_flag_captures_remaining_after_single_partial_consume() {
+ // --name Alice --verbose: String takes --name Alice, Flag takes --verbose.
+ let (name, verbose): (String, Flag) = vec!["--name", "Alice", "--verbose"]
+ .to_picker()
+ .pick(&arg![name: String]) // Single: tag [0, 1]
+ .pick(&arg![verbose: Flag]) // Flag: tag [2]
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(verbose, Flag::Active);
+}
+
+#[test]
+fn test_single_skips_already_claimed_positions() {
+ // --verbose --name Alice: Flag takes --verbose, String takes --name Alice.
+ let (verbose, name): (Flag, String) = vec!["--verbose", "--name", "Alice"]
+ .to_picker()
+ .pick(&arg![verbose: Flag]) // Flag: tag [0]
+ .pick(&arg![name: String]) // Single: tag [1, 2]
+ .unwrap();
+ assert_eq!(verbose, Flag::Active);
+ assert_eq!(name, "Alice");
+}
diff --git a/arg_picker/test/src/test/route_test.rs b/arg_picker/test/src/test/route_test.rs
new file mode 100644
index 0000000..7594c6e
--- /dev/null
+++ b/arg_picker/test/src/test/route_test.rs
@@ -0,0 +1,200 @@
+use arg_picker::{macros::arg, IntoPicker};
+
+// Route mechanism — or_route
+
+#[test]
+fn test_or_route_triggered_to_result() {
+ // flag not present and no default value → route triggered → Err
+ let args: Vec<&str> = vec![];
+ let result: Result<bool, &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "missing_verbose")
+ .to_result();
+ assert_eq!(result, Err("missing_verbose"));
+}
+
+#[test]
+fn test_or_route_not_triggered_when_flag_present() {
+ // flag present → route not triggered → Ok
+ let args = vec!["--verbose"];
+ let result: Result<bool, &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "missing_verbose")
+ .to_result();
+ assert_eq!(result, Ok(true));
+}
+
+#[test]
+fn test_or_default_priority_over_or_route() {
+ // or_default takes priority over or_route: even if the flag is absent,
+ // having a default prevents the route from being triggered
+ let args: Vec<&str> = vec![];
+ let result: Result<bool, &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![verbose: bool])
+ .or_default()
+ .or_route(|| "should_not_reach")
+ .to_result();
+ assert_eq!(result, Ok(false));
+}
+
+#[test]
+fn test_or_route_to_option_returns_none() {
+ // When route is triggered, to_option returns None
+ let args: Vec<&str> = vec![];
+ let opt: Option<bool> = args
+ .with_route::<&'static str>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "missing")
+ .to_option();
+ assert_eq!(opt, None);
+}
+
+#[test]
+#[should_panic(expected = "called `Option::unwrap()` on a `None` value")]
+fn test_or_route_unwrap_panics() {
+ let args: Vec<&str> = vec![];
+ args.with_route::<&'static str>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "missing")
+ .unwrap();
+}
+
+#[test]
+fn test_or_route_with_string_route() {
+ // Route type is String
+ let args: Vec<&str> = vec![];
+ let result: Result<bool, String> = args
+ .with_route::<String>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "route_hit".to_string())
+ .to_result();
+ assert_eq!(result, Err("route_hit".to_string()));
+}
+
+#[test]
+fn test_or_route_with_custom_enum() {
+ // Route type is a custom enum
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
+ #[allow(dead_code)]
+ enum Redirect {
+ Help,
+ Version,
+ }
+
+ let args: Vec<&str> = vec![];
+ let result: Result<bool, Redirect> = args
+ .with_route::<Redirect>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| Redirect::Help)
+ .to_result();
+ assert_eq!(result, Err(Redirect::Help));
+}
+
+// Route mechanism — multiple flags
+
+#[test]
+fn test_two_flags_first_missing_triggers_route() {
+ // Both flags missing; first has or_route → Err("first");
+ // first route wins (first-checked, first-triggered)
+ let args: Vec<&str> = vec![];
+ let result: Result<(bool, bool), &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![flag_a: bool])
+ .or_route(|| "first_missing")
+ .pick(&arg![flag_b: bool])
+ .or_route(|| "second_missing")
+ .to_result();
+ assert_eq!(result, Err("first_missing"));
+}
+
+#[test]
+fn test_two_flags_second_missing_triggers_route() {
+ // First has or_default (no route triggered), second missing with or_route → Err("second")
+ let args: Vec<&str> = vec![];
+ let result: Result<(bool, bool), &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![flag_a: bool])
+ .or_default()
+ .pick(&arg![flag_b: bool])
+ .or_route(|| "second_missing")
+ .to_result();
+ assert_eq!(result, Err("second_missing"));
+}
+
+#[test]
+fn test_two_flags_both_present_route_not_triggered() {
+ // Both flags present → route not triggered → Ok((true, true))
+ let args = vec!["--flag-a", "--flag-b"];
+ let result: Result<(bool, bool), &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![flag_a: bool])
+ .or_route(|| "first_missing")
+ .pick(&arg![flag_b: bool])
+ .or_route(|| "second_missing")
+ .to_result();
+ assert_eq!(result, Ok((true, true)));
+}
+
+#[test]
+fn test_two_flags_first_missing_no_route_second_has_route() {
+ // First missing but has no or_route, second missing with or_route → Err("second")
+ // Note: the first has neither route nor default, so pick failure does not modify error_route
+ let args: Vec<&str> = vec![];
+ let result: Result<(bool, bool), &'static str> = args
+ .with_route::<&'static str>()
+ .pick(&arg![flag_a: bool])
+ // No or_default, no or_route either
+ .pick(&arg![flag_b: bool])
+ .or_route(|| "second_missing")
+ .to_result();
+ assert_eq!(result, Err("second_missing"));
+}
+
+#[test]
+fn test_two_flags_only_second_has_default() {
+ // First is missing with no default/route (v1=NotFound), second is missing but has or_default
+ // Use unpack to check both results
+ let args: Vec<&str> = vec![];
+ let (a, b) = args
+ .to_picker()
+ .pick(&arg![flag_a: bool])
+ .pick(&arg![flag_b: bool])
+ .or_default()
+ .unpack();
+ assert_eq!(a, None);
+ assert_eq!(b, Some(false));
+}
+
+// Route with with_route
+
+#[test]
+fn test_with_route_and_or_route() {
+ // with_route::&lt;String&gt;() sets the Route type + or_route triggers
+ let args: Vec<&str> = vec![];
+ let result: Result<bool, String> = args
+ .with_route::<String>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "redirected".to_string())
+ .to_result();
+ assert_eq!(result, Err("redirected".to_string()));
+}
+
+#[test]
+fn test_with_route_type_switch() {
+ // with_route switching Route type clears old route_$ and error_route
+ let args: Vec<&str> = vec![];
+ let result: Result<(bool, bool), i32> = args
+ .with_route::<String>()
+ .pick(&arg![verbose: bool])
+ .or_route(|| "string_route".to_string())
+ .with_route::<i32>()
+ .pick(&arg![other: bool])
+ .or_route(|| -1)
+ .to_result();
+ // The first flag is missing, but its or_route is cleared when with_route switches (route_$ = None)
+ // The second flag is missing and has or_route → Err(-1)
+ assert_eq!(result, Err(-1));
+}
diff --git a/arg_picker/test/src/test/style_test.rs b/arg_picker/test/src/test/style_test.rs
new file mode 100644
index 0000000..b470d4f
--- /dev/null
+++ b/arg_picker/test/src/test/style_test.rs
@@ -0,0 +1,300 @@
+use arg_picker::parselib::{
+ build_possible_flags, FlagMatcher, Matcher, ParserStyle, ParserStyleNamingCase,
+ POWERSHELL_STYLE, UNIX_STYLE, WINDOWS_STYLE,
+};
+use arg_picker::PickerArgInfo;
+
+use crate::make_masked;
+
+// Style: formatting utilities
+
+#[test]
+fn test_unix_style_flag_string() {
+ assert_eq!(UNIX_STYLE.flag_string('v'), "-v");
+ assert_eq!(UNIX_STYLE.flag_string("verbose"), "--verbose");
+}
+
+#[test]
+fn test_windows_style_flag_string() {
+ assert_eq!(WINDOWS_STYLE.flag_string('v'), "/v");
+ assert_eq!(WINDOWS_STYLE.flag_string("verbose"), "/verbose");
+}
+
+#[test]
+fn test_powershell_style_flag_string() {
+ assert_eq!(POWERSHELL_STYLE.flag_string('v'), "-v");
+ assert_eq!(POWERSHELL_STYLE.flag_string("Verbose"), "-Verbose");
+}
+
+#[test]
+fn test_build_possible_flags_windows() {
+ // Build PickerArgInfo from a flag definition: `verbose: bool`
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+ let flags = build_possible_flags(&WINDOWS_STYLE, &info);
+ assert_eq!(flags, vec!["/Verbose"]);
+}
+
+#[test]
+fn test_build_possible_flags_with_short_and_alias() {
+ let mut info = PickerArgInfo::new();
+ info.set_short('n');
+ info.set_long("name");
+ info.set_alias(vec!["nickname"]);
+ let flags = build_possible_flags(&UNIX_STYLE, &info);
+ assert_eq!(flags, vec!["-n", "--name", "--nickname"]);
+}
+
+// Style: matching with different styles via Matcher trait
+
+#[test]
+fn test_windows_style_match() {
+ // Windows style: /verbose (case insensitive)
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![make_masked("/verbose", 0)];
+ let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_windows_style_match_case_insensitive() {
+ // Windows style is case-insensitive: /VERBOSE should match "verbose"
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![make_masked("/VERBOSE", 0)];
+ let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_windows_style_no_match_on_unrelated_flag() {
+ // Different flag should not match
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![make_masked("/output", 0)];
+ let result = FlagMatcher::on_match_one(&args, &WINDOWS_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+#[test]
+fn test_powershell_style_match() {
+ // PowerShell style: -Verbose (case insensitive)
+ let mut info = PickerArgInfo::new();
+ info.set_long("Verbose");
+
+ let args = vec![make_masked("-Verbose", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_powershell_style_match_case_insensitive() {
+ let mut info = PickerArgInfo::new();
+ info.set_long("Verbose");
+
+ let args = vec![make_masked("-VERBOSE", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(result, Some(0));
+}
+
+#[test]
+fn test_unix_style_case_sensitive_no_match() {
+ // UNIX style is case-sensitive: --VERBOSE should NOT match --verbose
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![make_masked("--VERBOSE", 0)];
+ let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(result, None);
+}
+
+#[test]
+fn test_windows_style_match_all() {
+ // on_match_all should find all matching flags
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+ info.set_short('v');
+
+ let args = vec![
+ make_masked("/v", 0),
+ make_masked("/output", 1),
+ make_masked("/VERBOSE", 2),
+ ];
+ let result = FlagMatcher::on_match_all(&args, &WINDOWS_STYLE, &info);
+ assert_eq!(result, vec![0, 2]);
+}
+
+#[test]
+fn test_windows_style_match_after_end_of_options() {
+ // end_of_options is always "--" regardless of style
+ // Flags after -- should not match
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![
+ make_masked("/verbose", 0),
+ make_masked("--", 1),
+ make_masked("/verbose", 2),
+ ];
+ let result = FlagMatcher::on_match_all(&args, &WINDOWS_STYLE, &info);
+ // end_of_options is always "--" regardless of style
+ assert_eq!(result, vec![0]);
+}
+
+// Naming case conversion
+
+/// A Unix-like style with kebab-case naming.
+const KEBAB_STYLE: ParserStyle<'static> = ParserStyle {
+ naming_case: ParserStyleNamingCase::Kebab,
+ ..UNIX_STYLE
+};
+
+#[test]
+fn test_kebab_case_naming_for_multiword_flag() {
+ // flag name `flag_a` → Kebab → `flag-a` → `--flag-a`
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag_a");
+
+ let args = vec![make_masked("--flag-a", 0)];
+ let result = FlagMatcher::on_match_one(&args, &KEBAB_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "--flag-a should match flag_a via kebab-case conversion"
+ );
+}
+
+#[test]
+fn test_snake_case_should_not_match_as_long_flag() {
+ // With kebab naming, `--flag_a` (snake) should NOT match `flag_a`
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag_a");
+
+ let args = vec![make_masked("--flag_a", 0)];
+ let result = FlagMatcher::on_match_one(&args, &KEBAB_STYLE, &info);
+ assert_eq!(
+ result, None,
+ "--flag_a should NOT match in kebab-style context"
+ );
+}
+
+#[test]
+fn test_kebab_case_naming_for_unix_style() {
+ // UNIX_STYLE now uses Kebab case: `flag_a` → `flag-a` → `--flag-a`
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag_a");
+
+ let args = vec![make_masked("--flag-a", 0)];
+ let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "--flag-a should match with kebab-style UNIX_STYLE"
+ );
+}
+
+#[test]
+fn test_snake_case_rejected_by_unix_style() {
+ // UNIX_STYLE uses Kebab: `--flag_a` (snake) should NOT match
+ let mut info = PickerArgInfo::new();
+ info.set_long("flag_a");
+
+ let args = vec![make_masked("--flag_a", 0)];
+ let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result, None,
+ "--flag_a should NOT match under kebab-style UNIX_STYLE"
+ );
+}
+
+#[test]
+fn test_powershell_pascal_case_naming() {
+ // POWERSHELL_STYLE uses Pascal case: `verbose` → `Verbose` → `-Verbose`
+ let mut info = PickerArgInfo::new();
+ info.set_long("verbose");
+
+ let args = vec![make_masked("-Verbose", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "-Verbose should match verbose via Pascal case"
+ );
+}
+
+#[test]
+fn test_flag_naming_kebab_matches_my_name() {
+ // UNIX_STYLE now uses Kebab: `my_name` → `my-name` → `--my-name`
+ let mut info = PickerArgInfo::new();
+ info.set_long("my_name");
+
+ let args = vec![make_masked("--my-name", 0)];
+ let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "--my-name should match via kebab conversion"
+ );
+}
+
+#[test]
+fn test_flag_naming_kebab_rejects_my_name_underscore() {
+ // Kebab style: `--my_name` (snake) should NOT match
+ let mut info = PickerArgInfo::new();
+ info.set_long("my_name");
+
+ let args = vec![make_masked("--my_name", 0)];
+ let result = FlagMatcher::on_match_one(&args, &UNIX_STYLE, &info);
+ assert_eq!(
+ result, None,
+ "--my_name should NOT match under kebab-style UNIX_STYLE"
+ );
+}
+
+#[test]
+fn test_flag_naming_pascal_matches_my_name() {
+ // `my_name` under Pascal → `MyName` → `-MyName`
+ let mut info = PickerArgInfo::new();
+ info.set_long("my_name");
+
+ let args = vec![make_masked("-MyName", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "-MyName should match via Pascal conversion"
+ );
+}
+
+#[test]
+fn test_flag_naming_pascal_matches_lowercase() {
+ // PowerShell is case-insensitive: `-myname` should also match
+ let mut info = PickerArgInfo::new();
+ info.set_long("my_name");
+
+ let args = vec![make_masked("-myname", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(
+ result,
+ Some(0),
+ "-myname (lowercase) should match via case-insensitive Pascal"
+ );
+}
+
+#[test]
+fn test_flag_naming_pascal_rejects_my_name_underscore() {
+ // Pascal style: `-my_name` (underscore) should NOT match
+ let mut info = PickerArgInfo::new();
+ info.set_long("my_name");
+
+ let args = vec![make_masked("-my_name", 0)];
+ let result = FlagMatcher::on_match_one(&args, &POWERSHELL_STYLE, &info);
+ assert_eq!(
+ result, None,
+ "-my_name should NOT match under Pascal-style naming"
+ );
+}
diff --git a/arg_picker/test/src/test/value_flag_test.rs b/arg_picker/test/src/test/value_flag_test.rs
new file mode 100644
index 0000000..3df053e
--- /dev/null
+++ b/arg_picker/test/src/test/value_flag_test.rs
@@ -0,0 +1,174 @@
+use arg_picker::value::Flag;
+use arg_picker::{macros::arg, IntoPicker};
+
+// Basic Flag — present / absent
+
+#[test]
+fn test_flag_present() {
+ let flag: Flag = vec!["--verbose"]
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .unwrap();
+ assert_eq!(flag, Flag::Active);
+}
+
+#[test]
+fn test_flag_absent_returns_inactive() {
+ // Unlike bool, Flag::pick returns Parsed(Inactive) when no match is found,
+ // so or_default() is NOT required — unwrap() works directly.
+ let flag: Flag = Vec::<&str>::new()
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .unwrap();
+ assert_eq!(flag, Flag::Inactive);
+}
+
+// Short Flag
+
+#[test]
+fn test_flag_short_present() {
+ let flag: Flag = vec!["-v"]
+ .to_picker()
+ .pick(&arg![verbose: Flag, 'v'])
+ .unwrap();
+ assert_eq!(flag, Flag::Active);
+}
+
+// Multiple Flags
+
+#[test]
+fn test_two_flags_both_present() {
+ let (a, b): (Flag, Flag) = vec!["--flag-a", "--flag-b"]
+ .to_picker()
+ .pick(&arg![flag_a: Flag])
+ .pick(&arg![flag_b: Flag])
+ .unwrap();
+ assert_eq!(a, Flag::Active);
+ assert_eq!(b, Flag::Active);
+}
+
+#[test]
+fn test_two_flags_one_present() {
+ let (a, b): (Flag, Flag) = vec!["--flag-a"]
+ .to_picker()
+ .pick(&arg![flag_a: Flag])
+ .pick(&arg![flag_b: Flag])
+ .unwrap();
+ assert_eq!(a, Flag::Active);
+ assert_eq!(b, Flag::Inactive);
+}
+
+#[test]
+fn test_two_flags_neither_present() {
+ let (a, b): (Flag, Flag) = Vec::<&str>::new()
+ .to_picker()
+ .pick(&arg![flag_a: Flag])
+ .pick(&arg![flag_b: Flag])
+ .unwrap();
+ assert_eq!(a, Flag::Inactive);
+ assert_eq!(b, Flag::Inactive);
+}
+
+// After `--` (end-of-options)
+
+#[test]
+fn test_flag_after_end_of_options() {
+ let flag: Flag = vec!["--", "--verbose"]
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .unwrap();
+ assert_eq!(flag, Flag::Inactive);
+}
+
+// Alias
+
+#[test]
+fn test_flag_with_alias() {
+ let flag: Flag = vec!["--cfg"]
+ .to_picker()
+ .pick(&arg![config: Flag, "cfg"])
+ .unwrap();
+ assert_eq!(flag, Flag::Active);
+}
+
+#[test]
+fn test_flag_primary_name() {
+ let flag: Flag = vec!["--config"]
+ .to_picker()
+ .pick(&arg![config: Flag, "cfg"])
+ .unwrap();
+ assert_eq!(flag, Flag::Active);
+}
+
+// Unrelated flag should not match
+
+#[test]
+fn test_unrelated_flag_does_not_match() {
+ let flag: Flag = vec!["--other"]
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .unwrap();
+ assert_eq!(flag, Flag::Inactive);
+}
+
+// to_result / to_option
+
+#[test]
+fn test_flag_to_result() {
+ let result: Result<Flag, ()> = vec!["--verbose"]
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .to_result();
+ assert_eq!(result, Ok(Flag::Active));
+}
+
+#[test]
+fn test_flag_to_option() {
+ let opt: Option<Flag> = vec!["--verbose"]
+ .to_picker()
+ .pick(&arg![verbose: Flag])
+ .to_option();
+ assert_eq!(opt, Some(Flag::Active));
+}
+
+// Bool conversions
+
+#[test]
+fn test_flag_converts_to_bool() {
+ let flag = Flag::Active;
+ assert!(bool::from(flag));
+
+ let flag = Flag::Inactive;
+ assert!(!bool::from(flag));
+}
+
+#[test]
+fn test_flag_from_bool() {
+ assert_eq!(Flag::from(true), Flag::Active);
+ assert_eq!(Flag::from(false), Flag::Inactive);
+}
+
+#[test]
+fn test_flag_deref_to_bool() {
+ let active = Flag::Active;
+ assert!(*active);
+
+ let inactive = Flag::Inactive;
+ assert!(!*inactive);
+}
+
+// Flag never triggers route (unlike bool)
+//
+// Flag::pick always returns Parsed, so the fallback chain
+// (default → route) is never entered.
+
+#[test]
+fn test_flag_absent_does_not_trigger_route() {
+ // Even without or_default / or_route, absent flag returns Inactive, not a route
+ let result: Result<Flag, &str> = Vec::<&str>::new()
+ .with_route::<&str>()
+ .pick(&arg![verbose: Flag])
+ .or_route(|| "should_not_fire")
+ .to_result();
+ assert_eq!(result, Ok(Flag::Inactive));
+}
diff --git a/arg_picker/test/src/test/value_string_test.rs b/arg_picker/test/src/test/value_string_test.rs
new file mode 100644
index 0000000..2c3b50b
--- /dev/null
+++ b/arg_picker/test/src/test/value_string_test.rs
@@ -0,0 +1,171 @@
+use arg_picker::{macros::arg, IntoPicker};
+
+// Basic named String — present / absent
+
+#[test]
+fn test_string_named_present() {
+ let val: String = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "Alice");
+}
+
+#[test]
+fn test_string_named_absent_uses_default() {
+ let val: String = Vec::<&str>::new()
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// Named String — eq mode
+
+#[test]
+fn test_string_named_eq_mode() {
+ let val: String = vec!["--name=Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "Alice");
+}
+
+// Named String — short flag
+
+#[test]
+fn test_string_named_short_flag() {
+ let val: String = vec!["-n", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String, 'n'])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "Alice");
+}
+
+// Named String — no value after flag
+
+#[test]
+fn test_string_named_missing_value_triggers_default() {
+ // --name at end with no following arg → pick returns NotFound → or_default gives ""
+ let val: String = vec!["--name"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// Positional String
+
+#[test]
+fn test_string_positional() {
+ let val: String = vec!["file.txt"]
+ .to_picker()
+ .pick(&arg![String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "file.txt");
+}
+
+#[test]
+fn test_string_positional_takes_first() {
+ let val: String = vec!["first", "second"]
+ .to_picker()
+ .pick(&arg![String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "first");
+}
+
+// Multiple occurrences (Single only tags one occurrence per Parseable)
+
+#[test]
+fn test_string_two_named_flags() {
+ let (a, b): (String, String) = vec!["--name", "Alice", "--greeting", "Hello"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .pick(&arg![greeting: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(a, "Alice");
+ assert_eq!(b, "Hello");
+}
+
+// Mixed named + positional
+
+#[test]
+fn test_string_named_and_positional() {
+ let (name, file): (String, String) = vec!["--name", "Alice", "file.txt"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .pick(&arg![String])
+ .or_default()
+ .unwrap();
+ assert_eq!(name, "Alice");
+ assert_eq!(file, "file.txt");
+}
+
+// After `--` (end-of-options)
+
+#[test]
+fn test_string_named_after_end_of_options() {
+ // Named arg after `--` should not be matched → default
+ let val: String = vec!["--", "--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// Unrelated flag should not match → default
+
+#[test]
+fn test_string_unrelated_flag() {
+ let val: String = vec!["--other", "value"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .unwrap();
+ assert_eq!(val, "");
+}
+
+// to_result / to_option
+
+#[test]
+fn test_string_to_result() {
+ let result: Result<String, ()> = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .to_result();
+ assert_eq!(result, Ok("Alice".to_string()));
+}
+
+#[test]
+fn test_string_to_option() {
+ let opt: Option<String> = vec!["--name", "Alice"]
+ .to_picker()
+ .pick(&arg![name: String])
+ .or_default()
+ .to_option();
+ assert_eq!(opt, Some("Alice".to_string()));
+}
+
+// Custom default via .or()
+
+#[test]
+fn test_string_custom_default() {
+ let val: String = Vec::<&str>::new()
+ .to_picker()
+ .pick(&arg![name: String])
+ .or(|| "default_name".to_string())
+ .unwrap();
+ assert_eq!(val, "default_name");
+}