xtask_todo_lib/devshell/script/
parse.rs1use 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
20fn 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
39pub 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
54fn 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}