1use crate::ast::{Guard, PlatformGuard, Step, StepKind, WorkspaceTarget};
2use crate::lexer::{self, RawToken, Rule};
3use anyhow::{Result, anyhow, bail};
4use pest::iterators::Pair;
5use std::collections::VecDeque;
6
7#[derive(Clone)]
8struct ScopeFrame {
9 line_no: usize,
10 had_command: bool,
11}
12
13pub struct ScriptParser<'a> {
14 tokens: VecDeque<RawToken<'a>>,
15 steps: Vec<Step>,
16 guard_stack: Vec<Vec<Vec<Guard>>>,
17 pending_guards: Option<Vec<Vec<Guard>>>,
18 pending_inline_guards: Option<Vec<Vec<Guard>>>,
19 pending_can_open_block: bool,
20 pending_scope_enters: usize,
21 scope_stack: Vec<ScopeFrame>,
22}
23
24impl<'a> ScriptParser<'a> {
25 pub fn new(input: &'a str) -> Result<Self> {
26 let tokens = VecDeque::from(lexer::tokenize(input)?);
27 Ok(Self {
28 tokens,
29 steps: Vec::new(),
30 guard_stack: vec![Vec::new()],
31 pending_guards: None,
32 pending_inline_guards: None,
33 pending_can_open_block: false,
34 pending_scope_enters: 0,
35 scope_stack: Vec::new(),
36 })
37 }
38
39 pub fn parse(mut self) -> Result<Vec<Step>> {
40 while let Some(token) = self.tokens.pop_front() {
41 match token {
42 RawToken::Guard { pair, line_end } => {
43 let groups = parse_guard_line(pair)?;
44 self.handle_guard_token(line_end, groups)?
45 }
46 RawToken::BlockStart { line_no } => self.start_block_from_pending(line_no)?,
47 RawToken::BlockEnd { line_no } => self.end_block(line_no)?,
48 RawToken::Command { pair, line_no } => {
49 let kind = parse_command(pair)?;
50 self.handle_command_token(line_no, kind)?
51 }
52 }
53 }
54
55 if self.guard_stack.len() != 1 {
56 bail!("unclosed guard block at end of script");
57 }
58 if let Some(pending) = &self.pending_guards
59 && !pending.is_empty()
60 {
61 bail!("guard declared on final lines without a following command");
62 }
63
64 Ok(self.steps)
65 }
66
67 fn handle_guard_token(&mut self, line_end: usize, groups: Vec<Vec<Guard>>) -> Result<()> {
68 if let Some(RawToken::Command { line_no, .. }) = self.tokens.front()
69 && *line_no == line_end
70 {
71 self.pending_inline_guards = Some(groups);
72 self.pending_can_open_block = false;
73 return Ok(());
74 }
75 self.stash_pending_guard(groups);
76 self.pending_can_open_block = true;
77 Ok(())
78 }
79
80 fn handle_command_token(&mut self, line_no: usize, kind: StepKind) -> Result<()> {
81 let inline = self.pending_inline_guards.take();
82 self.handle_command(line_no, kind, inline)
83 }
84
85 fn stash_pending_guard(&mut self, groups: Vec<Vec<Guard>>) {
86 self.pending_guards = Some(if let Some(existing) = self.pending_guards.take() {
87 combine_guard_groups(&existing, &groups)
88 } else {
89 groups
90 });
91 }
92
93 fn start_block_from_pending(&mut self, line_no: usize) -> Result<()> {
94 let guards = self
95 .pending_guards
96 .take()
97 .ok_or_else(|| anyhow!("line {}: '{{' without a pending guard", line_no))?;
98 if !self.pending_can_open_block {
99 bail!("line {}: '{{' must directly follow a guard", line_no);
100 }
101 self.pending_can_open_block = false;
102 self.start_block(guards, line_no)
103 }
104
105 fn start_block(&mut self, guards: Vec<Vec<Guard>>, line_no: usize) -> Result<()> {
106 let with_pending = if let Some(pending) = self.pending_guards.take() {
107 combine_guard_groups(&pending, &guards)
108 } else {
109 guards
110 };
111 let parent = self.guard_stack.last().cloned().unwrap_or_default();
112 let next = if parent.is_empty() {
113 with_pending
114 } else if with_pending.is_empty() {
115 parent
116 } else {
117 combine_guard_groups(&parent, &with_pending)
118 };
119 self.guard_stack.push(next);
120 self.scope_stack.push(ScopeFrame {
121 line_no,
122 had_command: false,
123 });
124 self.pending_scope_enters += 1;
125 Ok(())
126 }
127
128 fn end_block(&mut self, line_no: usize) -> Result<()> {
129 if self.guard_stack.len() == 1 {
130 bail!("line {}: unexpected '}}'", line_no);
131 }
132 if self.pending_guards.is_some() {
133 bail!(
134 "line {}: guard declared immediately before '}}' without a command",
135 line_no
136 );
137 }
138 let frame = self
139 .scope_stack
140 .last()
141 .cloned()
142 .ok_or_else(|| anyhow!("line {}: scope stack underflow", line_no))?;
143 if !frame.had_command {
144 bail!(
145 "line {}: guard block starting on line {} must contain at least one command",
146 line_no,
147 frame.line_no
148 );
149 }
150 let step = self
151 .steps
152 .last_mut()
153 .ok_or_else(|| anyhow!("line {}: guard block closed without any commands", line_no))?;
154 step.scope_exit += 1;
155 self.scope_stack.pop();
156 self.guard_stack.pop();
157 Ok(())
158 }
159
160 fn guard_context(&mut self, inline: Option<Vec<Vec<Guard>>>) -> Vec<Vec<Guard>> {
161 let mut context = if let Some(top) = self.guard_stack.last() {
162 top.clone()
163 } else {
164 Vec::new()
165 };
166 if let Some(pending) = self.pending_guards.take() {
167 context = if context.is_empty() {
168 pending
169 } else {
170 combine_guard_groups(&context, &pending)
171 };
172 self.pending_can_open_block = false;
173 }
174 if let Some(inline_groups) = inline {
175 context = if context.is_empty() {
176 inline_groups
177 } else {
178 combine_guard_groups(&context, &inline_groups)
179 };
180 }
181 context
182 }
183
184 fn handle_command(
185 &mut self,
186 _line_no: usize,
187 kind: StepKind,
188 inline_guards: Option<Vec<Vec<Guard>>>,
189 ) -> Result<()> {
190 let guards = self.guard_context(inline_guards);
191 let scope_enter = self.pending_scope_enters;
192 self.pending_scope_enters = 0;
193 for frame in self.scope_stack.iter_mut() {
194 frame.had_command = true;
195 }
196 self.steps.push(Step {
197 guards,
198 kind,
199 scope_enter,
200 scope_exit: 0,
201 });
202 Ok(())
203 }
204}
205
206pub fn parse_script(input: &str) -> Result<Vec<Step>> {
207 ScriptParser::new(input)?.parse()
208}
209
210fn combine_guard_groups(a: &[Vec<Guard>], b: &[Vec<Guard>]) -> Vec<Vec<Guard>> {
211 if a.is_empty() {
212 return b.to_vec();
213 }
214 if b.is_empty() {
215 return a.to_vec();
216 }
217 let mut combined = Vec::new();
218 for left in a {
219 for right in b {
220 let mut merged = left.clone();
221 merged.extend(right.clone());
222 combined.push(merged);
223 }
224 }
225 combined
226}
227
228fn parse_command(pair: Pair<Rule>) -> Result<StepKind> {
229 let kind = match pair.as_rule() {
230 Rule::workdir_command => {
231 let arg = parse_single_arg(pair)?;
232 StepKind::Workdir(arg)
233 }
234 Rule::workspace_command => {
235 let target = parse_workspace_target(pair)?;
236 StepKind::Workspace(target)
237 }
238 Rule::env_command => {
239 let (key, value) = parse_env_pair(pair)?;
240 StepKind::Env { key, value }
241 }
242 Rule::echo_command => {
243 let msg = parse_message(pair)?;
244 StepKind::Echo(msg)
245 }
246 Rule::run_command => {
247 let cmd = parse_run_args(pair)?;
248 StepKind::Run(cmd)
249 }
250 Rule::run_bg_command => {
251 let cmd = parse_run_args(pair)?;
252 StepKind::RunBg(cmd)
253 }
254 Rule::copy_command => {
255 let mut args = parse_args(pair)?;
256 StepKind::Copy {
257 from: args.remove(0),
258 to: args.remove(0),
259 }
260 }
261 Rule::capture_command => {
262 let path = parse_single_arg_from_pair(pair.clone())?;
263 let cmd = parse_run_args_from_pair(pair)?;
264 StepKind::Capture { path, cmd }
265 }
266 Rule::copy_git_command => {
267 let mut args = parse_args(pair)?;
268 StepKind::CopyGit {
269 rev: args.remove(0),
270 from: args.remove(0),
271 to: args.remove(0),
272 }
273 }
274 Rule::symlink_command => {
275 let mut args = parse_args(pair)?;
276 StepKind::Symlink {
277 from: args.remove(0),
278 to: args.remove(0),
279 }
280 }
281 Rule::mkdir_command => {
282 let arg = parse_single_arg(pair)?;
283 StepKind::Mkdir(arg)
284 }
285 Rule::ls_command => {
286 let args = parse_args(pair)?;
287 StepKind::Ls(args.into_iter().next())
288 }
289 Rule::cwd_command => StepKind::Cwd,
290 Rule::cat_command => {
291 let arg = parse_single_arg(pair)?;
292 StepKind::Cat(arg)
293 }
294 Rule::write_command => {
295 let path = parse_single_arg_from_pair(pair.clone())?;
296 let contents = parse_message(pair)?;
297 StepKind::Write { path, contents }
298 }
299 Rule::exit_command => {
300 let code = parse_exit_code(pair)?;
301 StepKind::Exit(code)
302 }
303 _ => bail!("unknown command rule: {:?}", pair.as_rule()),
304 };
305 Ok(kind)
306}
307
308fn parse_single_arg(pair: Pair<Rule>) -> Result<String> {
309 for inner in pair.into_inner() {
310 if inner.as_rule() == Rule::argument {
311 return parse_argument(inner);
312 }
313 }
314 bail!("missing argument")
315}
316
317fn parse_single_arg_from_pair(pair: Pair<Rule>) -> Result<String> {
318 for inner in pair.into_inner() {
319 if inner.as_rule() == Rule::argument {
320 return parse_argument(inner);
321 }
322 }
323 bail!("missing argument")
324}
325
326fn parse_args(pair: Pair<Rule>) -> Result<Vec<String>> {
327 let mut args = Vec::new();
328 for inner in pair.into_inner() {
329 if inner.as_rule() == Rule::argument {
330 args.push(parse_argument(inner)?);
331 }
332 }
333 Ok(args)
334}
335
336fn parse_argument(pair: Pair<Rule>) -> Result<String> {
337 let inner = pair.into_inner().next().unwrap();
338 match inner.as_rule() {
339 Rule::quoted_string => parse_quoted_string(inner),
340 Rule::unquoted_arg => Ok(inner.as_str().to_string()),
341 _ => unreachable!(),
342 }
343}
344
345fn parse_quoted_string(pair: Pair<Rule>) -> Result<String> {
346 let s = pair.as_str();
347 let _quote = s.chars().next().unwrap();
348 let content = &s[1..s.len() - 1];
349
350 let mut out = String::with_capacity(content.len());
351 let mut escape = false;
352 for ch in content.chars() {
353 if escape {
354 out.push(ch);
355 escape = false;
356 } else if ch == '\\' {
357 escape = true;
358 } else {
359 out.push(ch);
360 }
361 }
362 Ok(out)
363}
364
365fn parse_workspace_target(pair: Pair<Rule>) -> Result<WorkspaceTarget> {
366 for inner in pair.into_inner() {
367 if inner.as_rule() == Rule::workspace_target {
368 return match inner.as_str().to_ascii_lowercase().as_str() {
369 "snapshot" => Ok(WorkspaceTarget::Snapshot),
370 "local" => Ok(WorkspaceTarget::Local),
371 _ => bail!("unknown workspace target"),
372 };
373 }
374 }
375 bail!("missing workspace target")
376}
377
378fn parse_env_pair(pair: Pair<Rule>) -> Result<(String, String)> {
379 for inner in pair.into_inner() {
380 if inner.as_rule() == Rule::env_pair {
381 let mut parts = inner.into_inner();
382 let key = parts.next().unwrap().as_str().to_string();
383 let value_pair = parts.next().unwrap();
384 let value = match value_pair.as_rule() {
385 Rule::env_value_part => {
386 let inner_val = value_pair.into_inner().next().unwrap();
387 match inner_val.as_rule() {
388 Rule::quoted_string => parse_quoted_string(inner_val)?,
389 Rule::unquoted_env_value => inner_val.as_str().to_string(),
390 _ => unreachable!(
391 "unexpected rule in env_value_part: {:?}",
392 inner_val.as_rule()
393 ),
394 }
395 }
396 _ => unreachable!("expected env_value_part"),
397 };
398 return Ok((key, value));
399 }
400 }
401 bail!("missing env pair")
402}
403
404fn parse_message(pair: Pair<Rule>) -> Result<String> {
405 for inner in pair.into_inner() {
406 if inner.as_rule() == Rule::message {
407 return parse_concatenated_string(inner);
408 }
409 }
410 bail!("missing message")
411}
412
413fn parse_run_args(pair: Pair<Rule>) -> Result<String> {
414 for inner in pair.into_inner() {
415 if inner.as_rule() == Rule::run_args {
416 return parse_raw_concatenated_string(inner);
417 }
418 }
419 bail!("missing run args")
420}
421
422fn parse_run_args_from_pair(pair: Pair<Rule>) -> Result<String> {
423 for inner in pair.into_inner() {
424 if inner.as_rule() == Rule::run_args {
425 return parse_raw_concatenated_string(inner);
426 }
427 }
428 bail!("missing run args")
429}
430
431fn parse_concatenated_string(pair: Pair<Rule>) -> Result<String> {
432 let mut body = String::new();
433 let mut last_end = None;
434 for part in pair.into_inner() {
435 let span = part.as_span();
436 if let Some(end) = last_end
437 && span.start() > end
438 {
439 body.push(' ');
440 }
441 match part.as_rule() {
442 Rule::quoted_string => body.push_str(&parse_quoted_string(part)?),
443 Rule::unquoted_msg_content | Rule::unquoted_run_content => body.push_str(part.as_str()),
444 _ => {}
445 }
446 last_end = Some(span.end());
447 }
448 Ok(body)
449}
450
451fn parse_raw_concatenated_string(pair: Pair<Rule>) -> Result<String> {
452 let parts: Vec<_> = pair.into_inner().collect();
453 if parts.len() == 1 && parts[0].as_rule() == Rule::quoted_string {
454 let raw = parts[0].as_str();
455 let unquoted = parse_quoted_string(parts[0].clone())?;
456 let needs_quotes = unquoted.is_empty()
457 || unquoted.chars().any(|c| c == ';' || c == '\n' || c == '\r')
458 || unquoted.contains("//")
459 || unquoted.contains("/*");
460 if needs_quotes {
461 return Ok(raw.to_string());
462 }
463 return Ok(unquoted);
464 }
465 let mut body = String::new();
466 let mut last_end = None;
467 for part in parts {
468 let span = part.as_span();
469 if let Some(end) = last_end
470 && span.start() > end
471 {
472 body.push(' ');
473 }
474 match part.as_rule() {
475 Rule::quoted_string => {
476 let raw = part.as_str();
477 let unquoted = parse_quoted_string(part.clone())?;
478 let needs_quotes = unquoted.is_empty()
481 || unquoted
482 .chars()
483 .any(|c| c.is_whitespace() || c == ';' || c == '\n' || c == '\r')
484 || unquoted.contains("//")
485 || unquoted.contains("/*");
486
487 if needs_quotes {
488 body.push_str(raw);
489 } else {
490 body.push_str(&unquoted);
491 }
492 }
493 Rule::unquoted_msg_content | Rule::unquoted_run_content => body.push_str(part.as_str()),
494 _ => {}
495 }
496 last_end = Some(span.end());
497 }
498 Ok(body)
499}
500
501fn parse_exit_code(pair: Pair<Rule>) -> Result<i32> {
502 for inner in pair.into_inner() {
503 if inner.as_rule() == Rule::exit_code {
504 return inner
505 .as_str()
506 .parse()
507 .map_err(|_| anyhow!("invalid exit code"));
508 }
509 }
510 bail!("missing exit code")
511}
512
513fn parse_guard_line(pair: Pair<Rule>) -> Result<Vec<Vec<Guard>>> {
514 let mut groups = Vec::new();
515 for inner in pair.into_inner() {
516 if inner.as_rule() == Rule::guard_groups {
517 groups = parse_guard_groups(inner)?;
518 }
519 }
520 Ok(groups)
521}
522
523fn parse_guard_groups(pair: Pair<Rule>) -> Result<Vec<Vec<Guard>>> {
524 let mut groups = Vec::new();
525 for inner in pair.into_inner() {
526 if inner.as_rule() == Rule::guard_conjunction {
527 groups.push(parse_guard_conjunction(inner)?);
528 }
529 }
530 Ok(groups)
531}
532
533fn parse_guard_conjunction(pair: Pair<Rule>) -> Result<Vec<Guard>> {
534 let mut group = Vec::new();
535 for inner in pair.into_inner() {
536 if inner.as_rule() == Rule::guard_term {
537 group.push(parse_guard_term(inner)?);
538 }
539 }
540 Ok(group)
541}
542
543fn parse_guard_term(pair: Pair<Rule>) -> Result<Guard> {
544 let mut invert = false;
545 let mut guard = None;
546
547 for inner in pair.into_inner() {
548 match inner.as_rule() {
549 Rule::invert => invert = true,
550 Rule::env_guard => guard = Some(parse_env_guard(inner, invert)?),
551 Rule::platform_guard => guard = Some(parse_platform_guard(inner, invert)?),
552 Rule::bare_platform => guard = Some(parse_bare_platform(inner, invert)?),
553 _ => {}
554 }
555 }
556 guard.ok_or_else(|| anyhow!("missing guard predicate"))
557}
558
559fn parse_env_guard(pair: Pair<Rule>, invert: bool) -> Result<Guard> {
560 let mut key = String::new();
561 let mut value = None;
562 let mut is_not_equals = false;
563
564 for inner in pair.into_inner() {
565 match inner.as_rule() {
566 Rule::env_key => key = inner.as_str().trim().to_string(),
567 Rule::env_comparison => {
568 let inner_comp = inner.into_inner().next().unwrap();
569 match inner_comp.as_rule() {
570 Rule::equals => {
571 value = Some(
572 inner_comp
573 .into_inner()
574 .next()
575 .unwrap()
576 .as_str()
577 .trim()
578 .to_string(),
579 );
580 }
581 Rule::not_equals => {
582 is_not_equals = true;
583 value = Some(
584 inner_comp
585 .into_inner()
586 .next()
587 .unwrap()
588 .as_str()
589 .trim()
590 .to_string(),
591 );
592 }
593 _ => {}
594 }
595 }
596 _ => {}
597 }
598 }
599
600 if let Some(val) = value {
601 Ok(Guard::EnvEquals {
602 key,
603 value: val,
604 invert: invert ^ is_not_equals,
605 })
606 } else {
607 Ok(Guard::EnvExists { key, invert })
608 }
609}
610
611fn parse_platform_guard(pair: Pair<Rule>, invert: bool) -> Result<Guard> {
612 let mut tag = "";
613 for inner in pair.into_inner() {
614 if inner.as_rule() == Rule::platform_tag {
615 tag = inner.as_str();
616 }
617 }
618 parse_platform_tag(tag, invert)
619}
620
621fn parse_bare_platform(pair: Pair<Rule>, invert: bool) -> Result<Guard> {
622 let tag = pair.into_inner().next().unwrap().as_str();
623 parse_platform_tag(tag, invert)
624}
625
626fn parse_platform_tag(tag: &str, invert: bool) -> Result<Guard> {
627 let target = match tag.to_ascii_lowercase().as_str() {
628 "unix" => PlatformGuard::Unix,
629 "windows" => PlatformGuard::Windows,
630 "mac" | "macos" => PlatformGuard::Macos,
631 "linux" => PlatformGuard::Linux,
632 _ => bail!("unknown platform '{}'", tag),
633 };
634 Ok(Guard::Platform { target, invert })
635}