clitest_lib/parser/v0/
parse.rs

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    // Handle the preamble before the first command block
45
46    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                // NOTE: These can exist in the preamble as well.
92                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                // NOTE: These can exist in the preamble as well.
98                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}