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);