Skip to main content

xtask_todo_lib/devshell/
mod.rs

1//! Devshell REPL and VFS: same logic as the `cargo-devshell` binary, exposed so tests can cover it.
2
3pub mod command;
4pub mod completion;
5pub mod host_text;
6pub mod parser;
7pub mod sandbox;
8pub mod script;
9pub mod serialization;
10pub mod todo_io;
11pub mod vfs;
12pub mod vm;
13
14mod repl;
15
16use std::cell::RefCell;
17use std::io::{self, BufReader, IsTerminal};
18use std::path::Path;
19use std::rc::Rc;
20
21use vfs::Vfs;
22
23/// Error from `run_with` (usage or REPL failure).
24#[derive(Debug)]
25pub enum RunWithError {
26    Usage,
27    ReplFailed,
28}
29
30impl std::fmt::Display for RunWithError {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            Self::Usage => f.write_str("usage error"),
34            Self::ReplFailed => f.write_str("repl failed"),
35        }
36    }
37}
38
39impl std::error::Error for RunWithError {}
40
41/// Run the devshell using process args and standard I/O (for the binary).
42///
43/// # Errors
44/// Returns an error if usage is wrong or I/O fails critically.
45pub fn run_main() -> Result<(), Box<dyn std::error::Error>> {
46    let args: Vec<String> = std::env::args().collect();
47    let is_tty = io::stdin().is_terminal();
48    let mut stdin = BufReader::new(io::stdin());
49    let mut stdout = io::stdout();
50    let mut stderr = io::stderr();
51    run_main_from_args(&args, is_tty, &mut stdin, &mut stdout, &mut stderr)
52}
53
54/// Same as `run_main` but takes args, `is_tty`, and streams (for tests and callers that supply I/O).
55///
56/// # Errors
57/// Returns an error if usage is wrong or I/O fails critically.
58pub fn run_main_from_args<R, W1, W2>(
59    args: &[String],
60    is_tty: bool,
61    stdin: &mut R,
62    stdout: &mut W1,
63    stderr: &mut W2,
64) -> Result<(), Box<dyn std::error::Error>>
65where
66    R: std::io::BufRead + std::io::Read,
67    W1: std::io::Write,
68    W2: std::io::Write,
69{
70    let positionals: Vec<&str> = args
71        .iter()
72        .skip(1)
73        .filter(|a| *a != "-e" && *a != "-f")
74        .map(String::as_str)
75        .collect();
76    let set_e = args.iter().skip(1).any(|a| a == "-e");
77    let run_script = args.iter().skip(1).any(|a| a == "-f");
78
79    if run_script {
80        if positionals.len() != 1 {
81            writeln!(stderr, "usage: dev_shell [-e] -f script.dsh")?;
82            return Err(Box::new(std::io::Error::other("usage")));
83        }
84        let script_path = positionals[0];
85        let script_src = match host_text::read_host_text(Path::new(script_path)) {
86            Ok(s) => s,
87            Err(e) => {
88                writeln!(stderr, "dev_shell: {script_path}: {e}")?;
89                return Err(e.into());
90            }
91        };
92        let bin_path = Path::new(".dev_shell.bin");
93        let vfs = match serialization::load_from_file(bin_path) {
94            Ok(v) => v,
95            Err(e) => {
96                if e.kind() != io::ErrorKind::NotFound {
97                    let _ = writeln!(stderr, "Failed to load {}: {}", bin_path.display(), e);
98                }
99                Vfs::new()
100            }
101        };
102        let vfs = Rc::new(RefCell::new(vfs));
103        let Ok(vm_session) = vm::try_session_rc(stderr) else {
104            return Err(Box::new(std::io::Error::other("vm session")));
105        };
106        script::run_script(
107            &vfs,
108            &vm_session,
109            &script_src,
110            bin_path,
111            set_e,
112            stdin,
113            stdout,
114            stderr,
115        )
116        .map_err(|e| Box::new(std::io::Error::other(e.to_string())) as Box<dyn std::error::Error>)
117    } else {
118        let path = match positionals.as_slice() {
119            [] => Path::new(".dev_shell.bin"),
120            [p] => Path::new(p),
121            _ => {
122                writeln!(stderr, "usage: dev_shell [options] [path]")?;
123                return Err(Box::new(std::io::Error::other("usage")));
124            }
125        };
126        let vfs = match serialization::load_from_file(path) {
127            Ok(v) => v,
128            Err(e) => {
129                if e.kind() == io::ErrorKind::NotFound {
130                    if positionals.len() > 1 {
131                        let _ = writeln!(stderr, "File not found, starting with empty VFS");
132                    }
133                } else {
134                    let _ = writeln!(stderr, "Failed to load {}: {}", path.display(), e);
135                }
136                Vfs::new()
137            }
138        };
139        let vfs = Rc::new(RefCell::new(vfs));
140        let Ok(vm_session) = vm::try_session_rc(stderr) else {
141            return Err(Box::new(std::io::Error::other("vm session")));
142        };
143        repl::run(&vfs, &vm_session, is_tty, path, stdin, stdout, stderr).map_err(|()| {
144            Box::new(std::io::Error::other("repl error")) as Box<dyn std::error::Error>
145        })?;
146        Ok(())
147    }
148}
149
150/// Run the devshell with given args and streams (for tests).
151///
152/// # Errors
153/// Returns `RunWithError::Usage` on invalid args; `RunWithError::ReplFailed` if the REPL exits with error.
154pub fn run_with<R, W1, W2>(
155    args: &[String],
156    stdin: &mut R,
157    stdout: &mut W1,
158    stderr: &mut W2,
159) -> Result<(), RunWithError>
160where
161    R: std::io::BufRead + std::io::Read,
162    W1: std::io::Write,
163    W2: std::io::Write,
164{
165    let path = match args {
166        [] | [_] => Path::new(".dev_shell.bin"),
167        [_, path] => Path::new(path),
168        _ => {
169            let _ = writeln!(stderr, "usage: dev_shell [path]");
170            return Err(RunWithError::Usage);
171        }
172    };
173    let vfs = serialization::load_from_file(path).unwrap_or_default();
174    let vfs = Rc::new(RefCell::new(vfs));
175    let vm_session = vm::try_session_rc(stderr).map_err(|()| RunWithError::ReplFailed)?;
176    repl::run(&vfs, &vm_session, false, path, stdin, stdout, stderr)
177        .map_err(|()| RunWithError::ReplFailed)
178}
179
180#[cfg(test)]
181mod tests;