1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::output::*;
5use crate::script::*;
6use crate::util::ShellBit;
7
8use super::segment::{ScriptV0Segment, normalize_segments, segment_script};
9
10#[derive(Default)]
11struct OutputPatternBuilder {
12 ignore: Vec<OutputPattern>,
13 reject: Vec<OutputPattern>,
14 patterns: Vec<OutputPattern>,
15}
16
17pub fn parse_script(file_name: ScriptFile, script: &str) -> Result<Script, ScriptError> {
18 let lines = ScriptLine::parse(file_name.clone(), script);
19 let segments = segment_script(true, &mut lines.as_slice())?;
20 let normalized = normalize_segments(segments);
21 parse_normalized_script_v0(&normalized, file_name)
22}
23
24fn parse_normalized_script_v0(
25 segments: &[ScriptV0Segment],
26 file: ScriptFile,
27) -> Result<Script, ScriptError> {
28 let commands = parse_normalized_script_v0_commands(segments)?.into();
29
30 Ok(Script {
31 commands,
32 file,
33 includes: Arc::new(HashMap::new()),
34 })
35}
36
37fn parse_normalized_script_v0_commands(
38 segments: &[ScriptV0Segment],
39) -> Result<Vec<ScriptBlock>, ScriptError> {
40 let mut commands = vec![];
41
42 let preamble_index = segments
45 .iter()
46 .position(|segment| segment.is_command_block())
47 .unwrap_or(segments.len());
48 let (preamble, mut segments) = segments.split_at(preamble_index);
49
50 let builder = parse_script_v0_segments(preamble)?;
51 if !builder.ignore.is_empty() {
52 commands.push(ScriptBlock::GlobalIgnore(OutputPatterns::new(
53 builder.ignore,
54 )));
55 }
56 if !builder.reject.is_empty() {
57 commands.push(ScriptBlock::GlobalReject(OutputPatterns::new(
58 builder.reject,
59 )));
60 }
61
62 while let Some((command, remaining)) = segments.split_first() {
63 debug_assert!(
64 command.is_command_block(),
65 "not a command block: {command:?}"
66 );
67
68 if let ScriptV0Segment::SubBlock(_, block_type, args, sub_segments) = command {
69 let blocks = parse_normalized_script_v0_commands(sub_segments)?;
70
71 if block_type == "if" {
72 let condition = parse_if_condition(command.location().clone(), args)?;
73 commands.push(ScriptBlock::If(condition, blocks));
74 } else if block_type == "for" {
75 if args.len() >= 3 && args[1] == "in" {
76 commands.push(ScriptBlock::For(
77 ForCondition::Env(args[0].to_string(), args[2..].to_vec()),
78 blocks,
79 ));
80 } else {
81 return Err(ScriptError::new_with_data(
82 ScriptErrorType::InvalidBlockType,
83 command.location().clone(),
84 format!("for {args:?}"),
85 ));
86 }
87 } else if block_type == "background" {
88 commands.push(ScriptBlock::Background(blocks));
89 } else if block_type == "retry" {
90 commands.push(ScriptBlock::Retry(blocks));
91 } else if block_type == "defer" {
92 commands.push(ScriptBlock::Defer(blocks));
93 } else {
94 return Err(ScriptError::new_with_data(
95 ScriptErrorType::InvalidBlockType,
96 command.location().clone(),
97 block_type.clone(),
98 ));
99 }
100
101 segments = remaining;
102 continue;
103 }
104
105 if let ScriptV0Segment::Semi(location, text, args) = command {
106 segments = remaining;
107 if text == "pattern" {
108 commands.push(ScriptBlock::InternalCommand(
109 location.clone(),
110 InternalCommand::Pattern(args[0].to_string(), args[1].to_string()),
111 ));
112 continue;
113 } else if text == "using" {
114 if args.len() == 1 && args[0] == "tempdir" {
115 commands.push(ScriptBlock::InternalCommand(
116 location.clone(),
117 InternalCommand::UsingTempdir,
118 ));
119 continue;
120 }
121 if args.len() == 2 && args[0] == "dir" {
122 commands.push(ScriptBlock::InternalCommand(
123 location.clone(),
124 InternalCommand::UsingDir(args[1].clone(), false),
125 ));
126 continue;
127 }
128 if args.len() == 3 && args[0] == "new" && args[1] == "dir" {
129 commands.push(ScriptBlock::InternalCommand(
130 location.clone(),
131 InternalCommand::UsingDir(args[2].clone(), true),
132 ));
133 continue;
134 }
135 }
136 if text == "cd" && args.len() == 1 {
137 commands.push(ScriptBlock::InternalCommand(
138 location.clone(),
139 InternalCommand::ChangeDir(args[0].clone()),
140 ));
141 continue;
142 }
143 if text == "set" && args.len() == 2 {
144 commands.push(ScriptBlock::InternalCommand(
145 location.clone(),
146 InternalCommand::Set(args[0].to_string(), args[1].clone()),
147 ));
148 continue;
149 }
150 if text == "exit" && args.len() == 1 && args[0] == "script" {
151 commands.push(ScriptBlock::InternalCommand(
152 location.clone(),
153 InternalCommand::ExitScript,
154 ));
155 continue;
156 }
157 if text == "include" && args.len() == 1 {
158 commands.push(ScriptBlock::InternalCommand(
159 location.clone(),
160 InternalCommand::Include(args[0].to_string()),
161 ));
162 continue;
163 }
164 return Err(ScriptError::new_with_data(
165 ScriptErrorType::InvalidInternalCommand,
166 location.clone(),
167 format!("{text} {args:?}"),
168 ));
169 }
170
171 let next_command = remaining
172 .iter()
173 .position(|segment| segment.is_command_block())
174 .unwrap_or(remaining.len());
175 let mut pattern;
176 (pattern, segments) = remaining.split_at(next_command);
177
178 let location = command.location().clone();
179 let mut command = ScriptCommand {
180 command: match command {
181 ScriptV0Segment::Block(block) => block.block_type.clone().unwrap_command(),
182 _ => unreachable!(),
183 },
184 pattern: OutputPattern {
185 pattern: OutputPatternType::None,
186 ignore: Default::default(),
187 reject: Default::default(),
188 location: location.clone(),
189 },
190 exit: CommandExit::Success,
191 timeout: None,
192 expect_failure: false,
193 set_var: None,
194 };
195
196 if let Some(ScriptV0Segment::Block(maybe_meta)) = pattern.first() {
197 if maybe_meta.block_type.is_meta() {
198 pattern = pattern.split_first().unwrap().1;
199
200 for line in maybe_meta.lines.iter() {
201 if line.starts_with("%SET") {
202 if let Some(var) = line.text()[4..].split_whitespace().next() {
203 command.set_var = Some(var.to_string());
204 } else {
205 return Err(ScriptError::new(
206 ScriptErrorType::InvalidSetVariable,
207 line.location.clone(),
208 ));
209 }
210 } else if line.starts_with("%EXPECT_FAILURE") {
211 command.expect_failure = true;
212 } else if line.starts_with("%EXIT any") {
213 command.exit = CommandExit::Any;
214 } else if line.starts_with("%EXIT fail") {
215 command.exit = CommandExit::AnyFailure;
216 } else if line.starts_with("%EXIT timeout") {
217 command.exit = CommandExit::Timeout;
218 } else if line.starts_with("%EXIT ") {
219 if let Ok(status) = line.text()[6..].parse::<i32>() {
220 command.exit = CommandExit::Failure(status);
221 } else {
222 return Err(ScriptError::new(
223 ScriptErrorType::InvalidExitStatus,
224 line.location.clone(),
225 ));
226 }
227 } else if line.starts_with("%TIMEOUT ") {
228 if let Ok(timeout) = humantime::parse_duration(&line.text()[9..]) {
229 command.timeout = Some(timeout);
230 } else {
231 return Err(ScriptError::new(
232 ScriptErrorType::InvalidMetaCommand,
233 line.location.clone(),
234 ));
235 }
236 } else {
237 return Err(ScriptError::new_with_data(
238 ScriptErrorType::InvalidMetaCommand,
239 line.location.clone(),
240 format!("{line:?}"),
241 ));
242 }
243 }
244 }
245 }
246
247 let builder = parse_script_v0_segments(pattern)?;
248 command.pattern = OutputPattern::new_sequence(location, builder.patterns);
249 command.pattern.ignore = OutputPatterns::new(builder.ignore);
250 command.pattern.reject = OutputPatterns::new(builder.reject);
251 commands.push(ScriptBlock::Command(command));
252 }
253 Ok(commands)
254}
255
256fn parse_script_v0_segments(
257 segments: &[ScriptV0Segment],
258) -> Result<OutputPatternBuilder, ScriptError> {
259 let mut builder = OutputPatternBuilder::default();
260 for segment in segments {
261 parse_script_v0_segment(&mut builder, segment)?;
262 }
263 Ok(builder)
264}
265
266fn parse_script_v0_segment(
267 builder: &mut OutputPatternBuilder,
268 segment: &ScriptV0Segment,
269) -> Result<(), ScriptError> {
270 if segment.is_command_block() {
271 return Err(ScriptError::new(
272 ScriptErrorType::UnsupportedCommandPosition,
273 segment.location().clone(),
274 ));
275 }
276 match segment {
277 ScriptV0Segment::Block(block) => {
278 let mut pattern = block.lines.as_slice();
279 while let Some((line, rest)) = pattern.split_first() {
280 pattern = rest;
281 if line.text() == "!!!" {
282 let indent = line.text_untrimmed().find("!!!").unwrap();
283 while let Some((line, rest)) = pattern.split_first() {
284 pattern = rest;
285 if line.text() == "!!!" {
286 break;
287 } else {
288 builder.patterns.push(parse_pattern_line(
289 line.location.clone(),
290 &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
291 '!',
292 )?);
293 }
294 }
295 } else if line.text() == "???" {
296 let indent = line.text_untrimmed().find("???").unwrap();
297 while let Some((line, rest)) = pattern.split_first() {
298 pattern = rest;
299 if line.text() == "???" {
300 break;
301 } else {
302 builder.patterns.push(parse_pattern_line(
303 line.location.clone(),
304 &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
305 '?',
306 )?);
307 }
308 }
309 } else if line.text() == "!" || line.text() == "?" {
310 builder.patterns.push(parse_pattern_line(
311 line.location.clone(),
312 "",
313 line.first_char().unwrap(),
314 )?);
315 } else if line.starts_with("! ") || line.starts_with("? ") {
316 builder.patterns.push(parse_pattern_line(
317 line.location.clone(),
318 &line.text()[2..],
319 line.first_char().unwrap(),
320 )?);
321 } else if line.text() == "end" {
322 builder.patterns.push(OutputPattern {
323 pattern: OutputPatternType::End,
324 ignore: Default::default(),
325 reject: Default::default(),
326 location: line.location.clone(),
327 });
328 } else if line.text() == "none" {
329 builder.patterns.push(OutputPattern {
330 pattern: OutputPatternType::None,
331 ignore: Default::default(),
332 reject: Default::default(),
333 location: line.location.clone(),
334 });
335 } else {
336 return Err(ScriptError::new_with_data(
337 ScriptErrorType::InvalidPattern,
338 line.location.clone(),
339 format!("{:?}", line.text()),
340 ));
341 }
342 }
343 }
344 ScriptV0Segment::SubBlock(location, text, args, segments) => {
345 if text != "if" && !args.is_empty() {
346 return Err(ScriptError::new_with_data(
347 ScriptErrorType::InvalidPattern,
348 location.clone(),
349 format!("{text} {args:?}"),
350 ));
351 }
352 if text == "reject" {
353 let next = parse_script_v0_segments(segments)?;
354 if !next.ignore.is_empty() || !next.reject.is_empty() {
355 return Err(ScriptError::new(
356 ScriptErrorType::InvalidPattern,
357 location.clone(),
358 ));
359 }
360 builder.reject.extend(next.patterns);
361 } else if text == "ignore" {
362 let next = parse_script_v0_segments(segments)?;
363 if !next.ignore.is_empty() || !next.reject.is_empty() {
364 return Err(ScriptError::new(
365 ScriptErrorType::InvalidPattern,
366 location.clone(),
367 ));
368 }
369 builder.ignore.extend(next.patterns);
370 } else if text == "if" {
371 let condition = parse_if_condition(location.clone(), args)?;
372 let new_builder = parse_script_v0_segments(segments)?;
373 let pattern = OutputPattern {
374 pattern: OutputPatternType::If(
375 condition,
376 Box::new(OutputPattern::new_sequence(
377 location.clone(),
378 new_builder.patterns,
379 )),
380 ),
381 ignore: OutputPatterns::new(new_builder.ignore),
382 reject: OutputPatterns::new(new_builder.reject),
383 location: location.clone(),
384 };
385 builder.patterns.push(pattern);
386 } else {
387 let factory: &dyn Fn(&ScriptLocation, Vec<OutputPattern>) -> OutputPatternType =
388 match text.as_str() {
389 "repeat" => &|location, patterns| {
390 OutputPatternType::Repeat(Box::new(OutputPattern::new_sequence(
391 location.clone(),
392 patterns,
393 )))
394 },
395 "choice" => &|_location, patterns| OutputPatternType::Choice(patterns),
396 "unordered" => {
397 &|_location, patterns| OutputPatternType::Unordered(patterns)
398 }
399 "sequence" => &|_location, patterns| OutputPatternType::Sequence(patterns),
400 "optional" => &|location, patterns| {
401 OutputPatternType::Optional(Box::new(OutputPattern::new_sequence(
402 location.clone(),
403 patterns,
404 )))
405 },
406 "*" => &|location: &ScriptLocation, patterns| {
407 OutputPatternType::Any(Box::new(OutputPattern::new_sequence(
408 location.clone(),
409 patterns,
410 )))
411 },
412 _ => {
413 return Err(ScriptError::new_with_data(
414 ScriptErrorType::InvalidPattern,
415 location.clone(),
416 text.to_string(),
417 ));
418 }
419 };
420
421 let new_builder = parse_script_v0_segments(segments)?;
422 let pattern = OutputPattern {
423 pattern: factory(location, new_builder.patterns),
424 ignore: OutputPatterns::new(new_builder.ignore),
425 reject: OutputPatterns::new(new_builder.reject),
426 location: location.clone(),
427 };
428 builder.patterns.push(pattern);
429 }
430 }
431 ScriptV0Segment::Semi(location, text, args) => {
432 return Err(ScriptError::new_with_data(
433 ScriptErrorType::UnsupportedCommandPosition,
434 location.clone(),
435 format!("{text} {args:?}"),
436 ));
437 }
438 }
439 Ok(())
440}
441
442fn parse_if_condition(
443 location: ScriptLocation,
444 args: &[ShellBit],
445) -> Result<IfCondition, ScriptError> {
446 if args.len() == 1 && args[0] == "true" {
447 Ok(IfCondition::True)
448 } else if args.len() == 1 && args[0] == "false" {
449 Ok(IfCondition::False)
450 } else if args.len() == 3 && args[1] == "==" {
451 Ok(IfCondition::EnvEq(
452 false,
453 args[0].to_string(),
454 args[2].clone(),
455 ))
456 } else if args.len() == 3 && args[1] == "!=" {
457 Ok(IfCondition::EnvEq(
458 true,
459 args[0].to_string(),
460 args[2].clone(),
461 ))
462 } else {
463 return Err(ScriptError::new_with_data(
464 ScriptErrorType::InvalidIfCondition,
465 location.clone(),
466 format!("{args:?}"),
467 ));
468 }
469}
470
471fn parse_pattern_line(
472 location: ScriptLocation,
473 text: &str,
474 line_start: char,
475) -> Result<OutputPattern, ScriptError> {
476 if text.is_empty() {
477 return Ok(OutputPattern {
478 pattern: OutputPatternType::Literal("".to_string()),
479 ignore: Default::default(),
480 reject: Default::default(),
481 location,
482 });
483 }
484
485 let text = text.trim_end();
486
487 if line_start == '!' {
488 if !text.contains("%") {
489 return Ok(OutputPattern {
490 pattern: OutputPatternType::Literal(text.to_string()),
491 ignore: Default::default(),
492 reject: Default::default(),
493 location,
494 });
495 }
496
497 let pattern = GrokPattern::compile(text, true).map_err(|e| {
498 ScriptError::new_with_data(
499 ScriptErrorType::InvalidPattern,
500 location.clone(),
501 e.to_string(),
502 )
503 })?;
504 Ok(OutputPattern {
505 pattern: OutputPatternType::Pattern(Arc::new(pattern)),
506 ignore: Default::default(),
507 reject: Default::default(),
508 location,
509 })
510 } else if line_start == '?' {
511 let text = if text.ends_with('$') {
512 format!(r#"^{text}"#)
513 } else {
514 format!(r#"^{text}\s*$"#)
515 };
516 let pattern = GrokPattern::compile(&text, false).map_err(|e| {
517 ScriptError::new_with_data(
518 ScriptErrorType::InvalidPattern,
519 location.clone(),
520 e.to_string(),
521 )
522 })?;
523 Ok(OutputPattern {
524 pattern: OutputPatternType::Pattern(Arc::new(pattern)),
525 ignore: Default::default(),
526 reject: Default::default(),
527 location,
528 })
529 } else {
530 unreachable!("Invalid line start: {line_start}");
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537 use crate::output::Lines;
538
539 fn parse_pattern(pattern: &str) -> Result<OutputPattern, ScriptError> {
540 let lines = ScriptLine::parse(ScriptFile::new("test.cli"), pattern);
541 let segments = segment_script(true, &mut lines.as_slice()).unwrap();
542 let normalized = normalize_segments(segments);
543 Ok(parse_script_v0_segments(&normalized)?
544 .patterns
545 .first()
546 .unwrap()
547 .clone())
548 }
549
550 fn parse_lines(lines: &str) -> Result<Lines, ScriptError> {
551 Ok(Lines::new(
552 lines.lines().map(|l| l.to_string()).collect::<Vec<_>>(),
553 ))
554 }
555
556 #[test]
557 fn test_v0_patterns() {
558 let mut patterns = vec![];
559 patterns.push(parse_pattern("! a\n! b\n! c\n").unwrap());
560 patterns.push(parse_pattern("!!!\na\nb\nc\n!!!\n").unwrap());
561
562 let context = ScriptRunContext::default();
563 let context = OutputMatchContext::new(&context);
564 let output = parse_lines("a\nb\nc\n").unwrap();
565
566 for pattern in patterns {
567 let result = pattern.matches(context.clone(), output.clone());
568 assert!(result.is_ok());
569 }
570 }
571
572 #[test]
573 fn test_v0_block_pattern() {
574 let pattern = r#"
575 repeat {
576 choice {
577 ? pattern1 %{DATA}
578 ? pattern2 %{DATA}
579 ? pattern3 %{DATA}
580 }
581 }
582 "#;
583 let pattern = parse_pattern(pattern).unwrap();
584 eprintln!("{pattern:?}");
585 }
586}