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 #[expect(unused)]
208 fn is_meta_block(&self) -> bool {
209 match self {
210 ScriptV0Segment::Block(block) => block.block_type.is_meta(),
211 _ => false,
212 }
213 }
214
215 fn location(&self) -> &ScriptLocation {
216 match self {
217 ScriptV0Segment::Block(block) => &block.location,
218 ScriptV0Segment::SubBlock(location, ..) => location,
219 ScriptV0Segment::Semi(location, ..) => location,
220 }
221 }
222
223 #[allow(unused)]
224 fn last_location(&self) -> &ScriptLocation {
225 match self {
226 ScriptV0Segment::Block(block) => &block.lines.last().unwrap().location,
227 ScriptV0Segment::SubBlock(location, .., segments) => {
228 if let Some(last) = segments.last() {
229 last.last_location()
230 } else {
231 location
232 }
233 }
234 ScriptV0Segment::Semi(location, ..) => location,
235 }
236 }
237}
238
239pub fn parse_script(file_name: ScriptFile, script: &str) -> Result<Script, ScriptError> {
240 let lines = ScriptLine::parse(file_name.clone(), script);
241 let segments = segment_script(true, &mut lines.as_slice())?;
242 let normalized = normalize_segments(segments);
243 parse_normalized_script_v0(&normalized, file_name)
244}
245
246fn segment_script(
249 top_level: bool,
250 lines_slice: &mut &[ScriptLine],
251) -> Result<Vec<ScriptV0Segment>, ScriptError> {
252 let mut segments = Vec::new();
253 let mut current_segment = None;
254
255 fn is_subblock(text: &str) -> Option<(bool, &str, &str)> {
256 if text.starts_with(|c: char| c.is_alphabetic()) {
258 let is_semi = text.ends_with(';');
259 text.strip_suffix(|c: char| c == '{' || c == ';')
260 .map(|text| {
261 if let Some((block_type, args)) = text.trim().split_once(char::is_whitespace) {
262 (is_semi, block_type.trim(), args.trim())
263 } else {
264 (is_semi, text.trim(), "")
265 }
266 })
267 } else {
268 None
269 }
270 }
271
272 let mut lines = lines_slice.iter();
273 let orig_slice = *lines_slice;
274 let mut multiline_terminator = None;
275 while let Some(line) = lines.next() {
276 if let Some(terminator) = multiline_terminator {
277 if line.text() == terminator {
278 multiline_terminator = None;
279 }
280 } else if line.text() == "!!!" {
281 multiline_terminator = Some("!!!");
282 } else if line.text() == "???" {
283 multiline_terminator = Some("???");
284 }
285
286 if line.starts_with("$") {
289 if let Some(segment) = current_segment.take() {
290 segments.push(ScriptV0Segment::Block(segment));
291 }
292 let mut block_lines = vec![line.clone()];
293 let mut command = line.text()[1..].trim().to_string();
294 let mut line_count = 1;
295 let command = loop {
296 match parse_command_line(line.location.clone(), line_count, &command) {
297 Ok(command) => break command,
298 Err(e @ ScriptErrorType::UnclosedQuote)
299 | Err(e @ ScriptErrorType::UnclosedBackslash) => match lines.next() {
300 Some(line) => {
301 block_lines.push(line.clone());
302 command.push('\n');
303 command.push_str(line.text());
304 line_count += 1;
305 }
306 None => {
307 return Err(ScriptError::new(e, line.location.clone()));
308 }
309 },
310 Err(e) => {
311 return Err(ScriptError::new(e, line.location.clone()));
312 }
313 }
314 };
315
316 segments.push(ScriptV0Segment::Block(ScriptV0Block {
317 block_type: BlockType::Command(command),
318 lines: block_lines,
319 location: line.location.clone(),
320 }));
321 } else if let Some((is_semi, block_type, args)) = is_subblock(line.text()) {
322 if let Some(segment) = current_segment.take() {
323 segments.push(ScriptV0Segment::Block(segment));
324 }
325
326 let args = shell_split(args).map_err(|_| {
327 ScriptError::new_with_data(
328 ScriptErrorType::InvalidBlockArgs,
329 line.location.clone(),
330 format!("{block_type} {args}"),
331 )
332 })?;
333
334 if is_semi {
335 segments.push(ScriptV0Segment::Semi(
336 line.location.clone(),
337 block_type.to_string(),
338 args,
339 ));
340 } else {
341 let mut rest = lines.as_slice();
343 if rest.is_empty() {
344 return Err(ScriptError::new(
345 ScriptErrorType::InvalidBlockEnd,
346 line.location.clone(),
347 ));
348 }
349
350 segments.push(ScriptV0Segment::SubBlock(
351 line.location.clone(),
352 block_type.to_string(),
353 args,
354 segment_script(false, &mut rest)?,
355 ));
356 lines = rest.iter();
357 }
358 } else if line.text() == "}" {
359 if top_level {
362 return Err(ScriptError::new(
363 ScriptErrorType::InvalidBlockEnd,
364 line.location.clone(),
365 ));
366 }
367 *lines_slice = lines.as_slice();
368 if let Some(segment) = current_segment.take() {
369 segments.push(ScriptV0Segment::Block(segment));
370 }
371 return Ok(segments);
372 } else {
373 let block_type = if multiline_terminator.is_some() {
375 BlockType::Pattern
376 } else if line.starts_with("#") || line.is_empty() {
377 BlockType::Ineffectual
378 } else if line.starts_with("%") {
379 BlockType::Meta
380 } else if line.starts_with("*") {
381 BlockType::Any
382 } else {
383 BlockType::Pattern
384 };
385
386 let segment = current_segment.get_or_insert(ScriptV0Block {
387 block_type: block_type.clone(),
388 lines: Vec::new(),
389 location: line.location.clone(),
390 });
391 if !segment.block_type.is_same_type_as(&block_type) {
392 segments.push(ScriptV0Segment::Block(
393 segment.take(line.location.clone(), block_type),
394 ));
395 }
396 segment.lines.push(line.clone());
397 }
398 }
399
400 if !top_level {
401 return Err(ScriptError::new(
402 ScriptErrorType::InvalidBlockEnd,
403 orig_slice.last().unwrap().location.clone(),
404 ));
405 }
406
407 if let Some(segment) = current_segment.take() {
408 segments.push(ScriptV0Segment::Block(segment));
409 }
410
411 Ok(segments)
412}
413
414fn insert_virtual_end_block(location: ScriptLocation, segments: &mut Vec<ScriptV0Segment>) {
415 let line = ScriptLine::new(location.file.clone(), location.line - 1, "end");
416
417 segments.push(ScriptV0Segment::Block(ScriptV0Block {
418 location: line.location.clone(),
419 block_type: BlockType::Pattern,
420 lines: vec![line],
421 }));
422}
423
424fn normalize_segments(segments: Vec<ScriptV0Segment>) -> Vec<ScriptV0Segment> {
426 let mut new_segments = vec![];
427 let mut command_needs_end = false;
428
429 let Some(last_line) = segments.last().map(|segment| segment.location().clone()) else {
430 return segments;
431 };
432
433 for mut segment in segments {
434 if segment.is_command_block() && command_needs_end {
435 insert_virtual_end_block(segment.location().clone(), &mut new_segments);
436 command_needs_end = false;
437 }
438 match segment {
439 ScriptV0Segment::Block(ref mut block) => {
440 debug_assert!(
441 !block.lines.is_empty(),
442 "empty blocks should not exist here"
443 );
444 if block.block_type.is_ineffectual() {
445 continue;
446 }
447 if block.block_type.is_command() {
448 command_needs_end = true;
449 }
450 if let Some(ScriptV0Segment::Block(last_block)) = new_segments.last_mut() {
451 if block.block_type.is_command() {
452 new_segments.push(segment);
453 } else if block.block_type.is_same_type_as(&last_block.block_type) {
454 last_block.lines.extend(std::mem::take(&mut block.lines));
455 } else {
456 new_segments.push(segment);
457 }
458 } else {
459 new_segments.push(segment);
460 }
461 }
462 ScriptV0Segment::SubBlock(location, text, args, segments) => {
463 let normalized = normalize_segments(segments);
464 new_segments.push(ScriptV0Segment::SubBlock(location, text, args, normalized));
465 }
466 ScriptV0Segment::Semi(location, text, args) => {
467 new_segments.push(ScriptV0Segment::Semi(location, text, args));
468 }
469 }
470 }
471
472 if command_needs_end {
474 insert_virtual_end_block(last_line, &mut new_segments);
475 }
476
477 let mut i = 0;
479 while i < new_segments.len() {
480 if let ScriptV0Segment::Block(block) = &mut new_segments[i] {
481 if block.block_type.is_any() {
482 let location = block.location.clone();
483 new_segments[i] =
484 ScriptV0Segment::SubBlock(location.clone(), "*".to_string(), vec![], vec![]);
485
486 if i + 1 < new_segments.len() {
487 if let Some(first) = new_segments[i + 1].split_first() {
488 new_segments[i] = ScriptV0Segment::SubBlock(
489 location.clone(),
490 "*".to_string(),
491 vec![],
492 vec![first],
493 );
494 }
495 }
496 }
497 }
498 if new_segments[i].is_empty() {
499 new_segments.remove(i);
500 } else {
501 i += 1;
502 }
503 }
504
505 new_segments
506}
507
508pub fn parse_command_line(
509 location: ScriptLocation,
510 line_count: usize,
511 command: &str,
512) -> Result<CommandLine, ScriptErrorType> {
513 let command_str = command.to_string();
514 const SEPARATORS: &[&str] = &[
516 "&&", "||", "1>&2", "2>&1", "1>", "2>", "&", "|", ";", "(", ")", ">", "<", "=",
517 ];
518 let command = match shellish_parse::multiparse(
519 command,
520 shellish_parse::ParseOptions::default(),
521 SEPARATORS,
522 ) {
523 Ok(command) => command,
524 Err(shellish_parse::ParseError::DanglingString) => {
525 return Err(ScriptErrorType::UnclosedQuote);
526 }
527 Err(shellish_parse::ParseError::DanglingBackslash) => {
528 return Err(ScriptErrorType::UnclosedBackslash);
529 }
530 _ => {
531 return Err(ScriptErrorType::IllegalShellCommand);
532 }
533 };
534 let mut command_bits = vec![];
535 for (_, seperator) in command {
536 if let Some(seperator) = seperator {
537 if SEPARATORS[seperator] == "&" {
538 return Err(ScriptErrorType::BackgroundProcessNotAllowed);
539 }
540 if SEPARATORS[seperator] == ">&" {
541 return Err(ScriptErrorType::UnsupportedRedirection);
542 }
543 command_bits.push(SEPARATORS[seperator].to_string());
544 }
545 }
546
547 Ok(CommandLine::new(command_str, location, line_count))
548}
549
550#[derive(Default)]
551struct OutputPatternBuilder {
552 ignore: Vec<OutputPattern>,
553 reject: Vec<OutputPattern>,
554 patterns: Vec<OutputPattern>,
555}
556
557fn parse_normalized_script_v0(
558 segments: &[ScriptV0Segment],
559 file: ScriptFile,
560) -> Result<Script, ScriptError> {
561 let preamble_index = segments
564 .iter()
565 .position(|segment| segment.is_command_block())
566 .unwrap_or(segments.len());
567 let (preamble, rest) = segments.split_at(preamble_index);
568
569 let mut grok = Grok::with_default_patterns();
570
571 let builder = parse_script_v0_segments(preamble, &mut grok)?;
572 if let Some(pattern) = builder.patterns.first() {
573 return Err(ScriptError::new(
574 ScriptErrorType::InvalidGlobalPattern,
575 pattern.location.clone(),
576 ));
577 }
578 let global_ignore = builder.ignore;
579 let global_reject = builder.reject;
580
581 let commands =
582 parse_normalized_script_v0_commands(rest, &mut grok, &global_ignore, &global_reject)?;
583
584 Ok(Script {
585 commands,
586 file,
587 grok,
588 })
589}
590
591fn parse_normalized_script_v0_commands(
592 mut segments: &[ScriptV0Segment],
593 grok: &mut Grok,
594 global_ignore: &Vec<OutputPattern>,
595 global_reject: &Vec<OutputPattern>,
596) -> Result<Vec<ScriptBlock>, ScriptError> {
597 let mut commands = vec![];
598 while let Some((command, remaining)) = segments.split_first() {
599 debug_assert!(
600 command.is_command_block(),
601 "not a command block: {command:?}"
602 );
603
604 if let ScriptV0Segment::SubBlock(_, block_type, args, sub_segments) = command {
605 let blocks = parse_normalized_script_v0_commands(
606 sub_segments,
607 grok,
608 global_ignore,
609 global_reject,
610 )?;
611
612 if block_type == "if" {
613 let condition = parse_if_condition(command.location().clone(), args)?;
614 commands.push(ScriptBlock::If(condition, blocks));
615 } else if block_type == "for" {
616 if args.len() >= 3 && args[1] == "in" {
617 commands.push(ScriptBlock::For(
618 ForCondition::Env(args[0].to_string(), args[2..].to_vec()),
619 blocks,
620 ));
621 } else {
622 return Err(ScriptError::new_with_data(
623 ScriptErrorType::InvalidBlockType,
624 command.location().clone(),
625 format!("for {args:?}"),
626 ));
627 }
628 } else if block_type == "background" {
629 commands.push(ScriptBlock::Background(blocks));
630 } else if block_type == "retry" {
631 commands.push(ScriptBlock::Retry(blocks));
632 } else if block_type == "defer" {
633 commands.push(ScriptBlock::Defer(blocks));
634 } else {
635 return Err(ScriptError::new_with_data(
636 ScriptErrorType::InvalidBlockType,
637 command.location().clone(),
638 block_type.clone(),
639 ));
640 }
641
642 segments = remaining;
643 continue;
644 }
645
646 if let ScriptV0Segment::Semi(location, text, args) = command {
647 segments = remaining;
648 if text == "using" {
649 if args.len() == 1 && args[0] == "tempdir" {
650 commands.push(ScriptBlock::InternalCommand(InternalCommand::UsingTempdir));
651 continue;
652 }
653 if args.len() == 2 && args[0] == "dir" {
654 commands.push(ScriptBlock::InternalCommand(InternalCommand::UsingDir(
655 args[1].clone(),
656 false,
657 )));
658 continue;
659 }
660 if args.len() == 3 && args[0] == "new" && args[1] == "dir" {
661 commands.push(ScriptBlock::InternalCommand(InternalCommand::UsingDir(
662 args[2].clone(),
663 true,
664 )));
665 continue;
666 }
667 }
668 if text == "cd" && args.len() == 1 {
669 commands.push(ScriptBlock::InternalCommand(InternalCommand::ChangeDir(
670 args[0].clone(),
671 )));
672 continue;
673 }
674 if text == "set" && args.len() == 2 {
675 commands.push(ScriptBlock::InternalCommand(InternalCommand::Set(
676 args[0].to_string(),
677 args[1].clone(),
678 )));
679 continue;
680 }
681 return Err(ScriptError::new_with_data(
682 ScriptErrorType::InvalidInternalCommand,
683 location.clone(),
684 format!("{text} {args:?}"),
685 ));
686 }
687
688 let next_command = remaining
689 .iter()
690 .position(|segment| segment.is_command_block())
691 .unwrap_or(remaining.len());
692 let mut pattern;
693 (pattern, segments) = remaining.split_at(next_command);
694
695 let location = command.location().clone();
696 let mut command = ScriptCommand {
697 command: match command {
698 ScriptV0Segment::Block(block) => block.block_type.clone().unwrap_command(),
699 _ => unreachable!(),
700 },
701 pattern: OutputPattern {
702 pattern: OutputPatternType::None,
703 ignore: Default::default(),
704 reject: Default::default(),
705 location: location.clone(),
706 },
707 exit: CommandExit::Success,
708 timeout: None,
709 expect_failure: false,
710 set_var: None,
711 };
712
713 if let Some(ScriptV0Segment::Block(maybe_meta)) = pattern.first() {
714 if maybe_meta.block_type.is_meta() {
715 pattern = pattern.split_first().unwrap().1;
716
717 for line in maybe_meta.lines.iter() {
718 if line.starts_with("%SET") {
719 if let Some(var) = line.text()[4..].split_whitespace().next() {
720 command.set_var = Some(var.to_string());
721 } else {
722 return Err(ScriptError::new(
723 ScriptErrorType::InvalidSetVariable,
724 line.location.clone(),
725 ));
726 }
727 } else if line.starts_with("%EXPECT_FAILURE") {
728 command.expect_failure = true;
729 } else if line.starts_with("%EXIT any") {
730 command.exit = CommandExit::Any;
731 } else if line.starts_with("%EXIT fail") {
732 command.exit = CommandExit::AnyFailure;
733 } else if line.starts_with("%EXIT timeout") {
734 command.exit = CommandExit::Timeout;
735 } else if line.starts_with("%EXIT ") {
736 if let Ok(status) = line.text()[6..].parse::<i32>() {
737 command.exit = CommandExit::Failure(status);
738 } else {
739 return Err(ScriptError::new(
740 ScriptErrorType::InvalidExitStatus,
741 line.location.clone(),
742 ));
743 }
744 } else if line.starts_with("%TIMEOUT ") {
745 if let Ok(timeout) = humantime::parse_duration(&line.text()[9..]) {
746 command.timeout = Some(timeout);
747 } else {
748 return Err(ScriptError::new(
749 ScriptErrorType::InvalidMetaCommand,
750 line.location.clone(),
751 ));
752 }
753 } else {
754 return Err(ScriptError::new_with_data(
755 ScriptErrorType::InvalidMetaCommand,
756 line.location.clone(),
757 format!("{line:?}"),
758 ));
759 }
760 }
761 }
762 }
763
764 let builder = parse_script_v0_segments(pattern, grok)?;
765 command.pattern = OutputPattern::new_sequence(location, builder.patterns);
766 command.pattern.ignore = global_ignore
767 .iter()
768 .cloned()
769 .chain(builder.ignore.iter().cloned())
770 .collect::<Vec<_>>()
771 .into();
772 command.pattern.reject = global_reject
773 .iter()
774 .cloned()
775 .chain(builder.reject.iter().cloned())
776 .collect::<Vec<_>>()
777 .into();
778 commands.push(ScriptBlock::Command(command));
779 }
780 Ok(commands)
781}
782
783fn parse_script_v0_segments(
784 segments: &[ScriptV0Segment],
785 grok: &mut Grok,
786) -> Result<OutputPatternBuilder, ScriptError> {
787 let mut builder = OutputPatternBuilder::default();
788 for segment in segments {
789 parse_script_v0_segment(grok, &mut builder, segment)?;
790 }
791 Ok(builder)
792}
793
794fn parse_script_v0_segment(
795 grok: &mut Grok,
796 builder: &mut OutputPatternBuilder,
797 segment: &ScriptV0Segment,
798) -> Result<(), ScriptError> {
799 if segment.is_command_block() {
800 return Err(ScriptError::new(
801 ScriptErrorType::UnsupportedCommandPosition,
802 segment.location().clone(),
803 ));
804 }
805 match segment {
806 ScriptV0Segment::Block(block) => {
807 let mut pattern = block.lines.as_slice();
808 while let Some((line, rest)) = pattern.split_first() {
809 pattern = rest;
810 if line.text() == "!!!" {
811 let indent = line.text_untrimmed().find("!!!").unwrap();
812 while let Some((line, rest)) = pattern.split_first() {
813 pattern = rest;
814 if line.text() == "!!!" {
815 break;
816 } else {
817 builder.patterns.push(parse_pattern_line(
818 grok,
819 line.location.clone(),
820 &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
821 '!',
822 )?);
823 }
824 }
825 } else if line.text() == "???" {
826 let indent = line.text_untrimmed().find("???").unwrap();
827 while let Some((line, rest)) = pattern.split_first() {
828 pattern = rest;
829 if line.text() == "???" {
830 break;
831 } else {
832 builder.patterns.push(parse_pattern_line(
833 grok,
834 line.location.clone(),
835 &line.text_untrimmed()[indent.min(line.text_untrimmed().len())..],
836 '?',
837 )?);
838 }
839 }
840 } else if line.text() == "!" || line.text() == "?" {
841 builder.patterns.push(parse_pattern_line(
842 grok,
843 line.location.clone(),
844 "",
845 line.first_char().unwrap(),
846 )?);
847 } else if line.starts_with("! ") || line.starts_with("? ") {
848 builder.patterns.push(parse_pattern_line(
849 grok,
850 line.location.clone(),
851 &line.text()[2..],
852 line.first_char().unwrap(),
853 )?);
854 } else if let Some(pattern) = line.strip_prefix("pattern ") {
855 if let Some((name, pattern)) = pattern.split_once(' ') {
856 grok.add_pattern(name, pattern);
857 } else {
858 return Err(ScriptError::new(
859 ScriptErrorType::InvalidPatternDefinition,
860 line.location.clone(),
861 ));
862 }
863 } else if line.text() == "end" {
864 builder.patterns.push(OutputPattern {
865 pattern: OutputPatternType::End,
866 ignore: Default::default(),
867 reject: Default::default(),
868 location: line.location.clone(),
869 });
870 } else if line.text() == "none" {
871 builder.patterns.push(OutputPattern {
872 pattern: OutputPatternType::None,
873 ignore: Default::default(),
874 reject: Default::default(),
875 location: line.location.clone(),
876 });
877 } else {
878 return Err(ScriptError::new_with_data(
879 ScriptErrorType::InvalidPattern,
880 line.location.clone(),
881 format!("{:?}", line.text()),
882 ));
883 }
884 }
885 }
886 ScriptV0Segment::SubBlock(location, text, args, segments) => {
887 if text != "if" && !args.is_empty() {
888 return Err(ScriptError::new_with_data(
889 ScriptErrorType::InvalidPattern,
890 location.clone(),
891 format!("{text} {args:?}"),
892 ));
893 }
894 if text == "reject" {
895 let next = parse_script_v0_segments(segments, grok)?;
896 if !next.ignore.is_empty() || !next.reject.is_empty() {
897 return Err(ScriptError::new(
898 ScriptErrorType::InvalidPattern,
899 location.clone(),
900 ));
901 }
902 builder.reject.extend(next.patterns);
903 } else if text == "ignore" {
904 let next = parse_script_v0_segments(segments, grok)?;
905 if !next.ignore.is_empty() || !next.reject.is_empty() {
906 return Err(ScriptError::new(
907 ScriptErrorType::InvalidPattern,
908 location.clone(),
909 ));
910 }
911 builder.ignore.extend(next.patterns);
912 } else if text == "if" {
913 let condition = parse_if_condition(location.clone(), args)?;
914 let new_builder = parse_script_v0_segments(segments, grok)?;
915 let pattern = OutputPattern {
916 pattern: OutputPatternType::If(
917 condition,
918 Box::new(OutputPattern::new_sequence(
919 location.clone(),
920 new_builder.patterns,
921 )),
922 ),
923 ignore: Arc::new(new_builder.ignore),
924 reject: Arc::new(new_builder.reject),
925 location: location.clone(),
926 };
927 builder.patterns.push(pattern);
928 } else {
929 let factory: &dyn Fn(&ScriptLocation, Vec<OutputPattern>) -> OutputPatternType =
930 match text.as_str() {
931 "repeat" => &|location, patterns| {
932 OutputPatternType::Repeat(Box::new(OutputPattern::new_sequence(
933 location.clone(),
934 patterns,
935 )))
936 },
937 "choice" => &|_location, patterns| OutputPatternType::Choice(patterns),
938 "unordered" => {
939 &|_location, patterns| OutputPatternType::Unordered(patterns)
940 }
941 "sequence" => &|_location, patterns| OutputPatternType::Sequence(patterns),
942 "optional" => &|location, patterns| {
943 OutputPatternType::Optional(Box::new(OutputPattern::new_sequence(
944 location.clone(),
945 patterns,
946 )))
947 },
948 "*" => &|location: &ScriptLocation, patterns| {
949 OutputPatternType::Any(Box::new(OutputPattern::new_sequence(
950 location.clone(),
951 patterns,
952 )))
953 },
954 _ => {
955 return Err(ScriptError::new_with_data(
956 ScriptErrorType::InvalidPattern,
957 location.clone(),
958 text.to_string(),
959 ));
960 }
961 };
962
963 let new_builder = parse_script_v0_segments(segments, grok)?;
964 let pattern = OutputPattern {
965 pattern: factory(location, new_builder.patterns),
966 ignore: Arc::new(new_builder.ignore),
967 reject: Arc::new(new_builder.reject),
968 location: location.clone(),
969 };
970 builder.patterns.push(pattern);
971 }
972 }
973 ScriptV0Segment::Semi(location, text, args) => {
974 return Err(ScriptError::new_with_data(
975 ScriptErrorType::UnsupportedCommandPosition,
976 location.clone(),
977 format!("{text} {args:?}"),
978 ));
979 }
980 }
981 Ok(())
982}
983
984fn parse_if_condition(
985 location: ScriptLocation,
986 args: &[ShellBit],
987) -> Result<IfCondition, ScriptError> {
988 if args.len() == 1 && args[0] == "true" {
989 Ok(IfCondition::True)
990 } else if args.len() == 1 && args[0] == "false" {
991 Ok(IfCondition::False)
992 } else if args.len() == 3 && args[1] == "==" {
993 Ok(IfCondition::EnvEq(
994 false,
995 args[0].to_string(),
996 args[2].clone(),
997 ))
998 } else if args.len() == 3 && args[1] == "!=" {
999 Ok(IfCondition::EnvEq(
1000 true,
1001 args[0].to_string(),
1002 args[2].clone(),
1003 ))
1004 } else {
1005 return Err(ScriptError::new_with_data(
1006 ScriptErrorType::InvalidIfCondition,
1007 location.clone(),
1008 format!("{args:?}"),
1009 ));
1010 }
1011}
1012
1013fn parse_pattern_line(
1014 grok: &mut Grok,
1015 location: ScriptLocation,
1016 text: &str,
1017 line_start: char,
1018) -> Result<OutputPattern, ScriptError> {
1019 if text.is_empty() {
1020 return Ok(OutputPattern {
1021 pattern: OutputPatternType::Literal("".to_string()),
1022 ignore: Default::default(),
1023 reject: Default::default(),
1024 location,
1025 });
1026 }
1027
1028 let text = text.trim_end();
1029
1030 if line_start == '!' {
1031 if !text.contains("%") {
1032 return Ok(OutputPattern {
1033 pattern: OutputPatternType::Literal(text.to_string()),
1034 ignore: Default::default(),
1035 reject: Default::default(),
1036 location,
1037 });
1038 }
1039
1040 let pattern = GrokPattern::compile(grok, text, true).map_err(|e| {
1041 ScriptError::new_with_data(
1042 ScriptErrorType::InvalidPattern,
1043 location.clone(),
1044 e.to_string(),
1045 )
1046 })?;
1047 Ok(OutputPattern {
1048 pattern: OutputPatternType::Pattern(Arc::new(pattern)),
1049 ignore: Default::default(),
1050 reject: Default::default(),
1051 location,
1052 })
1053 } else if line_start == '?' {
1054 let text = if text.ends_with('$') {
1055 format!(r#"^{text}"#)
1056 } else {
1057 format!(r#"^{text}\s*$"#)
1058 };
1059 let pattern = GrokPattern::compile(grok, &text, false).map_err(|e| {
1060 ScriptError::new_with_data(
1061 ScriptErrorType::InvalidPattern,
1062 location.clone(),
1063 e.to_string(),
1064 )
1065 })?;
1066 Ok(OutputPattern {
1067 pattern: OutputPatternType::Pattern(Arc::new(pattern)),
1068 ignore: Default::default(),
1069 reject: Default::default(),
1070 location,
1071 })
1072 } else {
1073 unreachable!("Invalid line start: {line_start}");
1074 }
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use super::*;
1080
1081 fn parse_pattern(pattern: &str) -> Result<OutputPattern, ScriptError> {
1082 let lines = ScriptLine::parse(ScriptFile::new("test.cli"), pattern);
1083 let segments = segment_script(true, &mut lines.as_slice()).unwrap();
1084 let normalized = normalize_segments(segments);
1085 Ok(
1086 parse_script_v0_segments(&normalized, &mut Grok::with_default_patterns())?
1087 .patterns
1088 .first()
1089 .unwrap()
1090 .clone(),
1091 )
1092 }
1093
1094 fn parse_lines(lines: &str) -> Result<Lines, ScriptError> {
1095 Ok(Lines::new(
1096 lines.lines().map(|l| l.to_string()).collect::<Vec<_>>(),
1097 ))
1098 }
1099
1100 #[test]
1101 fn test_v0_patterns() {
1102 let mut patterns = vec![];
1103 patterns.push(parse_pattern("! a\n! b\n! c\n").unwrap());
1104 patterns.push(parse_pattern("!!!\na\nb\nc\n!!!\n").unwrap());
1105
1106 let context = ScriptRunContext::default();
1107 let context = OutputMatchContext::new(&context);
1108 let output = parse_lines("a\nb\nc\n").unwrap();
1109
1110 for pattern in patterns {
1111 let result = pattern.matches(context.clone(), output.clone());
1112 assert!(result.is_ok());
1113 }
1114 }
1115
1116 #[test]
1117 fn test_v0_block_pattern() {
1118 let pattern = r#"
1119 repeat {
1120 choice {
1121 ? pattern1 %{DATA}
1122 ? pattern2 %{DATA}
1123 ? pattern3 %{DATA}
1124 }
1125 }
1126 "#;
1127 let pattern = parse_pattern(pattern).unwrap();
1128 eprintln!("{pattern:?}");
1129 }
1130}