oxdock_parser/
parser.rs

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                // Preserve quotes if the content needs them to be parsed correctly
479                // or to preserve argument grouping (spaces).
480                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}