intelli_shell/utils/
variable.rs

1use std::sync::LazyLock;
2
3use regex::{CaptureMatches, Captures, Regex};
4
5const VARIABLE_REGEX: &str = r"\{\{((?:\{[^}]+\}|[^}]+))\}\}";
6
7/// Regex to match variables from a command, with a capturing group for the name
8pub static COMMAND_VARIABLE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(VARIABLE_REGEX).unwrap());
9
10/// Regex to match variables from a command, with a capturing group for the name.
11///
12/// This regex identifies if variables are unquoted, single-quoted, or double-quoted:
13/// - Group 1: Will exist if a single-quoted placeholder like '{{name}}' is matched
14/// - Group 2: Will exist if a double-quoted placeholder like "{{name}}" is matched
15/// - Group 3: Will exist if an unquoted placeholder like {{name}} is matched
16pub static COMMAND_VARIABLE_REGEX_QUOTES: LazyLock<Regex> =
17    LazyLock::new(|| Regex::new(&format!(r#"'{VARIABLE_REGEX}'|"{VARIABLE_REGEX}"|{VARIABLE_REGEX}"#)).unwrap());
18
19/// An iterator that splits a text string based on a regular expression, yielding both the substrings that _don't_ match
20/// the regex and the `Captures` objects for the parts that _do_ match.
21///
22/// This is useful when you need to process parts of a string separated by delimiters defined by a regex, but you also
23/// need access to the captured groups within those delimiters.
24///
25/// # Examples
26///
27/// ```rust
28/// # use intelli_shell::utils::{SplitCaptures, SplitItem};
29/// # use regex::Regex;
30/// let regex = Regex::new(r"\{(\w+)\}").unwrap();
31/// let text = "Hello {name}, welcome to {place}!";
32/// let mut parts = vec![];
33/// for item in SplitCaptures::new(&regex, text) {
34///     match item {
35///         SplitItem::Unmatched(s) => parts.push(format!("Unmatched: '{}'", s)),
36///         SplitItem::Captured(caps) => {
37///             parts.push(format!("Captured: '{}', Group 1: '{}'", &caps[0], &caps[1]))
38///         }
39///     }
40/// }
41/// assert_eq!(
42///     parts,
43///     vec![
44///         "Unmatched: 'Hello '",
45///         "Captured: '{name}', Group 1: 'name'",
46///         "Unmatched: ', welcome to '",
47///         "Captured: '{place}', Group 1: 'place'",
48///         "Unmatched: '!'",
49///     ]
50/// );
51/// ```
52pub struct SplitCaptures<'r, 't> {
53    /// Iterator over regex captures
54    finder: CaptureMatches<'r, 't>,
55    /// The original text being split
56    text: &'t str,
57    /// The byte index marking the end of the last match/unmatched part
58    last: usize,
59    /// Holds the captures of the _next_ match to be returned
60    caps: Option<Captures<'t>>,
61}
62
63impl<'r, 't> SplitCaptures<'r, 't> {
64    /// Creates a new [SplitCaptures] iterator.
65    pub fn new(regex: &'r Regex, text: &'t str) -> SplitCaptures<'r, 't> {
66        SplitCaptures {
67            finder: regex.captures_iter(text),
68            text,
69            last: 0,
70            caps: None,
71        }
72    }
73}
74
75/// Represents an item yielded by the [SplitCaptures] iterator.
76///
77/// It can be either a part of the string that did not match the regex ([Unmatched](SplitItem::Unmatched)) or the
78/// [Captures] object from a part that did match ([Captured](SplitItem::Captured)).
79#[derive(Debug)]
80pub enum SplitItem<'t> {
81    /// A string slice that did not match the regex separator
82    Unmatched(&'t str),
83    /// The [Captures] object resulting from a regex match
84    Captured(Captures<'t>),
85}
86
87impl<'t> Iterator for SplitCaptures<'_, 't> {
88    type Item = SplitItem<'t>;
89
90    /// Advances the iterator, returning the next unmatched slice or captured group.
91    ///
92    /// The iterator alternates between returning `SplitItem::Unmatched` and `SplitItem::Captured`,
93    /// starting and ending with `Unmatched` (unless the string is empty or fully matched).
94    fn next(&mut self) -> Option<SplitItem<'t>> {
95        // If we have pending captures from the previous iteration, return them now
96        if let Some(caps) = self.caps.take() {
97            return Some(SplitItem::Captured(caps));
98        }
99        // Find the next match using the internal captures iterator
100        match self.finder.next() {
101            // No more matches found
102            None => {
103                if self.last >= self.text.len() {
104                    None
105                } else {
106                    // If there's remaining text after the last match (or if the string was never matched), return it.
107                    // Get the final unmatched slice
108                    let s = &self.text[self.last..];
109                    // Mark the end of the string as processed
110                    self.last = self.text.len();
111                    Some(SplitItem::Unmatched(s))
112                }
113            }
114            // A match was found
115            Some(caps) => {
116                // Get the match bounds
117                let m = caps.get(0).unwrap();
118                // Extract the text between the end of the last item and the start of this match
119                let unmatched = &self.text[self.last..m.start()];
120                // Update the position to the end of the current match
121                self.last = m.end();
122                // Store the captures to be returned in the next iteration
123                self.caps = Some(caps);
124                // Return the unmatched part before the captures
125                Some(SplitItem::Unmatched(unmatched))
126            }
127        }
128    }
129}