diff options
Diffstat (limited to 'arg_picker/src/parselib')
| -rw-r--r-- | arg_picker/src/parselib/arg_matcher.rs | 142 | ||||
| -rw-r--r-- | arg_picker/src/parselib/flag_matcher.rs | 82 | ||||
| -rw-r--r-- | arg_picker/src/parselib/multi_arg_matcher.rs | 141 | ||||
| -rw-r--r-- | arg_picker/src/parselib/pos_matcher.rs | 71 | ||||
| -rw-r--r-- | arg_picker/src/parselib/single_matcher.rs | 59 | ||||
| -rw-r--r-- | arg_picker/src/parselib/style.rs | 258 | ||||
| -rw-r--r-- | arg_picker/src/parselib/utils.rs | 276 |
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() +} |
