xtask_todo_lib/devshell/script/
exec.rs1use std::cell::RefCell;
4use std::collections::HashMap;
5use std::fmt;
6use std::io::{BufRead, Read, Write};
7use std::path::Path;
8use std::rc::Rc;
9
10use super::ast::ScriptStmt;
11use super::parse::parse_script;
12use crate::devshell::command::{execute_pipeline, ExecContext, RunResult};
13use crate::devshell::host_text;
14use crate::devshell::parser;
15use crate::devshell::vfs::Vfs;
16use crate::devshell::vm::SessionHolder;
17use crate::devshell::workspace::read_logical_file_bytes_rc;
18
19const MAX_SOURCE_DEPTH: u32 = 64;
20
21#[must_use]
23pub fn read_script_source_text(
24 vfs: &Rc<RefCell<Vfs>>,
25 vm_session: &Rc<RefCell<SessionHolder>>,
26 path: &str,
27) -> Option<String> {
28 if let Ok(bytes) = read_logical_file_bytes_rc(vfs, vm_session, path) {
29 if let Some(t) = host_text::script_text_from_vfs_bytes(&bytes) {
30 return Some(t);
31 }
32 }
33 host_text::read_host_text(Path::new(path)).ok()
34}
35
36#[derive(Debug)]
38pub enum RunScriptError {
39 Parse,
40 CommandFailed,
41 Source,
42}
43
44impl fmt::Display for RunScriptError {
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 match self {
47 Self::Parse => f.write_str("script parse error"),
48 Self::CommandFailed => f.write_str("script command failed"),
49 Self::Source => f.write_str("script source error"),
50 }
51 }
52}
53
54impl std::error::Error for RunScriptError {}
55
56struct ExecScriptContext<'a, R, W1, W2> {
58 vfs: &'a Rc<RefCell<Vfs>>,
59 vm_session: Rc<RefCell<SessionHolder>>,
60 vars: &'a mut HashMap<String, String>,
61 set_e: &'a mut bool,
62 source_depth: u32,
63 stdin: &'a mut R,
64 stdout: &'a mut W1,
65 stderr: &'a mut W2,
66}
67
68fn exec_source<R, W1, W2>(
70 ctx: &mut ExecScriptContext<'_, R, W1, W2>,
71 path: &str,
72) -> Result<bool, RunScriptError>
73where
74 R: BufRead + Read,
75 W1: Write,
76 W2: Write,
77{
78 if ctx.source_depth >= MAX_SOURCE_DEPTH {
79 let _ = writeln!(ctx.stderr, "source: max depth {MAX_SOURCE_DEPTH} exceeded");
80 return Err(RunScriptError::Source);
81 }
82 let content = read_script_source_text(ctx.vfs, &ctx.vm_session, path);
83 let Some(content) = content else {
84 let _ = writeln!(ctx.stderr, "source: cannot read {path}");
85 return Err(RunScriptError::Source);
86 };
87 let lines = logical_lines(&content);
88 let sub = match parse_script(&lines) {
89 Ok(s) => s,
90 Err(e) => {
91 let _ = writeln!(ctx.stderr, "source {path}: {e}");
92 return Err(RunScriptError::Source);
93 }
94 };
95 ctx.source_depth += 1;
96 let result = exec_stmts(ctx, &sub);
97 ctx.source_depth -= 1;
98 result
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
103pub enum CmdOutcome {
104 Success,
105 Failed,
106 Exit,
107}
108
109fn run_command_line<R, W1, W2>(ctx: &mut ExecScriptContext<'_, R, W1, W2>, line: &str) -> CmdOutcome
111where
112 R: BufRead + Read,
113 W1: Write,
114 W2: Write,
115{
116 let line = expand_vars(line, ctx.vars);
117 let line = line.trim();
118 if line.is_empty() {
119 return CmdOutcome::Success;
120 }
121 let pipeline = match parser::parse_line(line) {
122 Ok(p) => p,
123 Err(e) => {
124 let _ = writeln!(ctx.stderr, "parse error: {e}");
125 return CmdOutcome::Failed;
126 }
127 };
128 let first_argv0 = pipeline
129 .commands
130 .first()
131 .and_then(|c| c.argv.first())
132 .map(String::as_str);
133 if first_argv0 == Some("exit") || first_argv0 == Some("quit") {
134 return CmdOutcome::Exit;
135 }
136 let mut vfs_ref = ctx.vfs.borrow_mut();
137 let mut sess_ref = ctx.vm_session.borrow_mut();
138 let mut exec_ctx = ExecContext {
139 vfs: &mut vfs_ref,
140 stdin: ctx.stdin,
141 stdout: ctx.stdout,
142 stderr: ctx.stderr,
143 vm_session: &mut sess_ref,
144 };
145 match execute_pipeline(&mut exec_ctx, &pipeline) {
146 Ok(RunResult::Continue) => CmdOutcome::Success,
147 Ok(RunResult::Exit) => CmdOutcome::Exit,
148 Err(e) => {
149 let _ = writeln!(ctx.stderr, "error: {e}");
150 CmdOutcome::Failed
151 }
152 }
153}
154
155fn exec_stmts<R, W1, W2>(
157 ctx: &mut ExecScriptContext<'_, R, W1, W2>,
158 stmts: &[ScriptStmt],
159) -> Result<bool, RunScriptError>
160where
161 R: BufRead + Read,
162 W1: Write,
163 W2: Write,
164{
165 for stmt in stmts {
166 match stmt {
167 ScriptStmt::Assign(n, v) => {
168 ctx.vars.insert(n.clone(), v.clone());
169 }
170 ScriptStmt::SetE => *ctx.set_e = true,
171 ScriptStmt::Command(line) => {
172 let out = run_command_line(ctx, line);
173 match out {
174 CmdOutcome::Exit => return Ok(false),
175 CmdOutcome::Failed if *ctx.set_e => return Err(RunScriptError::CommandFailed),
176 _ => {}
177 }
178 }
179 ScriptStmt::If {
180 cond,
181 then_body,
182 else_body,
183 } => {
184 let out = run_command_line(ctx, cond);
185 let run_body = if out == CmdOutcome::Success {
186 then_body
187 } else {
188 else_body.as_deref().unwrap_or(&[])
189 };
190 if !run_body.is_empty() {
191 let cont = exec_stmts(ctx, run_body)?;
192 if !cont {
193 return Ok(false);
194 }
195 }
196 }
197 ScriptStmt::For { var, words, body } => {
198 for w in words {
199 let w_expanded = expand_vars(w, ctx.vars);
200 ctx.vars.insert(var.clone(), w_expanded);
201 let cont = exec_stmts(ctx, body)?;
202 if !cont {
203 return Ok(false);
204 }
205 }
206 }
207 ScriptStmt::While { cond, body } => loop {
208 let out = run_command_line(ctx, cond);
209 if out != CmdOutcome::Success {
210 break;
211 }
212 let cont = exec_stmts(ctx, body)?;
213 if !cont {
214 return Ok(false);
215 }
216 },
217 ScriptStmt::Source(path) => {
218 let cont = exec_source(ctx, path)?;
219 if !cont {
220 return Ok(false);
221 }
222 }
223 }
224 }
225 Ok(true)
226}
227
228#[must_use]
230pub fn expand_vars<S: std::hash::BuildHasher>(
231 s: &str,
232 vars: &HashMap<String, String, S>,
233) -> String {
234 let mut out = String::new();
235 let mut i = 0;
236 let bytes = s.as_bytes();
237 while i < bytes.len() {
238 if bytes[i] == b'$' && i + 1 < bytes.len() {
239 if bytes[i + 1] == b'{' {
240 let start = i + 2;
241 let mut end = start;
242 while end < bytes.len()
243 && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_')
244 {
245 end += 1;
246 }
247 if end < bytes.len() && bytes[end] == b'}' {
248 let name = std::str::from_utf8(&bytes[start..end]).unwrap_or("");
249 out.push_str(vars.get(name).map_or("", String::as_str));
250 i = end + 1;
251 continue;
252 }
253 } else if bytes[i + 1] == b'_' || bytes[i + 1].is_ascii_alphabetic() {
254 let start = i + 1;
255 let mut end = start;
256 while end < bytes.len()
257 && (bytes[end].is_ascii_alphanumeric() || bytes[end] == b'_')
258 {
259 end += 1;
260 }
261 let name = std::str::from_utf8(&bytes[start..end]).unwrap_or("");
262 out.push_str(vars.get(name).map_or("", String::as_str));
263 i = end;
264 continue;
265 }
266 }
267 out.push(char::from(bytes[i]));
268 i += 1;
269 }
270 out
271}
272
273#[must_use]
275pub fn logical_lines(source: &str) -> Vec<String> {
276 let raw_lines: Vec<&str> = source.lines().collect();
277 let mut merged: Vec<String> = Vec::new();
278 let mut current = String::new();
279
280 for line in raw_lines {
281 let line = line.trim_end();
282 if current.ends_with('\\') {
283 current.pop();
284 current.push_str(line.trim_start());
285 } else {
286 if !current.is_empty() {
287 merged.push(std::mem::take(&mut current));
288 }
289 current = line.to_string();
290 }
291 }
292 if !current.is_empty() {
293 merged.push(current);
294 }
295
296 let mut out: Vec<String> = Vec::new();
297 for line in merged {
298 let comment_start = line.find('#').unwrap_or(line.len());
299 let line = line[..comment_start].trim();
300 if !line.is_empty() {
301 out.push(line.to_string());
302 }
303 }
304 out
305}
306
307pub fn run_script<R, W1, W2>(
312 vfs: &Rc<RefCell<Vfs>>,
313 vm_session: &Rc<RefCell<SessionHolder>>,
314 script_src: &str,
315 set_e: bool,
316 stdin: &mut R,
317 stdout: &mut W1,
318 stderr: &mut W2,
319) -> Result<(), RunScriptError>
320where
321 R: BufRead + Read,
322 W1: Write,
323 W2: Write,
324{
325 let lines = logical_lines(script_src);
326 let stmts = match parse_script(&lines) {
327 Ok(s) => s,
328 Err(e) => {
329 let _ = writeln!(stderr, "script parse error: {e}");
330 return Err(RunScriptError::Parse);
331 }
332 };
333 let mut vars = HashMap::new();
334 let mut set_e_flag = set_e;
335 let mut ctx = ExecScriptContext {
336 vfs,
337 vm_session: Rc::clone(vm_session),
338 vars: &mut vars,
339 set_e: &mut set_e_flag,
340 source_depth: 0,
341 stdin,
342 stdout,
343 stderr,
344 };
345 let result = exec_stmts(&mut ctx, &stmts);
346 let cwd = vfs.borrow().cwd().to_string();
347 {
348 let mut vfs_mut = vfs.borrow_mut();
349 if let Err(e) = vm_session.borrow_mut().shutdown(&mut vfs_mut, &cwd) {
350 let _ = writeln!(stderr, "dev_shell: session shutdown: {e}");
351 }
352 }
353 result?;
354 Ok(())
355}