aboutsummaryrefslogtreecommitdiff
path: root/mingling_picker/src/parselib/arg_matcher.rs
blob: 38bb9cc0b8bf2c364c79db326d4abb992510fe35 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
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
    }
}