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