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