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, Lpt1, Lpt2, Lpt3, Prn, 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 Copy { from: FileSpec, to: FileSpec }, Deltree { path: PathBuf }, Dir { path: PathBuf }, Fc, Find, Mkdir { path: PathBuf }, Move, Remove { path: PathBuf }, Rename { from: FileSpec, to: FileSpec }, Replace, Rmdir { path: PathBuf }, Sort, Tree { path: PathBuf }, Type { file: FileSpec }, Xcopy { from: FileSpec, to: FileSpec, recursive: bool }, // Shell-oriented Append, Chdir { path: PathBuf }, EchoOff, EchoOn, EchoPlain, EchoText { message: String }, Exit, PathGet, PathSet { value: String }, PromptGet, PromptSet { message: String }, Set { name: String, value: String }, Setver, Ver, // Utilities Date, Time, // Screen-oriented Cls, More, // Dummies Verify, Fastopen, Smartdrv, Sizer, // For later Assign, Attrib, Chkdsk, Doskey, Dosshell, Edit, Fasthelp, Help, Join, Mem, Power, Subst, Truename, // For much later, if ever Break, Chcp, Ctty, Defrag, Diskcopy, Emm386, Fdisk, Format, Interlnk, Keyb, Label, Mode, Msav, Msbackup, Mscdex, Msd, Print, Qbasic, Restore, Scandisk, Share, Sys, Undelete, Unformat, Vol, Vsafe, // Scripting Call, Choice, Echo, For, Goto, If, Pause, Prompt, Rem { message: String }, Shift, } 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)) } } }