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