1use crate::ast::{
2 Guard, GuardExpr, IoBinding, IoStream, PlatformGuard, Step, StepKind, WorkspaceTarget,
3};
4use crate::lexer::{self, RawToken, Rule};
5use anyhow::{Result, anyhow, bail};
6use pest::iterators::Pair;
7use std::collections::VecDeque;
8
9#[derive(Clone)]
10struct ScopeFrame {
11 line_no: usize,
12 had_command: bool,
13}
14
15#[derive(Clone)]
16struct PendingIoBlock {
17 line_no: usize,
18 bindings: Vec<IoBinding>,
19 guards: Option<GuardExpr>,
20}
21
22#[derive(Clone)]
23struct IoScopeFrame {
24 line_no: usize,
25 had_command: bool,
26 bindings: Vec<IoBinding>,
27 guards: Option<GuardExpr>,
28}
29
30#[derive(Clone, Copy)]
31enum BlockKind {
32 Guard,
33 Io,
34}
35
36#[derive(Default)]
37struct IoBindingSet {
38 stdin: Option<IoBinding>,
39 stdout: Option<IoBinding>,
40 stderr: Option<IoBinding>,
41}
42
43impl IoBindingSet {
44 fn insert(&mut self, binding: IoBinding) {
45 match binding.stream {
46 IoStream::Stdin => self.stdin = Some(binding),
47 IoStream::Stdout => self.stdout = Some(binding),
48 IoStream::Stderr => self.stderr = Some(binding),
49 }
50 }
51
52 fn into_vec(self) -> Vec<IoBinding> {
53 let mut out = Vec::new();
54 if let Some(binding) = self.stdin {
55 out.push(binding);
56 }
57 if let Some(binding) = self.stdout {
58 out.push(binding);
59 }
60 if let Some(binding) = self.stderr {
61 out.push(binding);
62 }
63 out
64 }
65}
66
67pub struct ScriptParser<'a> {
68 tokens: VecDeque<RawToken<'a>>,
69 steps: Vec<Step>,
70 guard_stack: Vec<Option<GuardExpr>>,
71 pending_guards: Option<GuardExpr>,
72 pending_inline_guards: Option<GuardExpr>,
73 pending_can_open_block: bool,
74 pending_scope_enters: usize,
75 scope_stack: Vec<ScopeFrame>,
76 pending_io_block: Option<PendingIoBlock>,
77 io_scope_stack: Vec<IoScopeFrame>,
78 block_stack: Vec<BlockKind>,
79}
80
81impl<'a> ScriptParser<'a> {
82 pub fn new(input: &'a str) -> Result<Self> {
83 let tokens = VecDeque::from(lexer::tokenize(input)?);
84 Ok(Self {
85 tokens,
86 steps: Vec::new(),
87 guard_stack: vec![None],
88 pending_guards: None,
89 pending_inline_guards: None,
90 pending_can_open_block: false,
91 pending_scope_enters: 0,
92 scope_stack: Vec::new(),
93 pending_io_block: None,
94 io_scope_stack: Vec::new(),
95 block_stack: Vec::new(),
96 })
97 }
98
99 pub fn parse(mut self) -> Result<Vec<Step>> {
100 while let Some(token) = self.tokens.pop_front() {
101 if self.pending_io_block.is_some() && !matches!(token, RawToken::BlockStart { .. }) {
102 let pending = self.pending_io_block.take().unwrap();
103 bail!(
104 "line {}: WITH_IO block must be followed by '{{'",
105 pending.line_no
106 );
107 }
108 match token {
109 RawToken::Guard { pair, line_end } => {
110 let groups = parse_guard_line(pair)?;
111 self.handle_guard_token(line_end, groups)?
112 }
113 RawToken::BlockStart { line_no } => self.start_block(line_no)?,
114 RawToken::BlockEnd { line_no } => self.end_block(line_no)?,
115 RawToken::Command { pair, line_no } => {
116 let kind = parse_command(pair)?;
117 self.handle_command_token(line_no, kind)?
118 }
119 }
120 }
121
122 if let Some(pending) = self.pending_io_block.take() {
123 bail!(
124 "line {}: WITH_IO block must be followed by '{{'",
125 pending.line_no
126 );
127 }
128
129 if self.guard_stack.len() != 1 {
130 bail!("unclosed guard block at end of script");
131 }
132 if self.pending_guards.is_some() {
133 bail!("guard declared on final lines without a following command");
134 }
135
136 if let Some(frame) = self.io_scope_stack.last() {
137 bail!(
138 "WITH_IO block starting on line {} was not closed",
139 frame.line_no
140 );
141 }
142
143 {
146 let mut seen_non_prelude = false;
147 let mut inherit_count = 0usize;
148 for step in &self.steps {
149 match &step.kind {
150 StepKind::InheritEnv { .. } => {
151 if seen_non_prelude {
152 bail!("INHERIT_ENV must appear before any other commands");
153 }
154 if step.guard.is_some() || step.scope_enter > 0 || step.scope_exit > 0 {
155 bail!("INHERIT_ENV cannot be guarded or nested inside blocks");
156 }
157 inherit_count += 1;
158 }
159 kind => {
160 if contains_inherit_env(kind) {
161 bail!("INHERIT_ENV cannot be nested inside other commands");
162 }
163 seen_non_prelude = true;
164 }
165 }
166 }
167 if inherit_count > 1 {
168 bail!("only one INHERIT_ENV directive is allowed");
169 }
170 }
171
172 Ok(self.steps)
173 }
174
175 fn handle_guard_token(&mut self, line_end: usize, expr: GuardExpr) -> Result<()> {
176 if let Some(RawToken::Command { line_no, .. }) = self.tokens.front()
177 && *line_no == line_end
178 {
179 self.pending_inline_guards = Some(expr);
180 self.pending_can_open_block = false;
181 return Ok(());
182 }
183 self.stash_pending_guard(expr);
184 self.pending_can_open_block = true;
185 Ok(())
186 }
187
188 fn handle_command_token(&mut self, line_no: usize, kind: StepKind) -> Result<()> {
189 let inline = self.pending_inline_guards.take();
190 self.handle_command(line_no, kind, inline)
191 }
192
193 fn stash_pending_guard(&mut self, guard: GuardExpr) {
194 self.pending_guards = Some(if let Some(existing) = self.pending_guards.take() {
195 GuardExpr::all(vec![existing, guard])
196 } else {
197 guard
198 });
199 }
200
201 fn start_guard_block_from_pending(&mut self, line_no: usize) -> Result<()> {
202 let guards = self
203 .pending_guards
204 .take()
205 .ok_or_else(|| anyhow!("line {}: '{{' without a pending guard", line_no))?;
206 if !self.pending_can_open_block {
207 bail!("line {}: '{{' must directly follow a guard", line_no);
208 }
209 self.pending_can_open_block = false;
210 self.enter_guard_block(guards, line_no)
211 }
212
213 fn enter_guard_block(&mut self, guard: GuardExpr, line_no: usize) -> Result<()> {
214 let composed = if let Some(pending) = self.pending_guards.take() {
215 GuardExpr::all(vec![pending, guard])
216 } else {
217 guard
218 };
219 let parent = self.guard_stack.last().cloned().unwrap_or(None);
220 let next = and_guard_exprs(parent, Some(composed));
221 self.guard_stack.push(next);
222 self.scope_stack.push(ScopeFrame {
223 line_no,
224 had_command: false,
225 });
226 self.pending_scope_enters += 1;
227 Ok(())
228 }
229
230 fn begin_io_block(
231 &mut self,
232 line_no: usize,
233 bindings: Vec<IoBinding>,
234 guards: Option<GuardExpr>,
235 ) -> Result<()> {
236 if self.pending_io_block.is_some() {
237 bail!(
238 "line {}: previous WITH_IO block is still waiting for '{{'",
239 line_no
240 );
241 }
242 self.pending_io_block = Some(PendingIoBlock {
243 line_no,
244 bindings,
245 guards,
246 });
247 Ok(())
248 }
249
250 fn start_block(&mut self, line_no: usize) -> Result<()> {
251 if let Some(pending) = self.pending_io_block.take() {
252 self.block_stack.push(BlockKind::Io);
253 self.io_scope_stack.push(IoScopeFrame {
254 line_no: pending.line_no,
255 had_command: false,
256 bindings: pending.bindings,
257 guards: pending.guards,
258 });
259 Ok(())
260 } else {
261 self.start_guard_block_from_pending(line_no)?;
262 self.block_stack.push(BlockKind::Guard);
263 Ok(())
264 }
265 }
266
267 fn end_block(&mut self, line_no: usize) -> Result<()> {
268 let kind = self
269 .block_stack
270 .pop()
271 .ok_or_else(|| anyhow!("line {}: unexpected '}}'", line_no))?;
272 match kind {
273 BlockKind::Guard => self.end_guard_block(line_no),
274 BlockKind::Io => self.end_io_block(line_no),
275 }
276 }
277
278 fn end_guard_block(&mut self, line_no: usize) -> Result<()> {
279 if self.guard_stack.len() == 1 {
280 bail!("line {}: unexpected '}}'", line_no);
281 }
282 if self.pending_guards.is_some() {
283 bail!(
284 "line {}: guard declared immediately before '}}' without a command",
285 line_no
286 );
287 }
288 let frame = self
289 .scope_stack
290 .last()
291 .cloned()
292 .ok_or_else(|| anyhow!("line {}: scope stack underflow", line_no))?;
293 if !frame.had_command {
294 bail!(
295 "line {}: guard block starting on line {} must contain at least one command",
296 line_no,
297 frame.line_no
298 );
299 }
300 let step = self
301 .steps
302 .last_mut()
303 .ok_or_else(|| anyhow!("line {}: guard block closed without any commands", line_no))?;
304 step.scope_exit += 1;
305 self.scope_stack.pop();
306 self.guard_stack.pop();
307 Ok(())
308 }
309
310 fn end_io_block(&mut self, line_no: usize) -> Result<()> {
311 let frame = self
312 .io_scope_stack
313 .pop()
314 .ok_or_else(|| anyhow!("line {}: unexpected '}}'", line_no))?;
315 if !frame.had_command {
316 bail!(
317 "line {}: WITH_IO block starting on line {} must contain at least one command",
318 line_no,
319 frame.line_no
320 );
321 }
322 Ok(())
323 }
324
325 fn guard_context(&mut self, inline: Option<GuardExpr>) -> Option<GuardExpr> {
326 let mut context = self.guard_stack.last().cloned().unwrap_or(None);
327 if let Some(pending) = self.pending_guards.take() {
328 context = and_guard_exprs(context, Some(pending));
329 self.pending_can_open_block = false;
330 }
331 if let Some(inline_guard) = inline {
332 context = and_guard_exprs(context, Some(inline_guard));
333 }
334 context
335 }
336
337 fn handle_command(
338 &mut self,
339 line_no: usize,
340 kind: StepKind,
341 inline_guards: Option<GuardExpr>,
342 ) -> Result<()> {
343 if let StepKind::WithIoBlock { bindings } = kind {
344 let guards = self.guard_context(inline_guards);
345 self.begin_io_block(line_no, bindings, guards)?;
346 return Ok(());
347 }
348
349 let guards = self.guard_context(inline_guards);
350 let guards = self.apply_io_guards(guards);
351 let scope_enter = self.pending_scope_enters;
352 self.pending_scope_enters = 0;
353 for frame in self.scope_stack.iter_mut() {
354 frame.had_command = true;
355 }
356 for frame in self.io_scope_stack.iter_mut() {
357 frame.had_command = true;
358 }
359 let kind = self.apply_io_defaults(kind);
360 self.steps.push(Step {
361 guard: guards,
362 kind,
363 scope_enter,
364 scope_exit: 0,
365 });
366 Ok(())
367 }
368
369 fn apply_io_defaults(&self, kind: StepKind) -> StepKind {
370 let defaults = self.current_io_defaults();
371 if defaults.is_empty() {
372 return kind;
373 }
374 match kind {
375 StepKind::WithIo { bindings, cmd } => StepKind::WithIo {
376 bindings: merge_bindings(&defaults, &bindings),
377 cmd,
378 },
379 other => StepKind::WithIo {
380 bindings: defaults,
381 cmd: Box::new(other),
382 },
383 }
384 }
385
386 fn current_io_defaults(&self) -> Vec<IoBinding> {
387 if self.io_scope_stack.is_empty() {
388 return Vec::new();
389 }
390 let mut set = IoBindingSet::default();
391 for frame in &self.io_scope_stack {
392 for binding in &frame.bindings {
393 set.insert(binding.clone());
394 }
395 }
396 set.into_vec()
397 }
398
399 fn apply_io_guards(&self, guard: Option<GuardExpr>) -> Option<GuardExpr> {
400 self.io_scope_stack.iter().fold(guard, |acc, frame| {
401 and_guard_exprs(acc, frame.guards.clone())
402 })
403 }
404}
405
406pub fn parse_script(input: &str) -> Result<Vec<Step>> {
407 ScriptParser::new(input)?.parse()
408}
409
410fn and_guard_exprs(left: Option<GuardExpr>, right: Option<GuardExpr>) -> Option<GuardExpr> {
411 match (left, right) {
412 (None, None) => None,
413 (Some(expr), None) | (None, Some(expr)) => Some(expr),
414 (Some(lhs), Some(rhs)) => Some(GuardExpr::all(vec![lhs, rhs])),
415 }
416}
417
418fn merge_bindings(defaults: &[IoBinding], overrides: &[IoBinding]) -> Vec<IoBinding> {
419 let mut set = IoBindingSet::default();
420 for binding in defaults {
421 set.insert(binding.clone());
422 }
423 for binding in overrides {
424 set.insert(binding.clone());
425 }
426 set.into_vec()
427}
428
429fn contains_inherit_env(kind: &StepKind) -> bool {
430 match kind {
431 StepKind::InheritEnv { .. } => true,
432 StepKind::WithIo { cmd, .. } => contains_inherit_env(cmd),
433 _ => false,
434 }
435}
436
437fn parse_command(pair: Pair<Rule>) -> Result<StepKind> {
438 let kind = match pair.as_rule() {
439 Rule::workdir_command => {
440 let arg = parse_single_arg(pair)?;
441 StepKind::Workdir(arg.into())
442 }
443 Rule::workspace_command => {
444 let target = parse_workspace_target(pair)?;
445 StepKind::Workspace(target)
446 }
447 Rule::env_command => {
448 let (key, value) = parse_env_pair(pair)?;
449 StepKind::Env {
450 key,
451 value: value.into(),
452 }
453 }
454 Rule::echo_command => {
455 let msg = parse_message(pair)?;
456 StepKind::Echo(msg.into())
457 }
458 Rule::run_command => {
459 let cmd = parse_run_args(pair)?;
460 StepKind::Run(cmd.into())
461 }
462 Rule::run_bg_command => {
463 let cmd = parse_run_args(pair)?;
464 StepKind::RunBg(cmd.into())
465 }
466 Rule::copy_command => {
467 let mut args: Vec<String> = Vec::new();
468 let mut from_current_workspace = false;
469 for inner in pair.into_inner() {
470 match inner.as_rule() {
471 Rule::from_current_workspace_flag => from_current_workspace = true,
472 Rule::argument => args.push(parse_argument(inner)?),
473 _ => {}
474 }
475 }
476 if args.len() != 2 {
477 bail!("COPY expects 2 arguments (from, to)");
478 }
479 StepKind::Copy {
480 from_current_workspace,
481 from: args.remove(0).into(),
482 to: args.remove(0).into(),
483 }
484 }
485 Rule::with_io_command => {
486 let mut bindings = Vec::new();
487 let mut cmd = None;
488 for inner in pair.into_inner() {
489 match inner.as_rule() {
490 Rule::io_flags => {
491 for flag in inner.into_inner() {
492 if flag.as_rule() == Rule::io_binding {
493 bindings.push(parse_io_binding(flag)?);
494 }
495 }
496 }
497 _ => {
498 cmd = Some(Box::new(parse_command(inner)?));
499 }
500 }
501 }
502 if let Some(cmd) = cmd {
503 StepKind::WithIo { bindings, cmd }
504 } else {
505 StepKind::WithIoBlock { bindings }
506 }
507 }
508 Rule::copy_git_command => {
509 let mut args = Vec::new();
510 let mut include_dirty = false;
511 for inner in pair.into_inner() {
512 match inner.as_rule() {
513 Rule::include_dirty_flag => include_dirty = true,
514 Rule::argument => args.push(parse_argument(inner)?),
515 _ => {}
516 }
517 }
518 if args.len() != 3 {
519 bail!("COPY_GIT expects 3 arguments (rev, from, to)");
520 }
521 StepKind::CopyGit {
522 rev: args.remove(0).into(),
523 from: args.remove(0).into(),
524 to: args.remove(0).into(),
525 include_dirty,
526 }
527 }
528 Rule::hash_sha256_command => {
529 let arg = parse_single_arg(pair)?;
530 StepKind::HashSha256 { path: arg.into() }
531 }
532 Rule::inherit_env_command => {
533 let mut keys: Vec<String> = Vec::new();
534 for inner in pair.into_inner() {
535 match inner.as_rule() {
536 Rule::inherit_list => {
537 for key in inner.into_inner() {
538 if key.as_rule() == Rule::env_key {
539 keys.push(key.as_str().trim().to_string());
540 }
541 }
542 }
543 Rule::env_key => keys.push(inner.as_str().trim().to_string()),
544 _ => {}
545 }
546 }
547 StepKind::InheritEnv { keys }
548 }
549 Rule::symlink_command => {
550 let mut args = parse_args(pair)?;
551 StepKind::Symlink {
552 from: args.remove(0).into(),
553 to: args.remove(0).into(),
554 }
555 }
556 Rule::mkdir_command => {
557 let arg = parse_single_arg(pair)?;
558 StepKind::Mkdir(arg.into())
559 }
560 Rule::ls_command => {
561 let args = parse_args(pair)?;
562 StepKind::Ls(args.into_iter().next().map(Into::into))
563 }
564 Rule::cwd_command => StepKind::Cwd,
565 Rule::read_command => {
566 let args = parse_args(pair)?;
567 StepKind::Read(args.into_iter().next().map(Into::into))
568 }
569 Rule::write_command => {
570 let mut path = None;
571 let mut contents = None;
572 for inner in pair.into_inner() {
573 match inner.as_rule() {
574 Rule::argument if path.is_none() => {
575 path = Some(parse_argument(inner)?);
576 }
577 Rule::message => {
578 contents = Some(parse_concatenated_string(inner)?);
579 }
580 _ => {}
581 }
582 }
583 StepKind::Write {
584 path: path
585 .ok_or_else(|| anyhow!("WRITE expects a path argument"))?
586 .into(),
587 contents: contents.map(Into::into),
588 }
589 }
590 Rule::append_command => {
591 let mut path = None;
592 let mut contents = None;
593 for inner in pair.into_inner() {
594 match inner.as_rule() {
595 Rule::argument if path.is_none() => {
596 path = Some(parse_argument(inner)?);
597 }
598 Rule::message => {
599 contents = Some(parse_concatenated_string(inner)?);
600 }
601 _ => {}
602 }
603 }
604 StepKind::Append {
605 path: path
606 .ok_or_else(|| anyhow!("APPEND expects a path argument"))?
607 .into(),
608 contents: contents.map(Into::into),
609 }
610 }
611 Rule::assert_file_hash_command => parse_assert_file_hash(pair)?,
612 Rule::assert_file_content_command => parse_assert_file_content(pair)?,
613 Rule::assert_dir_command => StepKind::AssertDir(parse_single_arg(pair)?.into()),
614 Rule::assert_absent_command => StepKind::AssertAbsent(parse_single_arg(pair)?.into()),
615 Rule::assert_stdout_command => StepKind::AssertStdout(parse_message(pair)?.into()),
616 Rule::exit_command => {
617 let code = parse_exit_code(pair)?;
618 StepKind::Exit(code)
619 }
620 _ => bail!("unknown command rule: {:?}", pair.as_rule()),
621 };
622 Ok(kind)
623}
624
625fn parse_assert_file_hash(pair: Pair<Rule>) -> Result<StepKind> {
626 let mut digest = None;
627 let mut path = None;
628 for part in pair.into_inner() {
629 match part.as_rule() {
630 Rule::hash_digest => digest = Some(part.as_str().to_string()),
631 Rule::argument => path = Some(parse_argument(part)?),
632 _ => {}
633 }
634 }
635 Ok(StepKind::AssertFile {
636 hash: Some(digest.ok_or_else(|| anyhow!("missing hash digest"))?),
637 path: path
638 .ok_or_else(|| anyhow!("ASSERT_FILE --hash expects a path argument"))?
639 .into(),
640 contents: None,
641 })
642}
643
644fn parse_assert_file_content(pair: Pair<Rule>) -> Result<StepKind> {
645 let mut path = None;
646 let mut contents = None;
647 for part in pair.into_inner() {
648 match part.as_rule() {
649 Rule::argument if path.is_none() => {
650 path = Some(parse_argument(part)?);
651 }
652 Rule::message => {
653 contents = Some(parse_concatenated_string(part)?);
654 }
655 _ => {}
656 }
657 }
658 Ok(StepKind::AssertFile {
659 hash: None,
660 path: path
661 .ok_or_else(|| anyhow!("ASSERT_FILE expects a path argument"))?
662 .into(),
663 contents: contents.map(Into::into),
664 })
665}
666
667fn parse_single_arg(pair: Pair<Rule>) -> Result<String> {
668 for inner in pair.into_inner() {
669 if inner.as_rule() == Rule::argument {
670 return parse_argument(inner);
671 }
672 }
673 bail!("missing argument")
674}
675
676fn parse_args(pair: Pair<Rule>) -> Result<Vec<String>> {
677 let mut args = Vec::new();
678 for inner in pair.into_inner() {
679 if inner.as_rule() == Rule::argument {
680 args.push(parse_argument(inner)?);
681 }
682 }
683 Ok(args)
684}
685
686fn parse_argument(pair: Pair<Rule>) -> Result<String> {
687 let inner = pair.into_inner().next().unwrap();
688 match inner.as_rule() {
689 Rule::quoted_string => parse_quoted_string(inner),
690 Rule::templated_arg => Ok(inner.as_str().to_string()),
691 Rule::unquoted_arg => Ok(inner.as_str().to_string()),
692 _ => unreachable!(),
693 }
694}
695
696fn parse_quoted_string(pair: Pair<Rule>) -> Result<String> {
697 let s = pair.as_str();
698 let _quote = s.chars().next().unwrap();
699 let content = &s[1..s.len() - 1];
700
701 let mut out = String::with_capacity(content.len());
702 let mut escape = false;
703 for ch in content.chars() {
704 if escape {
705 out.push(ch);
706 escape = false;
707 } else if ch == '\\' {
708 escape = true;
709 } else {
710 out.push(ch);
711 }
712 }
713 Ok(out)
714}
715
716fn parse_workspace_target(pair: Pair<Rule>) -> Result<WorkspaceTarget> {
717 for inner in pair.into_inner() {
718 if inner.as_rule() == Rule::workspace_target {
719 return match inner.as_str().to_ascii_lowercase().as_str() {
720 "snapshot" => Ok(WorkspaceTarget::Snapshot),
721 "local" => Ok(WorkspaceTarget::Local),
722 _ => bail!("unknown workspace target"),
723 };
724 }
725 }
726 bail!("missing workspace target")
727}
728
729fn parse_env_pair(pair: Pair<Rule>) -> Result<(String, String)> {
730 for inner in pair.into_inner() {
731 if inner.as_rule() == Rule::env_pair {
732 let mut parts = inner.into_inner();
733 let key = parts.next().unwrap().as_str().to_string();
734 let value_pair = parts.next().unwrap();
735 let value = match value_pair.as_rule() {
736 Rule::env_value_part => {
737 let inner_val = value_pair.into_inner().next().unwrap();
738 match inner_val.as_rule() {
739 Rule::quoted_string => parse_quoted_string(inner_val)?,
740 Rule::unquoted_env_value => inner_val.as_str().to_string(),
741 _ => unreachable!(
742 "unexpected rule in env_value_part: {:?}",
743 inner_val.as_rule()
744 ),
745 }
746 }
747 _ => unreachable!("expected env_value_part"),
748 };
749 return Ok((key, value));
750 }
751 }
752 bail!("missing env pair")
753}
754
755fn parse_message(pair: Pair<Rule>) -> Result<String> {
756 for inner in pair.into_inner() {
757 if inner.as_rule() == Rule::message {
758 return parse_concatenated_string(inner);
759 }
760 }
761 bail!("missing message")
762}
763
764fn parse_run_args(pair: Pair<Rule>) -> Result<String> {
765 for inner in pair.into_inner() {
766 if inner.as_rule() == Rule::run_args {
767 return parse_smart_concatenated_string(inner);
768 }
769 }
770 bail!("missing run args")
771}
772
773fn parse_smart_concatenated_string(pair: Pair<Rule>) -> Result<String> {
774 let parts: Vec<_> = pair.into_inner().collect();
775
776 if parts.len() == 1 && parts[0].as_rule() == Rule::quoted_string {
780 return parse_quoted_string(parts[0].clone());
781 }
782
783 let mut body = String::new();
784 let mut last_end = None;
785 for part in parts {
786 let span = part.as_span();
787 if let Some(end) = last_end
788 && span.start() > end
789 {
790 body.push(' ');
791 }
792 match part.as_rule() {
793 Rule::quoted_string => {
794 let raw = part.as_str();
795 let unquoted = parse_quoted_string(part.clone())?;
796 let needs_quotes = unquoted.is_empty()
799 || unquoted
800 .chars()
801 .any(|c| c.is_whitespace() || c == ';' || c == '\n' || c == '\r')
802 || unquoted.contains("//")
803 || unquoted.contains("/*");
804
805 if needs_quotes {
806 body.push_str(raw);
807 } else {
808 body.push_str(&unquoted);
809 }
810 }
811 Rule::unquoted_msg_content | Rule::unquoted_run_content => body.push_str(part.as_str()),
812 _ => {}
813 }
814 last_end = Some(span.end());
815 }
816 Ok(body)
817}
818
819fn parse_concatenated_string(pair: Pair<Rule>) -> Result<String> {
820 let mut body = String::new();
821 let mut last_end = None;
822 for part in pair.into_inner() {
823 let span = part.as_span();
824 if let Some(end) = last_end
825 && span.start() > end
826 {
827 body.push(' ');
828 }
829 match part.as_rule() {
830 Rule::quoted_string => body.push_str(&parse_quoted_string(part)?),
831 Rule::unquoted_msg_content | Rule::unquoted_run_content => body.push_str(part.as_str()),
832 _ => {}
833 }
834 last_end = Some(span.end());
835 }
836 Ok(body)
837}
838
839fn parse_exit_code(pair: Pair<Rule>) -> Result<i32> {
840 for inner in pair.into_inner() {
841 if inner.as_rule() == Rule::exit_code {
842 return inner
843 .as_str()
844 .parse()
845 .map_err(|_| anyhow!("invalid exit code"));
846 }
847 }
848 bail!("missing exit code")
849}
850
851fn parse_guard_line(pair: Pair<Rule>) -> Result<GuardExpr> {
852 for inner in pair.into_inner() {
853 if inner.as_rule() == Rule::guard_expr {
854 return parse_guard_expr(inner);
855 }
856 }
857 bail!("guard line missing expression")
858}
859
860fn parse_io_binding(pair: Pair<Rule>) -> Result<IoBinding> {
861 let mut stream = None;
862 let mut pipe = None;
863 for inner in pair.into_inner() {
864 match inner.as_rule() {
865 Rule::io_stream => stream = Some(parse_io_stream(inner.as_str())),
866 Rule::pipe_binding => pipe = Some(parse_pipe_binding(inner)?),
867 _ => {}
868 }
869 }
870 let stream = stream.ok_or_else(|| anyhow!("missing IO stream in WITH_IO"))?;
871 Ok(IoBinding { stream, pipe })
872}
873
874fn parse_io_stream(text: &str) -> IoStream {
875 match text {
876 "stdin" => IoStream::Stdin,
877 "stdout" => IoStream::Stdout,
878 "stderr" => IoStream::Stderr,
879 _ => unreachable!("parser produced invalid io_stream token"),
880 }
881}
882
883fn parse_pipe_binding(pair: Pair<Rule>) -> Result<String> {
884 for inner in pair.into_inner() {
885 if inner.as_rule() == Rule::pipe_name {
886 return Ok(inner.as_str().to_string());
887 }
888 }
889 bail!("missing pipe identifier in WITH_IO binding");
890}
891
892fn parse_guard_expr(pair: Pair<Rule>) -> Result<GuardExpr> {
893 match pair.as_rule() {
894 Rule::guard_expr => {
895 let next = pair
896 .into_inner()
897 .next()
898 .ok_or_else(|| anyhow!("guard expression missing body"))?;
899 parse_guard_expr(next)
900 }
901 Rule::guard_seq => parse_guard_seq(pair),
902 Rule::guard_factor => parse_guard_factor(pair),
903 Rule::guard_not => parse_guard_not(pair),
904 Rule::guard_primary => parse_guard_primary(pair),
905 Rule::guard_group => parse_guard_group(pair),
906 Rule::guard_or_call => parse_guard_or_call(pair),
907 Rule::guard_term => Ok(GuardExpr::Predicate(parse_guard_term(pair)?)),
908 _ => bail!("unexpected guard expression rule: {:?}", pair.as_rule()),
909 }
910}
911
912fn parse_guard_seq(pair: Pair<Rule>) -> Result<GuardExpr> {
913 let mut exprs = Vec::new();
914 for inner in pair.into_inner() {
915 if inner.as_rule() == Rule::guard_factor {
916 exprs.push(parse_guard_factor(inner)?);
917 }
918 }
919 match exprs.len() {
920 0 => bail!("guard list requires at least one entry"),
921 1 => Ok(exprs.pop().unwrap()),
922 _ => Ok(GuardExpr::all(exprs)),
923 }
924}
925
926fn parse_guard_factor(pair: Pair<Rule>) -> Result<GuardExpr> {
927 for inner in pair.into_inner() {
928 if inner.as_rule() == Rule::guard_not {
929 return parse_guard_not(inner);
930 }
931 }
932 bail!("guard factor missing expression")
933}
934
935fn parse_guard_not(pair: Pair<Rule>) -> Result<GuardExpr> {
936 let mut invert_count = 0usize;
937 let mut primary = None;
938 for inner in pair.into_inner() {
939 match inner.as_rule() {
940 Rule::invert => invert_count += 1,
941 _ => primary = Some(parse_guard_primary(inner)?),
942 }
943 }
944 let expr = primary.ok_or_else(|| anyhow!("guard expression missing predicate"))?;
945 apply_inversion(expr, invert_count % 2 == 1)
946}
947
948fn parse_guard_primary(pair: Pair<Rule>) -> Result<GuardExpr> {
949 match pair.as_rule() {
950 Rule::guard_primary => {
951 let inner = pair
952 .into_inner()
953 .next()
954 .ok_or_else(|| anyhow!("guard primary missing body"))?;
955 parse_guard_primary(inner)
956 }
957 Rule::guard_group => parse_guard_group(pair),
958 Rule::guard_or_call => parse_guard_or_call(pair),
959 Rule::guard_term => Ok(GuardExpr::Predicate(parse_guard_term(pair)?)),
960 _ => bail!("unexpected guard primary rule: {:?}", pair.as_rule()),
961 }
962}
963
964fn parse_guard_group(pair: Pair<Rule>) -> Result<GuardExpr> {
965 for inner in pair.into_inner() {
966 if inner.as_rule() == Rule::guard_expr {
967 return parse_guard_expr(inner);
968 }
969 }
970 bail!("grouped guard missing expression")
971}
972
973fn parse_guard_or_call(pair: Pair<Rule>) -> Result<GuardExpr> {
974 let mut args = Vec::new();
975 for inner in pair.into_inner() {
976 if inner.as_rule() == Rule::guard_expr_list {
977 args = parse_guard_expr_list(inner)?;
978 }
979 }
980 if args.len() < 2 {
981 bail!("or(...) requires at least two guard expressions");
982 }
983 Ok(GuardExpr::or(args))
984}
985
986fn parse_guard_expr_list(pair: Pair<Rule>) -> Result<Vec<GuardExpr>> {
987 let mut exprs = Vec::new();
988 for inner in pair.into_inner() {
989 if inner.as_rule() == Rule::guard_expr {
990 push_guard_or_args_from_expr(inner, &mut exprs)?;
991 }
992 }
993 Ok(exprs)
994}
995
996fn push_guard_or_args_from_expr(expr_pair: Pair<Rule>, exprs: &mut Vec<GuardExpr>) -> Result<()> {
997 if let Some(seq_pair) = expr_pair
998 .clone()
999 .into_inner()
1000 .find(|inner| inner.as_rule() == Rule::guard_seq)
1001 {
1002 let factors: Vec<Pair<Rule>> = seq_pair
1003 .into_inner()
1004 .filter(|inner| inner.as_rule() == Rule::guard_factor)
1005 .collect();
1006 if factors.len() > 1 {
1007 for factor in factors {
1008 exprs.push(parse_guard_factor(factor)?);
1009 }
1010 return Ok(());
1011 }
1012 }
1013 exprs.push(parse_guard_expr(expr_pair)?);
1014 Ok(())
1015}
1016
1017fn apply_inversion(expr: GuardExpr, invert: bool) -> Result<GuardExpr> {
1018 if !invert {
1019 return Ok(expr);
1020 }
1021 match expr {
1022 GuardExpr::Predicate(guard) => {
1023 if let Guard::EnvEquals {
1024 key,
1025 value,
1026 invert: false,
1027 } = &guard
1028 {
1029 bail!(
1030 "inverted env equality is not allowed: use 'env:{}!={}' or '!env:{}'",
1031 key,
1032 value,
1033 key
1034 );
1035 }
1036 Ok(GuardExpr::Predicate(invert_guard(guard)))
1037 }
1038 other => Ok(!other),
1039 }
1040}
1041
1042fn invert_guard(guard: Guard) -> Guard {
1043 match guard {
1044 Guard::Platform { target, invert } => Guard::Platform {
1045 target,
1046 invert: !invert,
1047 },
1048 Guard::EnvExists { key, invert } => Guard::EnvExists {
1049 key,
1050 invert: !invert,
1051 },
1052 Guard::EnvEquals { key, value, invert } => Guard::EnvEquals {
1053 key,
1054 value,
1055 invert: !invert,
1056 },
1057 }
1058}
1059
1060fn parse_guard_term(pair: Pair<Rule>) -> Result<Guard> {
1061 for inner in pair.into_inner() {
1062 match inner.as_rule() {
1063 Rule::env_guard => return parse_env_guard(inner),
1064 Rule::bare_platform => return parse_bare_platform(inner, false),
1065 _ => {}
1066 }
1067 }
1068 bail!("missing guard predicate")
1069}
1070
1071fn parse_env_guard(pair: Pair<Rule>) -> Result<Guard> {
1072 let mut key = String::new();
1073 let mut value = None;
1074 let mut is_not_equals = false;
1075
1076 for inner in pair.into_inner() {
1077 match inner.as_rule() {
1078 Rule::env_key => key = inner.as_str().trim().to_string(),
1079 Rule::env_comparison => {
1080 for comp_part in inner.into_inner() {
1081 match comp_part.as_rule() {
1082 Rule::equals_env | Rule::not_equals_env => {
1083 for part in comp_part.into_inner() {
1084 match part.as_rule() {
1085 Rule::eq_op => {}
1086 Rule::neq_op => is_not_equals = true,
1087 Rule::env_value => {
1088 value = Some(part.as_str().trim().to_string());
1089 }
1090 _ => {}
1091 }
1092 }
1093 }
1094 Rule::eq_op => {}
1095 Rule::neq_op => is_not_equals = true,
1096 Rule::env_value => {
1097 value = Some(comp_part.as_str().trim().to_string());
1098 }
1099 _ => {}
1100 }
1101 }
1102 }
1103 _ => {}
1104 }
1105 }
1106
1107 if let Some(val) = value {
1108 Ok(Guard::EnvEquals {
1109 key,
1110 value: val,
1111 invert: is_not_equals,
1112 })
1113 } else {
1114 Ok(Guard::EnvExists { key, invert: false })
1115 }
1116}
1117
1118fn parse_bare_platform(pair: Pair<Rule>, invert: bool) -> Result<Guard> {
1119 let tag = pair.into_inner().next().unwrap().as_str();
1120 parse_platform_tag(tag, invert)
1121}
1122
1123fn parse_platform_tag(tag: &str, invert: bool) -> Result<Guard> {
1124 let target = match tag.to_ascii_lowercase().as_str() {
1125 "unix" => PlatformGuard::Unix,
1126 "windows" => PlatformGuard::Windows,
1127 "mac" | "macos" => PlatformGuard::Macos,
1128 "linux" => PlatformGuard::Linux,
1129 _ => bail!("unknown platform '{}'", tag),
1130 };
1131 Ok(Guard::Platform { target, invert })
1132}