clitest_lib/parser/
v0.rs

1use grok::Grok;
2use std::sync::Arc;
3
4use crate::command::CommandLine;
5use crate::output::*;
6use crate::script::*;
7use crate::util::ShellBit;
8use crate::util::shell_split;
9
10#[derive(Debug, Clone, derive_more::IsVariant, derive_more::Unwrap)]
11enum BlockType {
12    /// A command block.
13    Command(CommandLine),
14    /// Comments and whitespace lines.
15    Ineffectual,
16    /// Pattern lines.
17    Pattern,
18    /// Meta lines (`%EXPECT_FAILURE`, `%EXIT`, etc.).
19    Meta,
20    /// Any (`*`) block
21    Any,
22}
23
24impl BlockType {
25    fn is_same_type_as(&self, other: &Self) -> bool {
26        match (self, other) {
27            (BlockType::Command(_), BlockType::Command(_)) => true,
28            (BlockType::Ineffectual, BlockType::Ineffectual) => true,
29            (BlockType::Pattern, BlockType::Pattern) => true,
30            (BlockType::Meta, BlockType::Meta) => true,
31            (BlockType::Any, BlockType::Any) => true,
32            _ => false,
33        }
34    }
35}
36
37struct ScriptV0Block {
38    location: ScriptLocation,
39    block_type: BlockType,
40    lines: Vec<ScriptLine>,
41}
42
43impl ScriptV0Block {
44    /// Take the current block, replacing with an empty block at the given location.
45    pub fn take(&mut self, location: ScriptLocation, block_type: BlockType) -> Self {
46        Self {
47            location: std::mem::replace(&mut self.location, location),
48            block_type: std::mem::replace(&mut self.block_type, block_type),
49            lines: std::mem::take(&mut self.lines),
50        }
51    }
52
53    /// Split the first pattern line from the rest. If not a pattern block,
54    /// return None. May leave an empty block if the first line is the only line.
55    pub fn split_first(&mut self) -> Option<Self> {
56        match self.block_type {
57            BlockType::Pattern => {
58                let lines = &mut self.lines;
59                if lines.is_empty() {
60                    debug_assert!(false, "split_first called on empty pattern block");
61                    return None;
62                }
63                let first = lines.remove(0);
64                Some(Self {
65                    location: first.location.clone(),
66                    block_type: BlockType::Pattern,
67                    lines: vec![first],
68                })
69            }
70            _ => None,
71        }
72    }
73}
74
75impl std::fmt::Debug for ScriptV0Block {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        if f.alternate() {
78            let indent = f.width().unwrap_or_default();
79            let indent = " ".repeat(indent);
80            // HACK: Repurpose width as indent
81            // Left-pad by "indent" spaces
82            let c = match self.block_type {
83                BlockType::Command(_) => "$",
84                BlockType::Ineffectual => "#",
85                BlockType::Pattern => "",
86                BlockType::Meta => "%",
87                BlockType::Any => "*",
88            };
89            writeln!(f, "{indent}:{} {c}[", self.location.line)?;
90            for line in &self.lines {
91                writeln!(f, "{indent}  {:?}", line.text())?;
92            }
93            write!(f, "{indent}]")?;
94            Ok(())
95        } else {
96            f.debug_struct("ScriptBlock")
97                .field("location", &self.location)
98                .field("block_type", &self.block_type)
99                .field("lines", &self.lines)
100                .finish()
101        }
102    }
103}
104
105/// A segment of a script. This is the first stage of parsing, where we split
106/// the script.
107enum ScriptV0Segment {
108    Block(ScriptV0Block),
109    SubBlock(ScriptLocation, String, Vec<ShellBit>, Vec<ScriptV0Segment>),
110    Semi(ScriptLocation, String, Vec<ShellBit>),
111}
112
113impl std::fmt::Debug for ScriptV0Segment {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        if f.alternate() {
116            let indent = f.width().unwrap_or_default();
117            let indent_str = " ".repeat(indent);
118            // HACK: Indent the segments by using width, but don't print indent here
119            match self {
120                ScriptV0Segment::Block(block) => writeln!(f, "{:#indent$?}", block),
121                ScriptV0Segment::SubBlock(location, text, args, segments) => {
122                    writeln!(f, "{indent_str}:{} {text:?}{args:?} {{", location.line)?;
123                    for segment in segments {
124                        write!(f, "{segment:#indent$?}", indent = indent + 2)?;
125                    }
126                    writeln!(f, "{indent_str}}}")?;
127                    Ok(())
128                }
129                ScriptV0Segment::Semi(location, text, args) => {
130                    writeln!(f, "{indent_str}:{} {text:?}{args:?};", location.line)?;
131                    Ok(())
132                }
133            }
134        } else {
135            match self {
136                ScriptV0Segment::Block(block) => f
137                    .debug_struct("Block")
138                    .field("location", &block.location)
139                    .field("block_type", &block.block_type)
140                    .field("lines", &block.lines)
141                    .finish(),
142                ScriptV0Segment::SubBlock(location, text, args, segments) => f
143                    .debug_struct("SubBlock")
144                    .field("location", &location)
145                    .field("text", &text)
146                    .field("args", &args)
147                    .field("segments", &segments)
148                    .finish(),
149                ScriptV0Segment::Semi(location, text, args) => f
150                    .debug_struct("Semi")
151                    .field("location", &location)
152                    .field("text", &text)
153                    .field("args", &args)
154                    .finish(),
155            }
156        }
157    }
158}
159
160impl ScriptV0Segment {
161    fn is_empty(&self) -> bool {
162        match self {
163            ScriptV0Segment::Block(block) => block.lines.is_empty(),
164            ScriptV0Segment::SubBlock(_, text, _args, segments) => {
165                text != "*"
166                    && (segments.is_empty() || segments.iter().all(|segment| segment.is_empty()))
167            }
168            ScriptV0Segment::Semi(..) => false,
169        }
170    }
171
172    /// Used by wildcard handling.
173    fn split_first(&mut self) -> Option<Self> {
174        match self {
175            ScriptV0Segment::Block(block) => block.split_first().map(ScriptV0Segment::Block),
176            &mut ScriptV0Segment::SubBlock(ref location, ..) => {
177                if self.is_command_block() {
178                    None
179                } else {
180                    Some(std::mem::replace(
181                        self,
182                        ScriptV0Segment::Block(ScriptV0Block {
183                            location: location.clone(),
184                            block_type: BlockType::Ineffectual,
185                            lines: vec![],
186                        }),
187                    ))
188                }
189            }
190            ScriptV0Segment::Semi(..) => None,
191        }
192    }
193
194    /// Returns true if this segment is a command block, or the first block it
195    /// contains is a command block. Note that this should only be called on
196    /// normalized segments.
197    fn is_command_block(&self) -> bool {
198        match self {
199            ScriptV0Segment::Block(block) => block.block_type.is_command(),
200            ScriptV0Segment::SubBlock(.., segments) => {
201                segments.iter().any(|segment| segment.is_command_block())
202            }
203            ScriptV0Segment::Semi(..) => true,
204        }
205    }
206
207    fn is_meta_block(&self) -> bool {
208        match self {
209            ScriptV0Segment::Block(block) => block.block_type.is_meta(),
210            _ => false,
211        }
212    }
213
214    fn location(&self) -> &ScriptLocation {
215        match self {
216            ScriptV0Segment::Block(block) => &block.location,
217            ScriptV0Segment::SubBlock(location, ..) => location,
218            ScriptV0Segment::Semi(location, ..) => location,
219        }
220    }
221
222    fn last_location(&self) -> &ScriptLocation {
223        match self {
224            ScriptV0Segment::Block(block) => &block.lines.last().unwrap().location,
225            ScriptV0Segment::SubBlock(location, .., segments) => {
226                if let Some(last) = segments.last() {
227                    last.last_location()
228                } else {
229                    location
230                }
231            }
232            ScriptV0Segment::Semi(location, ..) => location,
233        }
234    }
235}
236
237pub fn parse_script(file_name: ScriptFile, script: &str) -> Result<Script, ScriptError> {
238    let lines = ScriptLine::parse(file_name.clone(), script);
239    let segments = segment_script(true, &mut lines.as_slice())?;
240    let normalized = normalize_segments(segments);
241    parse_normalized_script_v0(&normalized, file_name)
242}
243
244/// Split the script into parsing segments. These allow us to more easily parse
245/// in later phases because we avoid having to check for block boundaries.
246fn segment_script(
247    top_level: bool,
248    lines_slice: &mut &[ScriptLine],
249) -> Result<Vec<ScriptV0Segment>, ScriptError> {
250    let mut segments = Vec::new();
251    let mut current_segment = None;
252
253    fn is_subblock(text: &str) -> Option<(bool, &str, &str)> {
254        // Workaround for missing let chains
255        if text.starts_with(|c: char| c.is_alphabetic()) {
256            let is_semi = text.ends_with(';');
257            text.strip_suffix(|c: char| c == '{' || c == ';')
258                .map(|text| {
259                    if let Some((block_type, args)) = text.trim().split_once(char::is_whitespace) {
260                        (is_semi, block_type.trim(), args.trim())
261                    } else {
262                        (is_semi, text.trim(), "")
263                    }
264                })
265        } else {
266            None
267        }
268    }
269
270    let mut lines = lines_slice.iter();
271    let orig_slice = *lines_slice;
272    let mut multiline_terminator = None;
273    while let Some(line) = lines.next() {
274        if let Some(terminator) = multiline_terminator {
275            if line.text() == terminator {
276                multiline_terminator = None;
277            }
278        } else if line.text() == "!!!" {
279            multiline_terminator = Some("!!!");
280        } else if line.text() == "???" {
281            multiline_terminator = Some("???");
282        }
283
284        // For commands, we greedily consume all lines until we successfully
285        // parse a command (or fail to parse).
286        if line.starts_with("$") {
287            if let Some(segment) = current_segment.take() {
288                segments.push(ScriptV0Segment::Block(segment));
289            }
290            let mut block_lines = vec![line.clone()];
291            let mut command = line.text()[1..].trim().to_string();
292            let mut line_count = 1;
293            let command = loop {
294                match parse_command_line(line.location.clone(), line_count, &command) {
295                    Ok(command) => break command,
296                    Err(e @ ScriptErrorType::UnclosedQuote)
297                    | Err(e @ ScriptErrorType::UnclosedBackslash) => match lines.next() {
298                        Some(line) => {
299                            block_lines.push(line.clone());
300                            command.push('\n');
301                            command.push_str(line.text());
302                            line_count += 1;
303                        }
304                        None => {
305                            return Err(ScriptError::new(e, line.location.clone()));
306                        }
307                    },
308                    Err(e) => {
309                        return Err(ScriptError::new(e, line.location.clone()));
310                    }
311                }
312            };
313
314            segments.push(ScriptV0Segment::Block(ScriptV0Block {
315                block_type: BlockType::Command(command),
316                lines: block_lines,
317                location: line.location.clone(),
318            }));
319        } else if let Some((is_semi, block_type, args)) = is_subblock(line.text()) {
320            if let Some(segment) = current_segment.take() {
321                segments.push(ScriptV0Segment::Block(segment));
322            }
323
324            let args = shell_split(args).map_err(|_| {
325                ScriptError::new_with_data(
326                    ScriptErrorType::InvalidBlockArgs,
327                    line.location.clone(),
328                    format!("{block_type} {args}"),
329                )
330            })?;
331
332            if is_semi {
333                segments.push(ScriptV0Segment::Semi(
334                    line.location.clone(),
335                    block_type.to_string(),
336                    args,
337                ));
338            } else {
339                // Temporaraliy swap from iterator to slice
340                let mut rest = lines.as_slice();
341                if rest.is_empty() {
342                    return Err(ScriptError::new(
343                        ScriptErrorType::InvalidBlockEnd,
344                        line.location.clone(),
345                    ));
346                }
347
348                segments.push(ScriptV0Segment::SubBlock(
349                    line.location.clone(),
350                    block_type.to_string(),
351                    args,
352                    segment_script(false, &mut rest)?,
353                ));
354                lines = rest.iter();
355            }
356        } else if line.text() == "}" {
357            // Note that the closing brace is not included in the current
358            // segment, we omit these lines from the segment tree.
359            if top_level {
360                return Err(ScriptError::new(
361                    ScriptErrorType::InvalidBlockEnd,
362                    line.location.clone(),
363                ));
364            }
365            *lines_slice = lines.as_slice();
366            if let Some(segment) = current_segment.take() {
367                segments.push(ScriptV0Segment::Block(segment));
368            }
369            return Ok(segments);
370        } else {
371            // Split into ineffectual and non-ineffectual lines
372            let block_type = if multiline_terminator.is_some() {
373                BlockType::Pattern
374            } else if line.starts_with("#") || line.is_empty() {
375                BlockType::Ineffectual
376            } else if line.starts_with("%") {
377                BlockType::Meta
378            } else if line.starts_with("*") {
379                BlockType::Any
380            } else {
381                BlockType::Pattern
382            };
383
384            let segment = current_segment.get_or_insert(ScriptV0Block {
385                block_type: block_type.clone(),
386                lines: Vec::new(),
387                location: line.location.clone(),
388            });
389            if !segment.block_type.is_same_type_as(&block_type) {
390                segments.push(ScriptV0Segment::Block(
391                    segment.take(line.location.clone(), block_type),
392                ));
393            }
394            segment.lines.push(line.clone());
395        }
396    }
397
398    if !top_level {
399        return Err(ScriptError::new(
400            ScriptErrorType::InvalidBlockEnd,
401            orig_slice.last().unwrap().location.clone(),
402        ));
403    }
404
405    if let Some(segment) = current_segment.take() {
406        segments.push(ScriptV0Segment::Block(segment));
407    }
408
409    Ok(segments)
410}
411
412fn insert_virtual_end_block(location: ScriptLocation, segments: &mut Vec<ScriptV0Segment>) {
413    let line = ScriptLine::new(location.file.clone(), location.line - 1, "end");
414
415    segments.push(ScriptV0Segment::Block(ScriptV0Block {
416        location: line.location.clone(),
417        block_type: BlockType::Pattern,
418        lines: vec![line],
419    }));
420}
421
422/// Remove all ineffectual blocks, and merge consecutive blocks that are of the same type.
423fn normalize_segments(segments: Vec<ScriptV0Segment>) -> Vec<ScriptV0Segment> {
424    let mut new_segments = vec![];
425    let mut command_needs_end = false;
426
427    let Some(last_line) = segments.last().map(|segment| segment.location().clone()) else {
428        return segments;
429    };
430
431    for mut segment in segments {
432        if segment.is_command_block() && command_needs_end {
433            insert_virtual_end_block(segment.location().clone(), &mut new_segments);
434            command_needs_end = false;
435        }
436        match segment {
437            ScriptV0Segment::Block(ref mut block) => {
438                debug_assert!(
439                    !block.lines.is_empty(),
440                    "empty blocks should not exist here"
441                );
442                if block.block_type.is_ineffectual() {
443                    continue;
444                }
445                if block.block_type.is_command() {
446                    command_needs_end = true;
447                }
448                if let Some(ScriptV0Segment::Block(last_block)) = new_segments.last_mut() {
449                    if block.block_type.is_command() {
450                        new_segments.push(segment);
451                    } else if block.block_type.is_same_type_as(&last_block.block_type) {
452                        last_block.lines.extend(std::mem::take(&mut block.lines));
453                    } else {
454                        new_segments.push(segment);
455                    }
456                } else {
457                    new_segments.push(segment);
458                }
459            }
460            ScriptV0Segment::SubBlock(location, text, args, segments) => {
461                let normalized = normalize_segments(segments);
462                new_segments.push(ScriptV0Segment::SubBlock(location, text, args, normalized));
463            }
464            ScriptV0Segment::Semi(location, text, args) => {
465                new_segments.push(ScriptV0Segment::Semi(location, text, args));
466            }
467        }
468    }
469
470    // Add a virtual "end" block to the end of the last command block.
471    if command_needs_end {
472        insert_virtual_end_block(last_line, &mut new_segments);
473    }
474
475    // Pass 2: Convert any "any"-type blocks to sub-blocks and steal the next line or non-command subblock.
476    let mut i = 0;
477    while i < new_segments.len() {
478        if let ScriptV0Segment::Block(block) = &mut new_segments[i] {
479            if block.block_type.is_any() {
480                let location = block.location.clone();
481                new_segments[i] =
482                    ScriptV0Segment::SubBlock(location.clone(), "*".to_string(), vec![], vec![]);
483
484                if i + 1 < new_segments.len() {
485                    if let Some(first) = new_segments[i + 1].split_first() {
486                        new_segments[i] = ScriptV0Segment::SubBlock(
487                            location.clone(),
488                            "*".to_string(),
489                            vec![],
490                            vec![first],
491                        );
492                    }
493                }
494            }
495        }
496        if new_segments[i].is_empty() {
497            new_segments.remove(i);
498        } else {
499            i += 1;
500        }
501    }
502
503    new_segments
504}
505
506pub fn parse_command_line(
507    location: ScriptLocation,
508    line_count: usize,
509    command: &str,
510) -> Result<CommandLine, ScriptErrorType> {
511    let command_str = command.to_string();
512    // Process the accumulated command
513    const SEPARATORS: &[&str] = &[
514        "&&", "||", "1>&2", "2>&1", "1>", "2>", "&", "|", ";", "(", ")", ">", "<", "=",
515    ];
516    let command = match shellish_parse::multiparse(
517        command,
518        shellish_parse::ParseOptions::default(),
519        SEPARATORS,
520    ) {
521        Ok(command) => command,
522        Err(shellish_parse::ParseError::DanglingString) => {
523            return Err(ScriptErrorType::UnclosedQuote);
524        }
525        Err(shellish_parse::ParseError::DanglingBackslash) => {
526            return Err(ScriptErrorType::UnclosedBackslash);
527        }
528        _ => {
529            return Err(ScriptErrorType::IllegalShellCommand);
530        }
531    };
532    let mut command_bits = vec![];
533    for (_, seperator) in command {
534        if let Some(seperator) = seperator {
535            if SEPARATORS[seperator] == "&" {
536                return Err(ScriptErrorType::BackgroundProcessNotAllowed);
537            }
538            if SEPARATORS[seperator] == ">&" {
539                return Err(ScriptErrorType::UnsupportedRedirection);
540            }
541            command_bits.push(SEPARATORS[seperator].to_string());
542        }
543    }
544
545    Ok(CommandLine::new(command_str, location, line_count))
546}
547
548#[derive(Default)]
549struct OutputPatternBuilder {
550    ignore: Vec<OutputPattern>,
551    reject: Vec<OutputPattern>,
552    patterns: Vec<OutputPattern>,
553}
554
555impl OutputPatternBuilder {
556    fn push(&mut self, location: ScriptLocation, pattern: OutputPatternType) {
557        self.patterns.push(OutputPattern {
558            pattern,
559            ignore: Default::default(),
560            reject: Default::default(),
561            location,
562        });
563    }
564}
565
566fn parse_normalized_script_v0(
567    segments: &[ScriptV0Segment],
568    file: ScriptFile,
569) -> Result<Script, ScriptError> {
570    // Handle the preamble before the first command block
571
572    let preamble_index = segments
573        .iter()
574        .position(|segment| segment.is_command_block())
575        .unwrap_or(segments.len());
576    let (preamble, rest) = segments.split_at(preamble_index);
577
578    let mut grok = Grok::with_default_patterns();
579
580    let builder = parse_script_v0_segments(preamble, &mut grok)?;
581    if let Some(pattern) = builder.patterns.first() {
582        return Err(ScriptError::new(
583            ScriptErrorType::InvalidGlobalPattern,
584            pattern.location.clone(),
585        ));
586    }
587    let global_ignore = builder.ignore;
588    let global_reject = builder.reject;
589
590    let commands =
591        parse_normalized_script_v0_commands(rest, &mut grok, &global_ignore, &global_reject)?;
592
593    Ok(Script {
594        commands,
595        file,
596        grok,
597    })
598}
599
600fn parse_normalized_script_v0_commands(
601    mut segments: &[ScriptV0Segment],
602    grok: &mut Grok,
603    global_ignore: &Vec<OutputPattern>,
604    global_reject: &Vec<OutputPattern>,
605) -> Result<Vec<ScriptBlock>, ScriptError> {
606    let mut commands = vec![];
607    while let Some((command, remaining)) = segments.split_first() {
608        debug_assert!(
609            command.is_command_block(),
610            "not a command block: {command:?}"
611        );
612
613        if let ScriptV0Segment::SubBlock(_, block_type, args, sub_segments) = command {
614            let blocks = parse_normalized_script_v0_commands(
615                sub_segments,
616                grok,
617                global_ignore,
618                global_reject,
619            )?;
620
621            if block_type == "if" {
622                let condition = parse_if_condition(command.location().clone(), args)?;
623                commands.push(ScriptBlock::If(condition, blocks));
624            } else if block_type == "for" {
625                if args.len() >= 3 && args[1] == "in" {
626                    commands.push(ScriptBlock::For(
627                        ForCondition::Env(args[0].to_string(), args[2..].to_vec()),
628                        blocks,
629                    ));
630                } else {
631                    return Err(ScriptError::new_with_data(
632                        ScriptErrorType::InvalidBlockType,
633                        command.location().clone(),
634                        format!("for {args:?}"),
635                    ));
636                }
637            } else if block_type == "background" {
638                commands.push(ScriptBlock::Background(blocks));
639            } else if block_type == "retry" {
640                commands.push(ScriptBlock::Retry(blocks));
641            } else if block_type == "defer" {
642                commands.push(ScriptBlock::Defer(blocks));
643            } else {
644                return Err(ScriptError::new_with_data(
645                    ScriptErrorType::InvalidBlockType,
646                    command.location().clone(),
647                    block_type.clone(),
648                ));
649            }
650
651            segments = remaining;
652            continue;
653        }
654
655        if let ScriptV0Segment::Semi(location, text, args) = command {
656            segments = remaining;
657            if text == "using" {
658                if args.len() == 1 && args[0] == "tempdir" {
659                    commands.push(ScriptBlock::InternalCommand(InternalCommand::UsingTempdir));
660                    continue;
661                }
662                if args.len() == 2 && args[0] == "dir" {
663                    commands.push(ScriptBlock::InternalCommand(InternalCommand::UsingDir(
664                        args[1].clone(),
665                        false,
666                    )));
667                    continue;
668                }
669                if args.len() == 3 && args[0] == "new" && args[1] == "dir" {
670                    commands.push(ScriptBlock::InternalCommand(InternalCommand::UsingDir(
671                        args[2].clone(),
672                        true,
673                    )));
674                    continue;
675                }
676            }
677            if text == "cd" && args.len() == 1 {
678                commands.push(ScriptBlock::InternalCommand(InternalCommand::ChangeDir(
679                    args[0].clone(),
680                )));
681                continue;
682            }
683            if text == "set" && args.len() == 2 {
684                commands.push(ScriptBlock::InternalCommand(InternalCommand::Set(
685                    args[0].to_string(),
686                    args[1].clone(),
687                )));
688                continue;
689            }
690            return Err(ScriptError::new_with_data(
691                ScriptErrorType::InvalidInternalCommand,
692                location.clone(),
693                format!("{text} {args:?}"),
694            ));
695        }
696
697        let next_command = remaining
698            .iter()
699            .position(|segment| segment.is_command_block())
700            .unwrap_or(remaining.len());
701        let mut pattern;
702        (pattern, segments) = remaining.split_at(next_command);
703
704        let location = command.location().clone();
705        let mut command = ScriptCommand {
706            command: match command {
707                ScriptV0Segment::Block(block) => block.block_type.clone().unwrap_command(),
708                _ => unreachable!(),
709            },
710            pattern: OutputPattern {
711                pattern: OutputPatternType::None,
712                ignore: Default::default(),
713                reject: Default::default(),
714                location: location.clone(),
715            },
716            exit: CommandExit::Success,
717            expect_failure: false,
718            set_var: None,
719        };
720
721        if let Some(ScriptV0Segment::Block(maybe_meta)) = pattern.first() {
722            if maybe_meta.block_type.is_meta() {
723                pattern = pattern.split_first().unwrap().1;
724
725                for line in maybe_meta.lines.iter() {
726                    if line.starts_with("%SET") {
727                        if let Some(var) = line.text()[4..].split_whitespace().next() {
728                            command.set_var = Some(var.to_string());
729                        } else {
730                            return Err(ScriptError::new(
731                                ScriptErrorType::InvalidSetVariable,
732                                line.location.clone(),
733                            ));
734                        }
735                    } else if line.starts_with("%EXPECT_FAILURE") {
736                        command.expect_failure = true;
737                    } else if line.starts_with("%EXIT any") {
738                        command.exit = CommandExit::Any;
739                    } else if line.starts_with("%EXIT ") {
740                        if let Ok(status) = line.text()[6..].parse::<i32>() {
741                            command.exit = CommandExit::Failure(status);
742                        } else {
743                            return Err(ScriptError::new(
744                                ScriptErrorType::InvalidExitStatus,
745                                line.location.clone(),
746                            ));
747                        }
748                    }
749                }
750            }
751        }
752
753        let builder = parse_script_v0_segments(pattern, grok)?;
754        command.pattern = OutputPattern::new_sequence(location, builder.patterns);
755        command.pattern.ignore = global_ignore
756            .iter()
757            .cloned()
758            .chain(builder.ignore.iter().cloned())
759            .collect::<Vec<_>>()
760            .into();
761        command.pattern.reject = global_reject
762            .iter()
763            .cloned()
764            .chain(builder.reject.iter().cloned())
765            .collect::<Vec<_>>()
766            .into();
767        commands.push(ScriptBlock::Command(command));
768    }
769    Ok(commands)
770}
771
772fn parse_script_v0_segments(
773    segments: &[ScriptV0Segment],
774    grok: &mut Grok,
775) -> Result<OutputPatternBuilder, ScriptError> {
776    let mut builder = OutputPatternBuilder::default();
777    for segment in segments {
778        parse_script_v0_segment(grok, &mut builder, segment)?;
779    }
780    Ok(builder)
781}
782
783fn parse_script_v0_segment(
784    grok: &mut Grok,
785    builder: &mut OutputPatternBuilder,
786    segment: &ScriptV0Segment,
787) -> Result<(), ScriptError> {
788    if segment.is_command_block() {
789        return Err(ScriptError::new(
790            ScriptErrorType::UnsupportedCommandPosition,
791            segment.location().clone(),
792        ));
793    }
794    match segment {
795        ScriptV0Segment::Block(block) => {
796            let mut pattern = block.lines.as_slice();
797            while let Some((line, rest)) = pattern.split_first() {
798                pattern = rest;
799                if line.text() == "!!!" {
800                    let indent = line.text_untrimmed().find("!!!").unwrap();
801                    while let Some((line, rest)) = pattern.split_first() {
802                        pattern = rest;
803                        if line.text() == "!!!" {
804                            break;
805                        } else {
806                            builder.patterns.push(parse_pattern_line(
807                                grok,
808                                line.location.clone(),
809                                &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
810                                '!',
811                            )?);
812                        }
813                    }
814                } else if line.text() == "???" {
815                    let indent = line.text_untrimmed().find("???").unwrap();
816                    while let Some((line, rest)) = pattern.split_first() {
817                        pattern = rest;
818                        if line.text() == "???" {
819                            break;
820                        } else {
821                            builder.patterns.push(parse_pattern_line(
822                                grok,
823                                line.location.clone(),
824                                &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
825                                '?',
826                            )?);
827                        }
828                    }
829                } else if line.text() == "!" || line.text() == "?" {
830                    builder.patterns.push(parse_pattern_line(
831                        grok,
832                        line.location.clone(),
833                        "",
834                        line.first_char().unwrap(),
835                    )?);
836                } else if line.starts_with("! ") || line.starts_with("? ") {
837                    builder.patterns.push(parse_pattern_line(
838                        grok,
839                        line.location.clone(),
840                        &line.text()[2..],
841                        line.first_char().unwrap(),
842                    )?);
843                } else if let Some(pattern) = line.strip_prefix("pattern ") {
844                    if let Some((name, pattern)) = pattern.split_once(' ') {
845                        grok.add_pattern(name, pattern);
846                    } else {
847                        return Err(ScriptError::new(
848                            ScriptErrorType::InvalidPatternDefinition,
849                            line.location.clone(),
850                        ));
851                    }
852                } else if line.text() == "end" {
853                    builder.patterns.push(OutputPattern {
854                        pattern: OutputPatternType::End,
855                        ignore: Default::default(),
856                        reject: Default::default(),
857                        location: line.location.clone(),
858                    });
859                } else if line.text() == "none" {
860                    builder.patterns.push(OutputPattern {
861                        pattern: OutputPatternType::None,
862                        ignore: Default::default(),
863                        reject: Default::default(),
864                        location: line.location.clone(),
865                    });
866                } else {
867                    return Err(ScriptError::new_with_data(
868                        ScriptErrorType::InvalidPattern,
869                        line.location.clone(),
870                        format!("{:?}", line.text()),
871                    ));
872                }
873            }
874        }
875        ScriptV0Segment::SubBlock(location, text, args, segments) => {
876            if text != "if" && !args.is_empty() {
877                return Err(ScriptError::new_with_data(
878                    ScriptErrorType::InvalidPattern,
879                    location.clone(),
880                    format!("{text} {args:?}"),
881                ));
882            }
883            if text == "reject" {
884                let next = parse_script_v0_segments(segments, grok)?;
885                if !next.ignore.is_empty() || !next.reject.is_empty() {
886                    return Err(ScriptError::new(
887                        ScriptErrorType::InvalidPattern,
888                        location.clone(),
889                    ));
890                }
891                builder.reject.extend(next.patterns);
892            } else if text == "ignore" {
893                let next = parse_script_v0_segments(segments, grok)?;
894                if !next.ignore.is_empty() || !next.reject.is_empty() {
895                    return Err(ScriptError::new(
896                        ScriptErrorType::InvalidPattern,
897                        location.clone(),
898                    ));
899                }
900                builder.ignore.extend(next.patterns);
901            } else if text == "if" {
902                let condition = parse_if_condition(location.clone(), args)?;
903                let new_builder = parse_script_v0_segments(segments, grok)?;
904                let pattern = OutputPattern {
905                    pattern: OutputPatternType::If(
906                        condition,
907                        Box::new(OutputPattern::new_sequence(
908                            location.clone(),
909                            new_builder.patterns,
910                        )),
911                    ),
912                    ignore: Arc::new(new_builder.ignore),
913                    reject: Arc::new(new_builder.reject),
914                    location: location.clone(),
915                };
916                builder.patterns.push(pattern);
917            } else {
918                let factory: &dyn Fn(&ScriptLocation, Vec<OutputPattern>) -> OutputPatternType =
919                    match text.as_str() {
920                        "repeat" => &|location, patterns| {
921                            OutputPatternType::Repeat(Box::new(OutputPattern::new_sequence(
922                                location.clone(),
923                                patterns,
924                            )))
925                        },
926                        "choice" => &|_location, patterns| OutputPatternType::Choice(patterns),
927                        "unordered" => {
928                            &|_location, patterns| OutputPatternType::Unordered(patterns)
929                        }
930                        "sequence" => &|_location, patterns| OutputPatternType::Sequence(patterns),
931                        "optional" => &|location, patterns| {
932                            OutputPatternType::Optional(Box::new(OutputPattern::new_sequence(
933                                location.clone(),
934                                patterns,
935                            )))
936                        },
937                        "*" => &|location: &ScriptLocation, patterns| {
938                            OutputPatternType::Any(Box::new(OutputPattern::new_sequence(
939                                location.clone(),
940                                patterns,
941                            )))
942                        },
943                        _ => {
944                            return Err(ScriptError::new_with_data(
945                                ScriptErrorType::InvalidPattern,
946                                location.clone(),
947                                text.to_string(),
948                            ));
949                        }
950                    };
951
952                let new_builder = parse_script_v0_segments(segments, grok)?;
953                let pattern = OutputPattern {
954                    pattern: factory(location, new_builder.patterns),
955                    ignore: Arc::new(new_builder.ignore),
956                    reject: Arc::new(new_builder.reject),
957                    location: location.clone(),
958                };
959                builder.patterns.push(pattern);
960            }
961        }
962        ScriptV0Segment::Semi(location, text, args) => {
963            return Err(ScriptError::new_with_data(
964                ScriptErrorType::UnsupportedCommandPosition,
965                location.clone(),
966                format!("{text} {args:?}"),
967            ));
968        }
969    }
970    Ok(())
971}
972
973fn parse_if_condition(
974    location: ScriptLocation,
975    args: &[ShellBit],
976) -> Result<IfCondition, ScriptError> {
977    if args.len() == 1 && args[0] == "true" {
978        Ok(IfCondition::True)
979    } else if args.len() == 1 && args[0] == "false" {
980        Ok(IfCondition::False)
981    } else if args.len() == 3 && args[1] == "==" {
982        Ok(IfCondition::EnvEq(
983            false,
984            args[0].to_string(),
985            args[2].clone(),
986        ))
987    } else if args.len() == 3 && args[1] == "!=" {
988        Ok(IfCondition::EnvEq(
989            true,
990            args[0].to_string(),
991            args[2].clone(),
992        ))
993    } else {
994        return Err(ScriptError::new_with_data(
995            ScriptErrorType::InvalidIfCondition,
996            location.clone(),
997            format!("{args:?}"),
998        ));
999    }
1000}
1001
1002fn parse_pattern_line(
1003    grok: &mut Grok,
1004    location: ScriptLocation,
1005    text: &str,
1006    line_start: char,
1007) -> Result<OutputPattern, ScriptError> {
1008    if text.is_empty() {
1009        return Ok(OutputPattern {
1010            pattern: OutputPatternType::Literal("".to_string()),
1011            ignore: Default::default(),
1012            reject: Default::default(),
1013            location,
1014        });
1015    }
1016
1017    let text = text.trim_end();
1018
1019    if line_start == '!' {
1020        if !text.contains("%") {
1021            return Ok(OutputPattern {
1022                pattern: OutputPatternType::Literal(text.to_string()),
1023                ignore: Default::default(),
1024                reject: Default::default(),
1025                location,
1026            });
1027        }
1028
1029        let pattern = GrokPattern::compile(grok, text, true).map_err(|e| {
1030            ScriptError::new_with_data(
1031                ScriptErrorType::InvalidPattern,
1032                location.clone(),
1033                e.to_string(),
1034            )
1035        })?;
1036        Ok(OutputPattern {
1037            pattern: OutputPatternType::Pattern(Arc::new(pattern)),
1038            ignore: Default::default(),
1039            reject: Default::default(),
1040            location,
1041        })
1042    } else if line_start == '?' {
1043        let text = if text.ends_with('$') {
1044            format!(r#"^{text}"#)
1045        } else {
1046            format!(r#"^{text}\s*$"#)
1047        };
1048        let pattern = GrokPattern::compile(grok, &text, false).map_err(|e| {
1049            ScriptError::new_with_data(
1050                ScriptErrorType::InvalidPattern,
1051                location.clone(),
1052                e.to_string(),
1053            )
1054        })?;
1055        Ok(OutputPattern {
1056            pattern: OutputPatternType::Pattern(Arc::new(pattern)),
1057            ignore: Default::default(),
1058            reject: Default::default(),
1059            location,
1060        })
1061    } else {
1062        unreachable!("Invalid line start: {line_start}");
1063    }
1064}
1065
1066#[cfg(test)]
1067mod tests {
1068    use super::*;
1069
1070    fn parse_pattern(pattern: &str) -> Result<OutputPattern, ScriptError> {
1071        let lines = ScriptLine::parse(ScriptFile::new("test.cli"), pattern);
1072        let segments = segment_script(true, &mut lines.as_slice()).unwrap();
1073        let normalized = normalize_segments(segments);
1074        Ok(
1075            parse_script_v0_segments(&normalized, &mut Grok::with_default_patterns())?
1076                .patterns
1077                .first()
1078                .unwrap()
1079                .clone(),
1080        )
1081    }
1082
1083    fn parse_lines(lines: &str) -> Result<Lines, ScriptError> {
1084        Ok(Lines::new(
1085            lines.lines().map(|l| l.to_string()).collect::<Vec<_>>(),
1086        ))
1087    }
1088
1089    #[test]
1090    fn test_v0_patterns() {
1091        let mut patterns = vec![];
1092        patterns.push(parse_pattern("! a\n! b\n! c\n").unwrap());
1093        patterns.push(parse_pattern("!!!\na\nb\nc\n!!!\n").unwrap());
1094
1095        let context = ScriptRunContext::default();
1096        let context = OutputMatchContext::new(&context);
1097        let output = parse_lines("a\nb\nc\n").unwrap();
1098
1099        for pattern in patterns {
1100            let result = pattern.matches(context.clone(), output.clone());
1101            assert!(result.is_ok());
1102        }
1103    }
1104
1105    #[test]
1106    fn test_v0_block_pattern() {
1107        let pattern = r#"
1108        repeat {
1109            choice {
1110    ? pattern1 %{DATA}
1111    ? pattern2 %{DATA}
1112    ? pattern3 %{DATA}
1113            }
1114        }
1115        "#;
1116        let grok = Grok::with_default_patterns();
1117        let file = ScriptFile::new("test.cli");
1118        let pattern = parse_pattern(pattern).unwrap();
1119        eprintln!("{pattern:?}");
1120    }
1121}