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 if let ScriptV0Segment::Block(block) = maybe_meta {
196 if block.block_type.is_meta() {
197 pattern = pattern.split_first().unwrap().1;
198 parse_script_v0_meta(block, &mut command)?;
199 }
200 }
201 }
202
203 let builder = parse_script_v0_segments(pattern)?;
204 command.pattern = OutputPattern::new_sequence(location, builder.patterns);
205 command.pattern.ignore = OutputPatterns::new(builder.ignore);
206 command.pattern.reject = OutputPatterns::new(builder.reject);
207 commands.push(ScriptBlock::Command(command));
208 }
209 Ok(commands)
210}
211
212fn parse_script_v0_segments(
213 segments: &[ScriptV0Segment],
214) -> Result<OutputPatternBuilder, ScriptError> {
215 let mut builder = OutputPatternBuilder::default();
216 for segment in segments {
217 parse_script_v0_segment(&mut builder, segment)?;
218 }
219 Ok(builder)
220}
221
222fn parse_script_v0_segment(
223 builder: &mut OutputPatternBuilder,
224 segment: &ScriptV0Segment,
225) -> Result<(), ScriptError> {
226 if segment.is_command_block() {
227 return Err(ScriptError::new(
228 ScriptErrorType::UnsupportedCommandPosition,
229 segment.location().clone(),
230 ));
231 }
232 match segment {
233 ScriptV0Segment::Block(block) => {
234 let mut pattern = block.lines.as_slice();
235 while let Some((line, rest)) = pattern.split_first() {
236 pattern = rest;
237 if line.text() == ESCAPED_MULTILINE {
238 let indent = line.text_untrimmed().find(ESCAPED_MULTILINE).unwrap();
239 while let Some((line, rest)) = pattern.split_first() {
240 pattern = rest;
241 if line.text() == ESCAPED_MULTILINE {
242 break;
243 } else {
244 builder.patterns.push(parse_pattern_line(
245 line.location.clone(),
246 &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
247 '!',
248 )?);
249 }
250 }
251 } else if line.text() == REGEX_MULTILINE {
252 let indent = line.text_untrimmed().find(REGEX_MULTILINE).unwrap();
253 while let Some((line, rest)) = pattern.split_first() {
254 pattern = rest;
255 if line.text() == REGEX_MULTILINE {
256 break;
257 } else {
258 builder.patterns.push(parse_pattern_line(
259 line.location.clone(),
260 &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
261 '?',
262 )?);
263 }
264 }
265 } else if line.text() == LITERAL_MULTILINE {
266 let indent = line.text_untrimmed().find(LITERAL_MULTILINE).unwrap();
267 while let Some((line, rest)) = pattern.split_first() {
268 pattern = rest;
269 if line.text() == LITERAL_MULTILINE {
270 break;
271 } else {
272 builder.patterns.push(parse_pattern_line(
273 line.location.clone(),
274 &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
275 '"',
276 )?);
277 }
278 }
279 } else if line.text() == "!" || line.text() == "?" {
280 builder.patterns.push(parse_pattern_line(
281 line.location.clone(),
282 "",
283 line.first_char().unwrap(),
284 )?);
285 } else if line.starts_with("! ") || line.starts_with("? ") {
286 builder.patterns.push(parse_pattern_line(
287 line.location.clone(),
288 &line.text()[2..],
289 line.first_char().unwrap(),
290 )?);
291 } else if line.text() == "end" {
292 builder.patterns.push(OutputPattern {
293 pattern: OutputPatternType::End,
294 ignore: Default::default(),
295 reject: Default::default(),
296 location: line.location.clone(),
297 });
298 } else if line.text() == "none" {
299 builder.patterns.push(OutputPattern {
300 pattern: OutputPatternType::None,
301 ignore: Default::default(),
302 reject: Default::default(),
303 location: line.location.clone(),
304 });
305 } else {
306 return Err(ScriptError::new_with_data(
307 ScriptErrorType::InvalidPattern,
308 line.location.clone(),
309 format!("{:?}", line.text()),
310 ));
311 }
312 }
313 }
314 ScriptV0Segment::SubBlock(location, text, args, segments) => {
315 if text != "if" && !args.is_empty() {
316 return Err(ScriptError::new_with_data(
317 ScriptErrorType::InvalidPattern,
318 location.clone(),
319 format!("{text} {args:?}"),
320 ));
321 }
322 if text == "reject" {
323 let next = parse_script_v0_segments(segments)?;
324 if !next.ignore.is_empty() || !next.reject.is_empty() {
325 return Err(ScriptError::new(
326 ScriptErrorType::InvalidPattern,
327 location.clone(),
328 ));
329 }
330 builder.reject.extend(next.patterns);
331 } else if text == "ignore" {
332 let next = parse_script_v0_segments(segments)?;
333 if !next.ignore.is_empty() || !next.reject.is_empty() {
334 return Err(ScriptError::new(
335 ScriptErrorType::InvalidPattern,
336 location.clone(),
337 ));
338 }
339 builder.ignore.extend(next.patterns);
340 } else if text == "if" {
341 let condition = parse_if_condition(location.clone(), args)?;
342 let new_builder = parse_script_v0_segments(segments)?;
343 let pattern = OutputPattern {
344 pattern: OutputPatternType::If(
345 condition,
346 Box::new(OutputPattern::new_sequence(
347 location.clone(),
348 new_builder.patterns,
349 )),
350 ),
351 ignore: OutputPatterns::new(new_builder.ignore),
352 reject: OutputPatterns::new(new_builder.reject),
353 location: location.clone(),
354 };
355 builder.patterns.push(pattern);
356 } else {
357 let factory: &dyn Fn(&ScriptLocation, Vec<OutputPattern>) -> OutputPatternType =
358 match text.as_str() {
359 "repeat" => &|location, patterns| {
360 OutputPatternType::Repeat(Box::new(OutputPattern::new_sequence(
361 location.clone(),
362 patterns,
363 )))
364 },
365 "choice" => &|_location, patterns| OutputPatternType::Choice(patterns),
366 "unordered" => {
367 &|_location, patterns| OutputPatternType::Unordered(patterns)
368 }
369 "sequence" => &|_location, patterns| OutputPatternType::Sequence(patterns),
370 "optional" => &|location, patterns| {
371 OutputPatternType::Optional(Box::new(OutputPattern::new_sequence(
372 location.clone(),
373 patterns,
374 )))
375 },
376 "not" => &|location, patterns| {
377 OutputPatternType::Not(Box::new(OutputPattern::new_sequence(
378 location.clone(),
379 patterns,
380 )))
381 },
382 "*" => &|location: &ScriptLocation, patterns| {
383 OutputPatternType::Any(Box::new(OutputPattern::new_sequence(
384 location.clone(),
385 patterns,
386 )))
387 },
388 _ => {
389 return Err(ScriptError::new_with_data(
390 ScriptErrorType::InvalidPattern,
391 location.clone(),
392 text.to_string(),
393 ));
394 }
395 };
396
397 let new_builder = parse_script_v0_segments(segments)?;
398 let pattern = OutputPattern {
399 pattern: factory(location, new_builder.patterns),
400 ignore: OutputPatterns::new(new_builder.ignore),
401 reject: OutputPatterns::new(new_builder.reject),
402 location: location.clone(),
403 };
404 builder.patterns.push(pattern);
405 }
406 }
407 ScriptV0Segment::Semi(location, text, args) => {
408 return Err(ScriptError::new_with_data(
409 ScriptErrorType::UnsupportedCommandPosition,
410 location.clone(),
411 format!("{text} {args:?}"),
412 ));
413 }
414 }
415 Ok(())
416}
417
418fn parse_if_condition(
419 location: ScriptLocation,
420 args: &[ShellBit],
421) -> Result<IfCondition, ScriptError> {
422 if args.len() == 1 && args[0] == "true" {
423 Ok(IfCondition::True)
424 } else if args.len() == 1 && args[0] == "false" {
425 Ok(IfCondition::False)
426 } else if args.len() == 3 && args[1] == "==" {
427 Ok(IfCondition::EnvEq(
428 false,
429 args[0].to_string(),
430 args[2].clone(),
431 ))
432 } else if args.len() == 3 && args[1] == "!=" {
433 Ok(IfCondition::EnvEq(
434 true,
435 args[0].to_string(),
436 args[2].clone(),
437 ))
438 } else {
439 return Err(ScriptError::new_with_data(
440 ScriptErrorType::InvalidIfCondition,
441 location.clone(),
442 format!("{args:?}"),
443 ));
444 }
445}
446
447fn parse_pattern_line(
448 location: ScriptLocation,
449 text: &str,
450 line_start: char,
451) -> Result<OutputPattern, ScriptError> {
452 if text.is_empty() || line_start == '"' {
453 return Ok(OutputPattern {
454 pattern: OutputPatternType::Literal(text.to_string()),
455 ignore: Default::default(),
456 reject: Default::default(),
457 location,
458 });
459 }
460
461 let text = text.trim_end();
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, 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, 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 mut patterns = vec![];
654 patterns.push(parse_pattern("! a\n! b\n! c\n").unwrap());
655 patterns.push(parse_pattern("!!!\na\nb\nc\n!!!\n").unwrap());
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}