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 "*" => &|location: &ScriptLocation, patterns| {
377 OutputPatternType::Any(Box::new(OutputPattern::new_sequence(
378 location.clone(),
379 patterns,
380 )))
381 },
382 _ => {
383 return Err(ScriptError::new_with_data(
384 ScriptErrorType::InvalidPattern,
385 location.clone(),
386 text.to_string(),
387 ));
388 }
389 };
390
391 let new_builder = parse_script_v0_segments(segments)?;
392 let pattern = OutputPattern {
393 pattern: factory(location, new_builder.patterns),
394 ignore: OutputPatterns::new(new_builder.ignore),
395 reject: OutputPatterns::new(new_builder.reject),
396 location: location.clone(),
397 };
398 builder.patterns.push(pattern);
399 }
400 }
401 ScriptV0Segment::Semi(location, text, args) => {
402 return Err(ScriptError::new_with_data(
403 ScriptErrorType::UnsupportedCommandPosition,
404 location.clone(),
405 format!("{text} {args:?}"),
406 ));
407 }
408 }
409 Ok(())
410}
411
412fn parse_if_condition(
413 location: ScriptLocation,
414 args: &[ShellBit],
415) -> Result<IfCondition, ScriptError> {
416 if args.len() == 1 && args[0] == "true" {
417 Ok(IfCondition::True)
418 } else if args.len() == 1 && args[0] == "false" {
419 Ok(IfCondition::False)
420 } else if args.len() == 3 && args[1] == "==" {
421 Ok(IfCondition::EnvEq(
422 false,
423 args[0].to_string(),
424 args[2].clone(),
425 ))
426 } else if args.len() == 3 && args[1] == "!=" {
427 Ok(IfCondition::EnvEq(
428 true,
429 args[0].to_string(),
430 args[2].clone(),
431 ))
432 } else {
433 return Err(ScriptError::new_with_data(
434 ScriptErrorType::InvalidIfCondition,
435 location.clone(),
436 format!("{args:?}"),
437 ));
438 }
439}
440
441fn parse_pattern_line(
442 location: ScriptLocation,
443 text: &str,
444 line_start: char,
445) -> Result<OutputPattern, ScriptError> {
446 if text.is_empty() || line_start == '"' {
447 return Ok(OutputPattern {
448 pattern: OutputPatternType::Literal(text.to_string()),
449 ignore: Default::default(),
450 reject: Default::default(),
451 location,
452 });
453 }
454
455 let text = text.trim_end();
456
457 if line_start == '!' {
458 if !text.contains("%") {
459 return Ok(OutputPattern {
460 pattern: OutputPatternType::Literal(text.to_string()),
461 ignore: Default::default(),
462 reject: Default::default(),
463 location,
464 });
465 }
466
467 let pattern = GrokPattern::compile(text, true).map_err(|e| {
468 ScriptError::new_with_data(
469 ScriptErrorType::InvalidPattern,
470 location.clone(),
471 e.to_string(),
472 )
473 })?;
474 Ok(OutputPattern {
475 pattern: OutputPatternType::Pattern(Arc::new(pattern)),
476 ignore: Default::default(),
477 reject: Default::default(),
478 location,
479 })
480 } else if line_start == '?' {
481 let text = if text.ends_with('$') {
482 format!(r#"^{text}"#)
483 } else {
484 format!(r#"^{text}\s*$"#)
485 };
486 let pattern = GrokPattern::compile(&text, false).map_err(|e| {
487 ScriptError::new_with_data(
488 ScriptErrorType::InvalidPattern,
489 location.clone(),
490 e.to_string(),
491 )
492 })?;
493 Ok(OutputPattern {
494 pattern: OutputPatternType::Pattern(Arc::new(pattern)),
495 ignore: Default::default(),
496 reject: Default::default(),
497 location,
498 })
499 } else {
500 unreachable!("Invalid line start: {line_start}");
501 }
502}
503
504fn parse_script_v0_meta(
505 meta_block: &ScriptV0Block,
506 command: &mut ScriptCommand,
507) -> Result<(), ScriptError> {
508 for line in meta_block.lines.iter() {
509 let Some(meta_text) = line.text().strip_prefix('%') else {
510 continue;
511 };
512 let words = shell_split(meta_text).map_err(|e| {
513 ScriptError::new_with_data(
514 ScriptErrorType::InvalidMetaCommand,
515 line.location.clone(),
516 format!("{e}: {line}", line = line.text()),
517 )
518 })?;
519
520 if words.is_empty() {
521 return Err(ScriptError::new(
522 ScriptErrorType::InvalidMetaCommand,
523 line.location.clone(),
524 ));
525 }
526
527 let command_string = words[0].to_string();
528
529 match &*command_string {
530 "SET" | "set" => {
531 if words.len() == 2 {
532 command.set_var = Some(words[1].to_string());
533 } else if words.len() == 3 {
534 command
535 .set_vars
536 .insert(words[1].to_string(), words[2].clone());
537 } else {
538 return Err(ScriptError::new(
539 ScriptErrorType::InvalidSetVariable,
540 line.location.clone(),
541 ));
542 }
543 }
544 "EXPECT_FAILURE" | "expect_failure" => {
545 command.expect_failure = true;
546 }
547 "EXIT" | "exit" => {
548 if words.len() >= 2 {
549 match &*words[1].to_string() {
550 "any" => {
551 command.exit = CommandExit::Any;
552 }
553 "fail" => {
554 command.exit = CommandExit::AnyFailure;
555 }
556 "timeout" => {
557 command.exit = CommandExit::Timeout;
558 }
559 status_str => {
560 if let Ok(status) = status_str.parse::<i32>() {
561 command.exit = CommandExit::Failure(status);
562 } else {
563 return Err(ScriptError::new(
564 ScriptErrorType::InvalidExitStatus,
565 line.location.clone(),
566 ));
567 }
568 }
569 }
570 } else {
571 return Err(ScriptError::new(
572 ScriptErrorType::InvalidMetaCommand,
573 line.location.clone(),
574 ));
575 }
576 }
577 "TIMEOUT" | "timeout" => {
578 if words.len() >= 2 {
579 let timeout_text = words[1..]
580 .iter()
581 .map(|w| w.to_string())
582 .collect::<Vec<_>>()
583 .join(" ");
584 if let Ok(timeout) = humantime::parse_duration(&timeout_text) {
585 command.timeout = Some(timeout);
586 } else {
587 return Err(ScriptError::new(
588 ScriptErrorType::InvalidMetaCommand,
589 line.location.clone(),
590 ));
591 }
592 } else {
593 return Err(ScriptError::new(
594 ScriptErrorType::InvalidMetaCommand,
595 line.location.clone(),
596 ));
597 }
598 }
599 "EXPECT" | "expect" => {
600 if words.len() != 3 {
601 return Err(ScriptError::new(
602 ScriptErrorType::InvalidMetaCommand,
603 line.location.clone(),
604 ));
605 }
606
607 let key = words[1].to_string();
608 let value = words[2].clone();
609 command.expect.insert(key, value);
610 }
611 _ => {
612 return Err(ScriptError::new_with_data(
613 ScriptErrorType::InvalidMetaCommand,
614 line.location.clone(),
615 format!("{line:?}"),
616 ));
617 }
618 }
619 }
620 Ok(())
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use crate::output::Lines;
627
628 fn parse_pattern(pattern: &str) -> Result<OutputPattern, ScriptError> {
629 let lines = ScriptLine::parse(ScriptFile::new("test.cli"), pattern);
630 let segments = segment_script(true, &mut lines.as_slice()).unwrap();
631 let normalized = normalize_segments(segments);
632 Ok(parse_script_v0_segments(&normalized)?
633 .patterns
634 .first()
635 .unwrap()
636 .clone())
637 }
638
639 fn parse_lines(lines: &str) -> Result<Lines, ScriptError> {
640 Ok(Lines::new(
641 lines.lines().map(|l| l.to_string()).collect::<Vec<_>>(),
642 ))
643 }
644
645 #[test]
646 fn test_v0_patterns() {
647 let mut patterns = vec![];
648 patterns.push(parse_pattern("! a\n! b\n! c\n").unwrap());
649 patterns.push(parse_pattern("!!!\na\nb\nc\n!!!\n").unwrap());
650
651 let context = ScriptRunContext::default();
652 let context = OutputMatchContext::new(&context);
653 let output = parse_lines("a\nb\nc\n").unwrap();
654
655 for pattern in patterns {
656 let result = pattern.matches(context.clone(), output.clone());
657 assert!(result.is_ok());
658 }
659 }
660
661 #[test]
662 fn test_v0_block_pattern() {
663 let pattern = r#"
664 repeat {
665 choice {
666 ? pattern1 %{DATA}
667 ? pattern2 %{DATA}
668 ? pattern3 %{DATA}
669 }
670 }
671 "#;
672 let pattern = parse_pattern(pattern).unwrap();
673 eprintln!("{pattern:?}");
674 }
675}