aboutsummaryrefslogtreecommitdiff
path: root/arg_picker/src/parselib
diff options
context:
space:
mode:
Diffstat (limited to 'arg_picker/src/parselib')
-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
7 files changed, 1029 insertions, 0 deletions
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()
+}