Skip to main content

xtask_todo_lib/devshell/script/
parse.rs

1//! Script parser: logical lines → AST.
2
3use super::ast::{ParseError, ScriptStmt};
4
5const fn is_identifier_char(c: u8) -> bool {
6    c.is_ascii_alphanumeric() || c == b'_'
7}
8
9fn is_identifier(s: &str) -> bool {
10    let b = s.as_bytes();
11    if b.is_empty() {
12        return false;
13    }
14    if b[0] != b'_' && !b[0].is_ascii_alphabetic() {
15        return false;
16    }
17    b.iter().all(|&c| is_identifier_char(c))
18}
19
20/// Returns (name, value) if line is NAME=value (no space before =); else None.
21fn parse_assign(line: &str) -> Option<(String, String)> {
22    let line = line.trim();
23    let eq_pos = line.find('=')?;
24    if eq_pos == 0 {
25        return None;
26    }
27    let (left, right) = line.split_at(eq_pos);
28    let name = left.trim_end();
29    if name != left {
30        return None;
31    }
32    let value = right[1..].trim_start().to_string();
33    if name.is_empty() || !is_identifier(name) {
34        return None;
35    }
36    Some((name.to_string(), value))
37}
38
39/// Parse logical lines into a list of script statements.
40///
41/// # Errors
42/// Returns `ParseError` on unclosed if/for/while or invalid syntax.
43pub fn parse_script(lines: &[String]) -> Result<Vec<ScriptStmt>, ParseError> {
44    let mut stmts = Vec::new();
45    let mut i = 0;
46    while i < lines.len() {
47        let (stmt, consumed) = parse_one(lines, i)?;
48        stmts.push(stmt);
49        i += consumed;
50    }
51    Ok(stmts)
52}
53
54/// Parse one or more lines (one statement); returns (stmt, number of lines consumed).
55fn parse_one(lines: &[String], start: usize) -> Result<(ScriptStmt, usize), ParseError> {
56    let line = lines
57        .get(start)
58        .ok_or_else(|| ParseError("unexpected end".to_string()))?;
59    let line = line.trim();
60
61    if line == "set -e" {
62        return Ok((ScriptStmt::SetE, 1));
63    }
64    if let Some((name, value)) = parse_assign(line) {
65        return Ok((ScriptStmt::Assign(name, value), 1));
66    }
67    if let Some(rest) = line.strip_prefix("source ") {
68        let path = rest.trim();
69        if path.is_empty() {
70            return Err(ParseError("source: missing path".to_string()));
71        }
72        return Ok((ScriptStmt::Source(path.to_string()), 1));
73    }
74    if let Some(path) = line.strip_prefix(". ") {
75        let path = path.trim();
76        if path.is_empty() {
77            return Err(ParseError(".: missing path".to_string()));
78        }
79        return Ok((ScriptStmt::Source(path.to_string()), 1));
80    }
81    if let Some(rest) = line.strip_prefix("if ") {
82        return parse_if_block(lines, start, rest.trim());
83    }
84    if let Some(rest) = line.strip_prefix("for ") {
85        return parse_for_block(lines, start, rest.trim());
86    }
87    if let Some(rest) = line.strip_prefix("while ") {
88        return parse_while_block(lines, start, rest.trim());
89    }
90    Ok((ScriptStmt::Command(line.to_string()), 1))
91}
92
93fn parse_if_block(
94    lines: &[String],
95    start: usize,
96    rest: &str,
97) -> Result<(ScriptStmt, usize), ParseError> {
98    let (cond, _has_then) = if let Some(pos) = rest.find("; then") {
99        (rest[..pos].trim().to_string(), true)
100    } else if rest.contains("then") {
101        let pos = rest.find("then").unwrap();
102        (rest[..pos].trim().trim_end_matches(';').to_string(), true)
103    } else {
104        return Err(ParseError("if: missing 'then'".to_string()));
105    };
106    let mut then_body = Vec::new();
107    let mut else_body = None;
108    let mut i = start + 1;
109    while i < lines.len() {
110        let l = lines[i].trim();
111        if l == "fi" {
112            return Ok((
113                ScriptStmt::If {
114                    cond,
115                    then_body,
116                    else_body,
117                },
118                i - start + 1,
119            ));
120        }
121        if l == "else" {
122            else_body = Some(Vec::new());
123            i += 1;
124            continue;
125        }
126        let (stmt, n) = parse_one(lines, i)?;
127        if let Some(else_b) = &mut else_body {
128            else_b.push(stmt);
129        } else {
130            then_body.push(stmt);
131        }
132        i += n;
133    }
134    Err(ParseError("if: missing 'fi'".to_string()))
135}
136
137fn parse_for_block(
138    lines: &[String],
139    start: usize,
140    rest: &str,
141) -> Result<(ScriptStmt, usize), ParseError> {
142    let (var, words) = if let Some(pos) = rest.find(" in ") {
143        let var = rest[..pos].trim();
144        let after_in = rest[pos + " in ".len()..].trim();
145        let words = if let Some(semi) = after_in.find("; do") {
146            split_words(&after_in[..semi])
147        } else {
148            return Err(ParseError("for: missing '; do'".to_string()));
149        };
150        (var.to_string(), words)
151    } else {
152        return Err(ParseError("for: missing 'in'".to_string()));
153    };
154    if !is_identifier(&var) {
155        return Err(ParseError("for: invalid variable name".to_string()));
156    }
157    let mut body = Vec::new();
158    let mut i = start + 1;
159    while i < lines.len() {
160        if lines[i].trim() == "done" {
161            return Ok((ScriptStmt::For { var, words, body }, i - start + 1));
162        }
163        let (stmt, n) = parse_one(lines, i)?;
164        body.push(stmt);
165        i += n;
166    }
167    Err(ParseError("for: missing 'done'".to_string()))
168}
169
170fn parse_while_block(
171    lines: &[String],
172    start: usize,
173    rest: &str,
174) -> Result<(ScriptStmt, usize), ParseError> {
175    let cond = if let Some(pos) = rest.find("; do") {
176        rest[..pos].trim().to_string()
177    } else {
178        return Err(ParseError("while: missing '; do'".to_string()));
179    };
180    let mut body = Vec::new();
181    let mut i = start + 1;
182    while i < lines.len() {
183        if lines[i].trim() == "done" {
184            return Ok((ScriptStmt::While { cond, body }, i - start + 1));
185        }
186        let (stmt, n) = parse_one(lines, i)?;
187        body.push(stmt);
188        i += n;
189    }
190    Err(ParseError("while: missing 'done'".to_string()))
191}
192
193pub(super) fn split_words(s: &str) -> Vec<String> {
194    s.split_whitespace().map(String::from).collect()
195}