Skip to main content

double_o/pattern/
mod.rs

1use regex::Regex;
2use std::sync::LazyLock;
3
4// Public API re-exports
5pub use self::builtins::builtin_patterns;
6pub use self::toml::{FailureSection, PatternFile, load_user_patterns, parse_pattern_str};
7
8// Internal re-export for learn module
9#[doc(hidden)]
10pub use self::toml::validate_pattern_regexes;
11
12/// Get a reference to the static built-in patterns.
13pub fn builtins() -> &'static [Pattern] {
14    &BUILTINS
15}
16
17// ---------------------------------------------------------------------------
18// Types
19// ---------------------------------------------------------------------------
20
21/// A pattern for matching and extracting information from command output.
22///
23/// Patterns define how to compress command output using regex matching.
24/// When a command matches the `command_match` regex, the pattern's
25/// success or failure logic is applied to extract compressed output.
26pub struct Pattern {
27    /// Regex that matches the command line (e.g., `r"cargo test"`).
28    pub command_match: Regex,
29
30    /// Optional pattern for extracting a summary from successful command output.
31    pub success: Option<SuccessPattern>,
32
33    /// Optional strategy for filtering failed command output.
34    pub failure: Option<FailurePattern>,
35}
36
37/// Pattern for extracting a summary from successful command output.
38///
39/// Uses a strategy-based approach to handle different extraction methods:
40/// - Regex with template formatting (legacy)
41/// - Tail/head line extraction
42/// - Grep filtering
43pub struct SuccessPattern {
44    /// Strategy for extracting success output.
45    pub strategy: SuccessStrategy,
46}
47
48/// Strategy for filtering failed command output.
49///
50/// When a command exits with a non-zero status, the failure strategy
51/// extracts relevant error information (e.g., tail N lines, head N lines,
52/// grep for error keywords, or extract text between delimiters).
53pub struct FailurePattern {
54    /// The strategy to apply for extracting error information.
55    pub strategy: FailureStrategy,
56}
57
58/// Strategy for extracting error information from failed command output.
59///
60/// Each variant defines a different approach to identifying and extracting
61/// the most relevant error information from command output.
62pub enum FailureStrategy {
63    /// Keep the last N lines of output (tail).
64    Tail {
65        /// Number of lines to keep from the end.
66        lines: usize,
67    },
68
69    /// Keep the first N lines of output (head).
70    Head {
71        /// Number of lines to keep from the start.
72        lines: usize,
73    },
74
75    /// Filter lines matching a regex pattern.
76    Grep {
77        /// Regex pattern to match error lines.
78        pattern: Regex,
79    },
80
81    /// Extract text between two delimiter strings.
82    Between {
83        /// Starting delimiter string.
84        start: String,
85
86        /// Ending delimiter string.
87        end: String,
88    },
89}
90
91/// Strategy for extracting success output.
92///
93/// Mirrors failure strategies but for successful command output.
94/// Used when a command succeeds with large output and a pattern matches.
95pub enum SuccessStrategy {
96    /// Legacy format: regex with named capture groups + summary template.
97    Regex {
98        /// Regex with named capture groups for extracting values.
99        pattern: Regex,
100        /// Template string with `{name}` placeholders for summary formatting.
101        summary: String,
102    },
103
104    /// Keep the last N lines of output (tail).
105    Tail {
106        /// Number of lines to keep from the end.
107        lines: usize,
108    },
109
110    /// Keep the first N lines of output (head).
111    Head {
112        /// Number of lines to keep from the start.
113        lines: usize,
114    },
115
116    /// Filter lines matching a regex pattern.
117    Grep {
118        /// Regex pattern to match lines.
119        pattern: Regex,
120    },
121}
122
123// ---------------------------------------------------------------------------
124// Matching & extraction
125// ---------------------------------------------------------------------------
126
127/// Extract lines matching a regex pattern.
128///
129/// Shared helper for both success and failure grep strategies.
130fn extract_grep(output: &str, pattern: &Regex) -> String {
131    let mut result = String::new();
132    let mut first = true;
133    for line in output.lines() {
134        if pattern.is_match(line) {
135            if !first {
136                result.push('\n');
137            }
138            result.push_str(line);
139            first = false;
140        }
141    }
142    result
143}
144
145/// Extract the last N lines from output.
146fn extract_tail(output: &str, lines: usize) -> Option<String> {
147    let all: Vec<&str> = output.lines().collect();
148    let start = all.len().saturating_sub(lines);
149    if start >= all.len() {
150        None
151    } else {
152        Some(all[start..].join("\n"))
153    }
154}
155
156/// Extract the first N lines from output.
157fn extract_head(output: &str, lines: usize) -> Option<String> {
158    let all: Vec<&str> = output.lines().collect();
159    let end = lines.min(all.len());
160    if end == 0 {
161        None
162    } else {
163        Some(all[..end].join("\n"))
164    }
165}
166
167/// Find the first pattern whose `command_match` matches `command`.
168pub fn find_matching<'a>(command: &str, patterns: &'a [Pattern]) -> Option<&'a Pattern> {
169    patterns.iter().find(|p| p.command_match.is_match(command))
170}
171
172/// Like `find_matching` but works with a slice of references.
173///
174/// Useful when you have a slice of pattern references rather than values.
175pub fn find_matching_ref<'a>(command: &str, patterns: &[&'a Pattern]) -> Option<&'a Pattern> {
176    patterns
177        .iter()
178        .find(|p| p.command_match.is_match(command))
179        .copied()
180}
181
182/// Apply a success pattern to output, returning the formatted summary if it matches.
183pub fn extract_summary(pat: &SuccessPattern, output: &str) -> Option<String> {
184    match &pat.strategy {
185        SuccessStrategy::Regex { pattern, summary } => {
186            let caps = pattern.captures(output)?;
187            let mut result = String::with_capacity(summary.len() + output.len());
188            let mut i = 0;
189            while i < summary.len() {
190                if let Some(j) = summary[i..].find('{') {
191                    result.push_str(&summary[i..i + j]);
192                    i += j + 1;
193                    if let Some(k) = summary[i..].find('}') {
194                        let placeholder = &summary[i..i + k];
195                        if let Some(m) = caps.name(placeholder) {
196                            result.push_str(m.as_str());
197                        } else {
198                            result.push('{');
199                            result.push_str(placeholder);
200                            result.push('}');
201                        }
202                        i += k + 1;
203                    } else {
204                        result.push('{');
205                        result.push_str(&summary[i..]);
206                        break;
207                    }
208                } else {
209                    result.push_str(&summary[i..]);
210                    break;
211                }
212            }
213            Some(result)
214        }
215        SuccessStrategy::Tail { lines } => extract_tail(output, *lines),
216        SuccessStrategy::Head { lines } => extract_head(output, *lines),
217        SuccessStrategy::Grep { pattern } => {
218            let result = extract_grep(output, pattern);
219            if result.is_empty() {
220                None
221            } else {
222                Some(result)
223            }
224        }
225    }
226}
227
228/// Apply a failure strategy to extract actionable output.
229pub fn extract_failure(pat: &FailurePattern, output: &str) -> String {
230    match &pat.strategy {
231        FailureStrategy::Tail { lines } => {
232            let all: Vec<&str> = output.lines().collect();
233            let start = all.len().saturating_sub(*lines);
234            all[start..].join("\n")
235        }
236        FailureStrategy::Head { lines } => {
237            let all: Vec<&str> = output.lines().collect();
238            all[..*lines.min(&all.len())].join("\n")
239        }
240        FailureStrategy::Grep { pattern, .. } => extract_grep(output, pattern),
241        FailureStrategy::Between { start, end } => {
242            let mut capturing = false;
243            let mut lines = Vec::new();
244            for line in output.lines() {
245                if !capturing && line.contains(start.as_str()) {
246                    capturing = true;
247                }
248                if capturing {
249                    lines.push(line);
250                    if line.contains(end.as_str()) {
251                        break;
252                    }
253                }
254            }
255            lines.join("\n")
256        }
257    }
258}
259
260// Submodules
261mod builtins;
262mod toml;
263
264// Static builtin patterns
265static BUILTINS: LazyLock<Vec<Pattern>> = LazyLock::new(builtin_patterns);