clitest_lib/parser/v0/
parse.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use crate::output::*;
5use crate::script::*;
6use crate::util::ShellBit;
7
8use super::segment::{ScriptV0Segment, normalize_segments, segment_script};
9
10#[derive(Default)]
11struct OutputPatternBuilder {
12    ignore: Vec<OutputPattern>,
13    reject: Vec<OutputPattern>,
14    patterns: Vec<OutputPattern>,
15}
16
17pub fn parse_script(file_name: ScriptFile, script: &str) -> Result<Script, ScriptError> {
18    let lines = ScriptLine::parse(file_name.clone(), script);
19    let segments = segment_script(true, &mut lines.as_slice())?;
20    let normalized = normalize_segments(segments);
21    parse_normalized_script_v0(&normalized, file_name)
22}
23
24fn parse_normalized_script_v0(
25    segments: &[ScriptV0Segment],
26    file: ScriptFile,
27) -> Result<Script, ScriptError> {
28    let commands = parse_normalized_script_v0_commands(segments)?.into();
29
30    Ok(Script {
31        commands,
32        file,
33        includes: Arc::new(HashMap::new()),
34    })
35}
36
37fn parse_normalized_script_v0_commands(
38    segments: &[ScriptV0Segment],
39) -> Result<Vec<ScriptBlock>, ScriptError> {
40    let mut commands = vec![];
41
42    // Handle the preamble before the first command block
43
44    let preamble_index = segments
45        .iter()
46        .position(|segment| segment.is_command_block())
47        .unwrap_or(segments.len());
48    let (preamble, mut segments) = segments.split_at(preamble_index);
49
50    let builder = parse_script_v0_segments(preamble)?;
51    if !builder.ignore.is_empty() {
52        commands.push(ScriptBlock::GlobalIgnore(OutputPatterns::new(
53            builder.ignore,
54        )));
55    }
56    if !builder.reject.is_empty() {
57        commands.push(ScriptBlock::GlobalReject(OutputPatterns::new(
58            builder.reject,
59        )));
60    }
61
62    while let Some((command, remaining)) = segments.split_first() {
63        debug_assert!(
64            command.is_command_block(),
65            "not a command block: {command:?}"
66        );
67
68        if let ScriptV0Segment::SubBlock(_, block_type, args, sub_segments) = command {
69            let blocks = parse_normalized_script_v0_commands(sub_segments)?;
70
71            if block_type == "if" {
72                let condition = parse_if_condition(command.location().clone(), args)?;
73                commands.push(ScriptBlock::If(condition, blocks));
74            } else if block_type == "for" {
75                if args.len() >= 3 && args[1] == "in" {
76                    commands.push(ScriptBlock::For(
77                        ForCondition::Env(args[0].to_string(), args[2..].to_vec()),
78                        blocks,
79                    ));
80                } else {
81                    return Err(ScriptError::new_with_data(
82                        ScriptErrorType::InvalidBlockType,
83                        command.location().clone(),
84                        format!("for {args:?}"),
85                    ));
86                }
87            } else if block_type == "background" {
88                commands.push(ScriptBlock::Background(blocks));
89            } else if block_type == "retry" {
90                commands.push(ScriptBlock::Retry(blocks));
91            } else if block_type == "defer" {
92                commands.push(ScriptBlock::Defer(blocks));
93            } else {
94                return Err(ScriptError::new_with_data(
95                    ScriptErrorType::InvalidBlockType,
96                    command.location().clone(),
97                    block_type.clone(),
98                ));
99            }
100
101            segments = remaining;
102            continue;
103        }
104
105        if let ScriptV0Segment::Semi(location, text, args) = command {
106            segments = remaining;
107            if text == "pattern" {
108                commands.push(ScriptBlock::InternalCommand(
109                    location.clone(),
110                    InternalCommand::Pattern(args[0].to_string(), args[1].to_string()),
111                ));
112                continue;
113            } else if text == "using" {
114                if args.len() == 1 && args[0] == "tempdir" {
115                    commands.push(ScriptBlock::InternalCommand(
116                        location.clone(),
117                        InternalCommand::UsingTempdir,
118                    ));
119                    continue;
120                }
121                if args.len() == 2 && args[0] == "dir" {
122                    commands.push(ScriptBlock::InternalCommand(
123                        location.clone(),
124                        InternalCommand::UsingDir(args[1].clone(), false),
125                    ));
126                    continue;
127                }
128                if args.len() == 3 && args[0] == "new" && args[1] == "dir" {
129                    commands.push(ScriptBlock::InternalCommand(
130                        location.clone(),
131                        InternalCommand::UsingDir(args[2].clone(), true),
132                    ));
133                    continue;
134                }
135            }
136            if text == "cd" && args.len() == 1 {
137                commands.push(ScriptBlock::InternalCommand(
138                    location.clone(),
139                    InternalCommand::ChangeDir(args[0].clone()),
140                ));
141                continue;
142            }
143            if text == "set" && args.len() == 2 {
144                commands.push(ScriptBlock::InternalCommand(
145                    location.clone(),
146                    InternalCommand::Set(args[0].to_string(), args[1].clone()),
147                ));
148                continue;
149            }
150            if text == "exit" && args.len() == 1 && args[0] == "script" {
151                commands.push(ScriptBlock::InternalCommand(
152                    location.clone(),
153                    InternalCommand::ExitScript,
154                ));
155                continue;
156            }
157            if text == "include" && args.len() == 1 {
158                commands.push(ScriptBlock::InternalCommand(
159                    location.clone(),
160                    InternalCommand::Include(args[0].to_string()),
161                ));
162                continue;
163            }
164            return Err(ScriptError::new_with_data(
165                ScriptErrorType::InvalidInternalCommand,
166                location.clone(),
167                format!("{text} {args:?}"),
168            ));
169        }
170
171        let next_command = remaining
172            .iter()
173            .position(|segment| segment.is_command_block())
174            .unwrap_or(remaining.len());
175        let mut pattern;
176        (pattern, segments) = remaining.split_at(next_command);
177
178        let location = command.location().clone();
179        let mut command = ScriptCommand {
180            command: match command {
181                ScriptV0Segment::Block(block) => block.block_type.clone().unwrap_command(),
182                _ => unreachable!(),
183            },
184            pattern: OutputPattern {
185                pattern: OutputPatternType::None,
186                ignore: Default::default(),
187                reject: Default::default(),
188                location: location.clone(),
189            },
190            exit: CommandExit::Success,
191            timeout: None,
192            expect_failure: false,
193            set_var: None,
194        };
195
196        if let Some(ScriptV0Segment::Block(maybe_meta)) = pattern.first() {
197            if maybe_meta.block_type.is_meta() {
198                pattern = pattern.split_first().unwrap().1;
199
200                for line in maybe_meta.lines.iter() {
201                    if line.starts_with("%SET") {
202                        if let Some(var) = line.text()[4..].split_whitespace().next() {
203                            command.set_var = Some(var.to_string());
204                        } else {
205                            return Err(ScriptError::new(
206                                ScriptErrorType::InvalidSetVariable,
207                                line.location.clone(),
208                            ));
209                        }
210                    } else if line.starts_with("%EXPECT_FAILURE") {
211                        command.expect_failure = true;
212                    } else if line.starts_with("%EXIT any") {
213                        command.exit = CommandExit::Any;
214                    } else if line.starts_with("%EXIT fail") {
215                        command.exit = CommandExit::AnyFailure;
216                    } else if line.starts_with("%EXIT timeout") {
217                        command.exit = CommandExit::Timeout;
218                    } else if line.starts_with("%EXIT ") {
219                        if let Ok(status) = line.text()[6..].parse::<i32>() {
220                            command.exit = CommandExit::Failure(status);
221                        } else {
222                            return Err(ScriptError::new(
223                                ScriptErrorType::InvalidExitStatus,
224                                line.location.clone(),
225                            ));
226                        }
227                    } else if line.starts_with("%TIMEOUT ") {
228                        if let Ok(timeout) = humantime::parse_duration(&line.text()[9..]) {
229                            command.timeout = Some(timeout);
230                        } else {
231                            return Err(ScriptError::new(
232                                ScriptErrorType::InvalidMetaCommand,
233                                line.location.clone(),
234                            ));
235                        }
236                    } else {
237                        return Err(ScriptError::new_with_data(
238                            ScriptErrorType::InvalidMetaCommand,
239                            line.location.clone(),
240                            format!("{line:?}"),
241                        ));
242                    }
243                }
244            }
245        }
246
247        let builder = parse_script_v0_segments(pattern)?;
248        command.pattern = OutputPattern::new_sequence(location, builder.patterns);
249        command.pattern.ignore = OutputPatterns::new(builder.ignore);
250        command.pattern.reject = OutputPatterns::new(builder.reject);
251        commands.push(ScriptBlock::Command(command));
252    }
253    Ok(commands)
254}
255
256fn parse_script_v0_segments(
257    segments: &[ScriptV0Segment],
258) -> Result<OutputPatternBuilder, ScriptError> {
259    let mut builder = OutputPatternBuilder::default();
260    for segment in segments {
261        parse_script_v0_segment(&mut builder, segment)?;
262    }
263    Ok(builder)
264}
265
266fn parse_script_v0_segment(
267    builder: &mut OutputPatternBuilder,
268    segment: &ScriptV0Segment,
269) -> Result<(), ScriptError> {
270    if segment.is_command_block() {
271        return Err(ScriptError::new(
272            ScriptErrorType::UnsupportedCommandPosition,
273            segment.location().clone(),
274        ));
275    }
276    match segment {
277        ScriptV0Segment::Block(block) => {
278            let mut pattern = block.lines.as_slice();
279            while let Some((line, rest)) = pattern.split_first() {
280                pattern = rest;
281                if line.text() == "!!!" {
282                    let indent = line.text_untrimmed().find("!!!").unwrap();
283                    while let Some((line, rest)) = pattern.split_first() {
284                        pattern = rest;
285                        if line.text() == "!!!" {
286                            break;
287                        } else {
288                            builder.patterns.push(parse_pattern_line(
289                                line.location.clone(),
290                                &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
291                                '!',
292                            )?);
293                        }
294                    }
295                } else if line.text() == "???" {
296                    let indent = line.text_untrimmed().find("???").unwrap();
297                    while let Some((line, rest)) = pattern.split_first() {
298                        pattern = rest;
299                        if line.text() == "???" {
300                            break;
301                        } else {
302                            builder.patterns.push(parse_pattern_line(
303                                line.location.clone(),
304                                &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
305                                '?',
306                            )?);
307                        }
308                    }
309                } else if line.text() == "!" || line.text() == "?" {
310                    builder.patterns.push(parse_pattern_line(
311                        line.location.clone(),
312                        "",
313                        line.first_char().unwrap(),
314                    )?);
315                } else if line.starts_with("! ") || line.starts_with("? ") {
316                    builder.patterns.push(parse_pattern_line(
317                        line.location.clone(),
318                        &line.text()[2..],
319                        line.first_char().unwrap(),
320                    )?);
321                } else if line.text() == "end" {
322                    builder.patterns.push(OutputPattern {
323                        pattern: OutputPatternType::End,
324                        ignore: Default::default(),
325                        reject: Default::default(),
326                        location: line.location.clone(),
327                    });
328                } else if line.text() == "none" {
329                    builder.patterns.push(OutputPattern {
330                        pattern: OutputPatternType::None,
331                        ignore: Default::default(),
332                        reject: Default::default(),
333                        location: line.location.clone(),
334                    });
335                } else {
336                    return Err(ScriptError::new_with_data(
337                        ScriptErrorType::InvalidPattern,
338                        line.location.clone(),
339                        format!("{:?}", line.text()),
340                    ));
341                }
342            }
343        }
344        ScriptV0Segment::SubBlock(location, text, args, segments) => {
345            if text != "if" && !args.is_empty() {
346                return Err(ScriptError::new_with_data(
347                    ScriptErrorType::InvalidPattern,
348                    location.clone(),
349                    format!("{text} {args:?}"),
350                ));
351            }
352            if text == "reject" {
353                let next = parse_script_v0_segments(segments)?;
354                if !next.ignore.is_empty() || !next.reject.is_empty() {
355                    return Err(ScriptError::new(
356                        ScriptErrorType::InvalidPattern,
357                        location.clone(),
358                    ));
359                }
360                builder.reject.extend(next.patterns);
361            } else if text == "ignore" {
362                let next = parse_script_v0_segments(segments)?;
363                if !next.ignore.is_empty() || !next.reject.is_empty() {
364                    return Err(ScriptError::new(
365                        ScriptErrorType::InvalidPattern,
366                        location.clone(),
367                    ));
368                }
369                builder.ignore.extend(next.patterns);
370            } else if text == "if" {
371                let condition = parse_if_condition(location.clone(), args)?;
372                let new_builder = parse_script_v0_segments(segments)?;
373                let pattern = OutputPattern {
374                    pattern: OutputPatternType::If(
375                        condition,
376                        Box::new(OutputPattern::new_sequence(
377                            location.clone(),
378                            new_builder.patterns,
379                        )),
380                    ),
381                    ignore: OutputPatterns::new(new_builder.ignore),
382                    reject: OutputPatterns::new(new_builder.reject),
383                    location: location.clone(),
384                };
385                builder.patterns.push(pattern);
386            } else {
387                let factory: &dyn Fn(&ScriptLocation, Vec<OutputPattern>) -> OutputPatternType =
388                    match text.as_str() {
389                        "repeat" => &|location, patterns| {
390                            OutputPatternType::Repeat(Box::new(OutputPattern::new_sequence(
391                                location.clone(),
392                                patterns,
393                            )))
394                        },
395                        "choice" => &|_location, patterns| OutputPatternType::Choice(patterns),
396                        "unordered" => {
397                            &|_location, patterns| OutputPatternType::Unordered(patterns)
398                        }
399                        "sequence" => &|_location, patterns| OutputPatternType::Sequence(patterns),
400                        "optional" => &|location, patterns| {
401                            OutputPatternType::Optional(Box::new(OutputPattern::new_sequence(
402                                location.clone(),
403                                patterns,
404                            )))
405                        },
406                        "*" => &|location: &ScriptLocation, patterns| {
407                            OutputPatternType::Any(Box::new(OutputPattern::new_sequence(
408                                location.clone(),
409                                patterns,
410                            )))
411                        },
412                        _ => {
413                            return Err(ScriptError::new_with_data(
414                                ScriptErrorType::InvalidPattern,
415                                location.clone(),
416                                text.to_string(),
417                            ));
418                        }
419                    };
420
421                let new_builder = parse_script_v0_segments(segments)?;
422                let pattern = OutputPattern {
423                    pattern: factory(location, new_builder.patterns),
424                    ignore: OutputPatterns::new(new_builder.ignore),
425                    reject: OutputPatterns::new(new_builder.reject),
426                    location: location.clone(),
427                };
428                builder.patterns.push(pattern);
429            }
430        }
431        ScriptV0Segment::Semi(location, text, args) => {
432            return Err(ScriptError::new_with_data(
433                ScriptErrorType::UnsupportedCommandPosition,
434                location.clone(),
435                format!("{text} {args:?}"),
436            ));
437        }
438    }
439    Ok(())
440}
441
442fn parse_if_condition(
443    location: ScriptLocation,
444    args: &[ShellBit],
445) -> Result<IfCondition, ScriptError> {
446    if args.len() == 1 && args[0] == "true" {
447        Ok(IfCondition::True)
448    } else if args.len() == 1 && args[0] == "false" {
449        Ok(IfCondition::False)
450    } else if args.len() == 3 && args[1] == "==" {
451        Ok(IfCondition::EnvEq(
452            false,
453            args[0].to_string(),
454            args[2].clone(),
455        ))
456    } else if args.len() == 3 && args[1] == "!=" {
457        Ok(IfCondition::EnvEq(
458            true,
459            args[0].to_string(),
460            args[2].clone(),
461        ))
462    } else {
463        return Err(ScriptError::new_with_data(
464            ScriptErrorType::InvalidIfCondition,
465            location.clone(),
466            format!("{args:?}"),
467        ));
468    }
469}
470
471fn parse_pattern_line(
472    location: ScriptLocation,
473    text: &str,
474    line_start: char,
475) -> Result<OutputPattern, ScriptError> {
476    if text.is_empty() {
477        return Ok(OutputPattern {
478            pattern: OutputPatternType::Literal("".to_string()),
479            ignore: Default::default(),
480            reject: Default::default(),
481            location,
482        });
483    }
484
485    let text = text.trim_end();
486
487    if line_start == '!' {
488        if !text.contains("%") {
489            return Ok(OutputPattern {
490                pattern: OutputPatternType::Literal(text.to_string()),
491                ignore: Default::default(),
492                reject: Default::default(),
493                location,
494            });
495        }
496
497        let pattern = GrokPattern::compile(text, true).map_err(|e| {
498            ScriptError::new_with_data(
499                ScriptErrorType::InvalidPattern,
500                location.clone(),
501                e.to_string(),
502            )
503        })?;
504        Ok(OutputPattern {
505            pattern: OutputPatternType::Pattern(Arc::new(pattern)),
506            ignore: Default::default(),
507            reject: Default::default(),
508            location,
509        })
510    } else if line_start == '?' {
511        let text = if text.ends_with('$') {
512            format!(r#"^{text}"#)
513        } else {
514            format!(r#"^{text}\s*$"#)
515        };
516        let pattern = GrokPattern::compile(&text, false).map_err(|e| {
517            ScriptError::new_with_data(
518                ScriptErrorType::InvalidPattern,
519                location.clone(),
520                e.to_string(),
521            )
522        })?;
523        Ok(OutputPattern {
524            pattern: OutputPatternType::Pattern(Arc::new(pattern)),
525            ignore: Default::default(),
526            reject: Default::default(),
527            location,
528        })
529    } else {
530        unreachable!("Invalid line start: {line_start}");
531    }
532}
533
534#[cfg(test)]
535mod tests {
536    use super::*;
537    use crate::output::Lines;
538
539    fn parse_pattern(pattern: &str) -> Result<OutputPattern, ScriptError> {
540        let lines = ScriptLine::parse(ScriptFile::new("test.cli"), pattern);
541        let segments = segment_script(true, &mut lines.as_slice()).unwrap();
542        let normalized = normalize_segments(segments);
543        Ok(parse_script_v0_segments(&normalized)?
544            .patterns
545            .first()
546            .unwrap()
547            .clone())
548    }
549
550    fn parse_lines(lines: &str) -> Result<Lines, ScriptError> {
551        Ok(Lines::new(
552            lines.lines().map(|l| l.to_string()).collect::<Vec<_>>(),
553        ))
554    }
555
556    #[test]
557    fn test_v0_patterns() {
558        let mut patterns = vec![];
559        patterns.push(parse_pattern("! a\n! b\n! c\n").unwrap());
560        patterns.push(parse_pattern("!!!\na\nb\nc\n!!!\n").unwrap());
561
562        let context = ScriptRunContext::default();
563        let context = OutputMatchContext::new(&context);
564        let output = parse_lines("a\nb\nc\n").unwrap();
565
566        for pattern in patterns {
567            let result = pattern.matches(context.clone(), output.clone());
568            assert!(result.is_ok());
569        }
570    }
571
572    #[test]
573    fn test_v0_block_pattern() {
574        let pattern = r#"
575        repeat {
576            choice {
577    ? pattern1 %{DATA}
578    ? pattern2 %{DATA}
579    ? pattern3 %{DATA}
580            }
581        }
582        "#;
583        let pattern = parse_pattern(pattern).unwrap();
584        eprintln!("{pattern:?}");
585    }
586}