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 let original = text.to_string();
462
463 if line_start == '!' {
464 if !text.contains("%") {
465 return Ok(OutputPattern {
466 pattern: OutputPatternType::Literal(text.to_string()),
467 ignore: Default::default(),
468 reject: Default::default(),
469 location,
470 });
471 }
472
473 let pattern = GrokPattern::compile(text, original, true).map_err(|e| {
474 ScriptError::new_with_data(
475 ScriptErrorType::InvalidPattern,
476 location.clone(),
477 e.to_string(),
478 )
479 })?;
480 Ok(OutputPattern {
481 pattern: OutputPatternType::Pattern(Arc::new(pattern)),
482 ignore: Default::default(),
483 reject: Default::default(),
484 location,
485 })
486 } else if line_start == '?' {
487 let text = if text.ends_with('$') {
488 format!(r#"^{text}"#)
489 } else {
490 format!(r#"^{text}\s*$"#)
491 };
492 let pattern = GrokPattern::compile(&text, original, false).map_err(|e| {
493 ScriptError::new_with_data(
494 ScriptErrorType::InvalidPattern,
495 location.clone(),
496 e.to_string(),
497 )
498 })?;
499 Ok(OutputPattern {
500 pattern: OutputPatternType::Pattern(Arc::new(pattern)),
501 ignore: Default::default(),
502 reject: Default::default(),
503 location,
504 })
505 } else {
506 unreachable!("Invalid line start: {line_start}");
507 }
508}
509
510fn parse_script_v0_meta(
511 meta_block: &ScriptV0Block,
512 command: &mut ScriptCommand,
513) -> Result<(), ScriptError> {
514 for line in meta_block.lines.iter() {
515 let Some(meta_text) = line.text().strip_prefix('%') else {
516 continue;
517 };
518 let words = shell_split(meta_text).map_err(|e| {
519 ScriptError::new_with_data(
520 ScriptErrorType::InvalidMetaCommand,
521 line.location.clone(),
522 format!("{e}: {line}", line = line.text()),
523 )
524 })?;
525
526 if words.is_empty() {
527 return Err(ScriptError::new(
528 ScriptErrorType::InvalidMetaCommand,
529 line.location.clone(),
530 ));
531 }
532
533 let command_string = words[0].to_string();
534
535 match &*command_string {
536 "SET" | "set" => {
537 if words.len() == 2 {
538 command.set_var = Some(words[1].to_string());
539 } else if words.len() == 3 {
540 command
541 .set_vars
542 .insert(words[1].to_string(), words[2].clone());
543 } else {
544 return Err(ScriptError::new(
545 ScriptErrorType::InvalidSetVariable,
546 line.location.clone(),
547 ));
548 }
549 }
550 "EXPECT_FAILURE" | "expect_failure" => {
551 command.expect_failure = true;
552 }
553 "EXIT" | "exit" => {
554 if words.len() >= 2 {
555 match &*words[1].to_string() {
556 "any" => {
557 command.exit = CommandExit::Any;
558 }
559 "fail" => {
560 command.exit = CommandExit::AnyFailure;
561 }
562 "timeout" => {
563 command.exit = CommandExit::Timeout;
564 }
565 status_str => {
566 if let Ok(status) = status_str.parse::<i32>() {
567 command.exit = CommandExit::Failure(status);
568 } else {
569 return Err(ScriptError::new(
570 ScriptErrorType::InvalidExitStatus,
571 line.location.clone(),
572 ));
573 }
574 }
575 }
576 } else {
577 return Err(ScriptError::new(
578 ScriptErrorType::InvalidMetaCommand,
579 line.location.clone(),
580 ));
581 }
582 }
583 "TIMEOUT" | "timeout" => {
584 if words.len() >= 2 {
585 let timeout_text = words[1..]
586 .iter()
587 .map(|w| w.to_string())
588 .collect::<Vec<_>>()
589 .join(" ");
590 if let Ok(timeout) = humantime::parse_duration(&timeout_text) {
591 command.timeout = Some(timeout);
592 } else {
593 return Err(ScriptError::new(
594 ScriptErrorType::InvalidMetaCommand,
595 line.location.clone(),
596 ));
597 }
598 } else {
599 return Err(ScriptError::new(
600 ScriptErrorType::InvalidMetaCommand,
601 line.location.clone(),
602 ));
603 }
604 }
605 "EXPECT" | "expect" => {
606 if words.len() != 3 {
607 return Err(ScriptError::new(
608 ScriptErrorType::InvalidMetaCommand,
609 line.location.clone(),
610 ));
611 }
612
613 let key = words[1].to_string();
614 let value = words[2].clone();
615 command.expect.insert(key, value);
616 }
617 _ => {
618 return Err(ScriptError::new_with_data(
619 ScriptErrorType::InvalidMetaCommand,
620 line.location.clone(),
621 format!("{line:?}"),
622 ));
623 }
624 }
625 }
626 Ok(())
627}
628
629#[cfg(test)]
630mod tests {
631 use super::*;
632 use crate::output::Lines;
633
634 fn parse_pattern(pattern: &str) -> Result<OutputPattern, ScriptError> {
635 let lines = ScriptLine::parse(ScriptFile::new("test.cli"), pattern);
636 let segments = segment_script(true, &mut lines.as_slice()).unwrap();
637 let normalized = normalize_segments(segments);
638 Ok(parse_script_v0_segments(&normalized)?
639 .patterns
640 .first()
641 .unwrap()
642 .clone())
643 }
644
645 fn parse_lines(lines: &str) -> Result<Lines, ScriptError> {
646 Ok(Lines::new(
647 lines.lines().map(|l| l.to_string()).collect::<Vec<_>>(),
648 ))
649 }
650
651 #[test]
652 fn test_v0_patterns() {
653 let patterns = vec![
654 parse_pattern("! a\n! b\n! c\n").unwrap(),
655 parse_pattern("!!!\na\nb\nc\n!!!\n").unwrap(),
656 ];
657
658 let context = ScriptRunContext::default();
659 let context = OutputMatchContext::new(&context);
660 let output = parse_lines("a\nb\nc\n").unwrap();
661
662 for pattern in patterns {
663 let result = pattern.matches(context.clone(), output.clone());
664 assert!(result.is_ok());
665 }
666 }
667
668 #[test]
669 fn test_v0_block_pattern() {
670 let pattern = r#"
671 repeat {
672 choice {
673 ? pattern1 %{DATA}
674 ? pattern2 %{DATA}
675 ? pattern3 %{DATA}
676 }
677 }
678 "#;
679 let pattern = parse_pattern(pattern).unwrap();
680 eprintln!("{pattern:?}");
681 }
682}