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