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                        "*" => &|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}