Skip to main content

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            && 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}