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