use crossterm::{cursor, execute, terminal}; use eyre::{bail, Result}; use regex::Regex; use rustyline::DefaultEditor; use std::io; use std::io::Write; use std::path::PathBuf; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; use std::thread::spawn; use rustyline::error::ReadlineError; const STDOUT_BUFFER_SIZE: usize = 1024; const STDERR_BUFFER_SIZE: usize = 1024; fn main() -> Result<()> { color_eyre::install()?; let mut line_editor = DefaultEditor::new()?; loop { let line = match line_editor.readline(">>> ") { Ok(line) => line, Err(ReadlineError::Eof) => break, Err(err) => return Err(err.into()) }; let Ok(command) = Command::parse(&line) else { eprintln!("unrecognized command: {}", line); continue; }; line_editor.add_history_entry(&line)?; let Ok(CommandReceivers { stdout, stderr, exit_status }) = command.run() else { eprintln!("unimplemented command: {}", line); continue; }; // print stdout, stderr interleaved until exit_status produces something let stdout_writer = spawn(move || { loop { let Ok(bytes) = stdout.recv() else { break; }; let mut out = io::stderr(); out.write_all(bytes.as_slice()).expect("stdout write failed"); out.flush().expect("stdout flush failed"); } }); let stderr_writer = spawn(move || { loop { let Ok(bytes) = stderr.recv() else { break; }; let mut out = io::stderr(); out.write_all(bytes.as_slice()).expect("stderr write failed"); out.flush().expect("stdout flush failed"); } }); stdout_writer.join().expect("stdout writer thread failed"); stderr_writer.join().expect("stderr writer thread failed"); let _exit_status = exit_status.recv().expect("failed to receive exit status from command"); } Ok(()) } #[derive(Debug)] enum FileSpec { Con, Path(PathBuf), } #[derive(Debug)] enum Command { Pipe { left: Box, right: Box }, Redirect { command: Box, target: FileSpec }, External { program: String, args: Vec }, Builtin(BuiltinCommand), Empty, } struct CommandReceivers { stdout: Receiver>, stderr: Receiver>, exit_status: Receiver, } struct CommandSenders { stdout: SyncSender>, stderr: SyncSender>, exit_status: SyncSender, } struct CommandContext { senders: CommandSenders, receivers: CommandReceivers, } impl CommandContext { fn new() -> Self { let (sout, rout) = sync_channel(STDOUT_BUFFER_SIZE); let (serr, rerr) = sync_channel(STDERR_BUFFER_SIZE); let (sexit, rexit) = sync_channel(1); Self { senders: CommandSenders { stdout: sout, stderr: serr, exit_status: sexit, }, receivers: CommandReceivers { stdout: rout, stderr: rerr, exit_status: rexit, } } } fn split(self) -> (CommandSenders, CommandReceivers) { (self.senders, self.receivers) } } impl Command { pub(crate) fn run(&self) -> Result { use crate::Command::*; use crate::BuiltinCommand::*; let (senders, receivers) = CommandContext::new().split(); let CommandSenders { stdout, stderr, exit_status } = senders; match self { Empty => { exit_status.send(0)?; } Builtin(EchoText { message }) => { stdout.send(message.bytes().collect())?; stdout.send(b"\n".into())?; exit_status.send(0)?; } Builtin(Cls) => { let mut stdout_handle = io::stdout(); execute!(stdout_handle, terminal::Clear(terminal::ClearType::All))?; execute!(stdout_handle, cursor::MoveTo(0, 0))?; exit_status.send(0)?; } _ => bail!("Command::run not implemented for {:?}", self), } Ok(receivers) } } #[derive(Debug)] enum BuiltinCommand { // File-oriented Type { file: FileSpec }, Copy { from: FileSpec, to: FileSpec }, Xcopy { from: FileSpec, to: FileSpec, recursive: bool }, Mkdir { path: PathBuf }, Remove { path: PathBuf }, Rmdir { path: PathBuf }, Rename { from: FileSpec, to: FileSpec }, Dir { path: PathBuf }, Tree { path: PathBuf }, // Shell-oriented PromptGet, PromptSet { message: String }, EchoText { message: String }, EchoOn, EchoOff, EchoPlain, Exit, Set { name: String, value: String }, Chdir { path: PathBuf }, PathGet, PathSet { value: String }, Ver, // Scripting Rem { message: String }, // Utilities Date, Time, // Screen-oriented Cls, // Dummies Verify, // For later Assign, Join, Subst, Truename, Mem, // For much later, if ever Break, Chcp, Ctty, Vol, } impl Command { fn parse(input: &str) -> Result { use Command::*; use BuiltinCommand::*; let whitespace = Regex::new(r"\s+")?; let mut split_input = whitespace.splitn(input, 2); let Some(name) = split_input.next() else { return Ok(Empty); }; let args = split_input.next().unwrap_or(""); match name.to_uppercase().as_str() { "" => Ok(Empty), "ECHO" => Ok(Builtin(match args.to_uppercase().as_str() { "ON" => EchoOn, "OFF" => EchoOff, "" => EchoPlain, _ => EchoText { message: args.to_string() }, })), "CLS" => Ok(Builtin(Cls)), _ => Err(eyre::eyre!("parse not implemented for {:?}", input)) } } }