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 Command(CommandLine),
14 Ineffectual,
16 Pattern,
18 Meta,
20 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 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 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 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
105enum 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 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 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 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
244fn 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 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 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 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 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 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
422fn 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 if command_needs_end {
472 insert_virtual_end_block(last_line, &mut new_segments);
473 }
474
475 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 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 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}