diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 844 |
1 files changed, 0 insertions, 844 deletions
diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 8b9229e..0000000 --- a/src/main.rs +++ /dev/null @@ -1,844 +0,0 @@ -use crossterm::{cursor, execute, terminal}; -use eyre::{Result, bail}; -use regex::Regex; -use rustyline::DefaultEditor; -use rustyline::error::ReadlineError; -use std::collections::{LinkedList, VecDeque}; -use std::io; -use std::io::Write; -use std::path::{Component, Path, PathBuf}; -use std::sync::mpsc::{Receiver, SyncSender, sync_channel}; -use std::thread::spawn; - -const STDOUT_BUFFER_SIZE: usize = 1024; -const STDERR_BUFFER_SIZE: usize = 1024; - -fn main() -> Result<()> { - color_eyre::install()?; - - let mut line_editor = DefaultEditor::new()?; - let mut prompt_spec = "$p$g"; - - loop { - let cwd = std::env::current_dir()?; - let full_cwd = format_path(&cwd); - let mut interpolated_prompt = prompt_spec.replace("$p", &full_cwd).replace("$g", ">"); - interpolated_prompt.push_str(" "); - - let line = match line_editor.readline(&interpolated_prompt) { - 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, - status, - }) = command.run() - else { - eprintln!("unimplemented command: {}", line); - continue; - }; - - // print stdout, stderr interleaved until 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 command_result = status - .recv() - .expect("failed to receive exit status from command"); - - match command_result { - CommandStatus::ExitShell => break, - CommandStatus::Code(_) => {} - } - } - - Ok(()) -} - -/// Turns "/home/mulk/foo/longfilenames/excellent.text" into "C:\\HOME\\MULK\\FOO\\LONGFILE~1\\EXCELL~1.TEX" -fn format_path(p: &Path) -> String { - use std::path::Component::*; - - let mut prefix: Option<String> = None; - let mut components = Vec::new(); - - for component in p.components() { - match component { - Normal(c) => { - let s = c.to_string_lossy().to_uppercase(); - let s_parts: Vec<&str> = s.splitn(2, '.').collect(); - - let mut name: String; - let mut ext: Option<String>; - if s_parts.len() == 1 { - name = s.clone(); - ext = None; - } else { - name = s_parts[0].into(); - ext = Some(s_parts[1].into()); - } - - let mut result = String::new(); - - if name.len() > 8 || ext.as_ref().map_or(false, |e| e.len() > 3) { - name = name.chars().take(6).collect::<String>(); - name.push_str("~1"); - } - result.push_str(&name.to_uppercase()); - - if let Some(mut e) = ext { - if e.len() > 3 { - e = e.chars().take(3).collect::<String>(); - } - result.push_str("."); - result.push_str(&e.to_uppercase()); - } - - components.push(result); - } - - Prefix(c) => prefix = Some(component.as_os_str().to_string_lossy().to_uppercase()), - - RootDir => { - if prefix.is_none() { - prefix = Some("C:".into()) - } - components.push("".into()); - } - - CurDir => components.push(".".into()), - - ParentDir => components.push("..".into()), - } - } - - let mut result = String::new(); - if let Some(p) = prefix { - result.push_str(&p) - }; - result.push_str(&components.join("\\")); - result -} - -#[derive(Debug)] -enum FileSpec { - Con, - Lpt1, - Lpt2, - Lpt3, - Prn, - Path(PathBuf), -} - -#[derive(Debug)] -enum RedirectType { - OutputOverwrite, // > - OutputAppend, // >> - InputFrom, // < -} - -#[derive(Debug)] -struct Redirect { - redirect_type: RedirectType, - target: FileSpec, -} - -#[derive(Debug)] -enum Command { - Pipe { - left: Box<Command>, - right: Box<Command>, - }, - Redirect { - command: Box<Command>, - redirects: Vec<Redirect>, - }, - External { - program: String, - args: Vec<String>, - }, - Builtin(BuiltinCommand), - Empty, -} - -#[derive(Debug)] -enum CommandStatus { - Code(u16), - ExitShell, -} - -struct CommandReceivers { - stdout: Receiver<Vec<u8>>, - stderr: Receiver<Vec<u8>>, - status: Receiver<CommandStatus>, -} - -struct CommandSenders { - stdout: SyncSender<Vec<u8>>, - stderr: SyncSender<Vec<u8>>, - status: SyncSender<CommandStatus>, -} - -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, - status: sexit, - }, - receivers: CommandReceivers { - stdout: rout, - stderr: rerr, - status: rexit, - }, - } - } - - fn split(self) -> (CommandSenders, CommandReceivers) { - (self.senders, self.receivers) - } -} - -impl Command { - pub(crate) fn run(&self) -> Result<CommandReceivers> { - use crate::BuiltinCommand::*; - use crate::Command::*; - use crate::CommandStatus::*; - - let (senders, receivers) = CommandContext::new().split(); - let CommandSenders { - stdout, - stderr, - status: status, - } = senders; - - match self { - Empty => { - status.send(Code(0))?; - } - - Builtin(EchoText { message }) => { - stdout.send(message.bytes().collect())?; - stdout.send(b"\n".into())?; - status.send(Code(0))?; - } - - Builtin(Cls) => { - let mut stdout_handle = io::stdout(); - execute!(stdout_handle, terminal::Clear(terminal::ClearType::All))?; - execute!(stdout_handle, cursor::MoveTo(0, 0))?; - status.send(Code(0))?; - } - - Builtin(Exit) => { - status.send(ExitShell)?; - } - - _ => 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, -} - -#[derive(Debug, Clone, PartialEq)] -enum Token { - Word(String), - Pipe, - RedirectOut, // > - RedirectAppend, // >> - RedirectIn, // < - Newline, - Eof, -} - -struct Lexer { - input: Vec<char>, - position: usize, - current_char: Option<char>, -} - -impl Lexer { - fn new(input: &str) -> Self { - let chars: Vec<char> = input.chars().collect(); - let current_char = chars.get(0).copied(); - - Self { - input: chars, - position: 0, - current_char, - } - } - - fn advance(&mut self) { - self.position += 1; - self.current_char = self.input.get(self.position).copied(); - } - - fn peek(&self) -> Option<char> { - self.input.get(self.position + 1).copied() - } - - fn skip_whitespace(&mut self) { - while let Some(ch) = self.current_char { - if ch.is_whitespace() && ch != '\n' { - self.advance(); - } else { - break; - } - } - } - - fn read_word(&mut self) -> String { - let mut word = String::new(); - let mut in_quotes = false; - let mut quote_char = '"'; - - while let Some(ch) = self.current_char { - match ch { - // Handle quotes - '"' | '\'' if !in_quotes => { - in_quotes = true; - quote_char = ch; - self.advance(); - }, - ch if in_quotes && ch == quote_char => { - in_quotes = false; - self.advance(); - }, - // Stop at special characters when not in quotes - '|' | '>' | '<' | '\n' if !in_quotes => { - break; - }, - // Stop at whitespace when not in quotes - ch if !in_quotes && ch.is_whitespace() => { - break; - }, - // Regular character - _ => { - word.push(ch); - self.advance(); - } - } - } - - word - } - - fn next_token(&mut self) -> Token { - loop { - match self.current_char { - None => return Token::Eof, - - Some('\n') => { - self.advance(); - return Token::Newline; - }, - - Some(ch) if ch.is_whitespace() => { - self.skip_whitespace(); - continue; - }, - - Some('|') => { - self.advance(); - return Token::Pipe; - }, - - Some('>') => { - self.advance(); - if self.current_char == Some('>') { - self.advance(); - return Token::RedirectAppend; - } else { - return Token::RedirectOut; - } - }, - - Some('<') => { - self.advance(); - return Token::RedirectIn; - }, - - Some(_) => { - let word = self.read_word(); - if word.is_empty() { - // This shouldn't happen, but just in case - self.advance(); - continue; - } - return Token::Word(word); - } - } - } - } - - fn tokenize(&mut self) -> Vec<Token> { - let mut tokens = Vec::new(); - - loop { - let token = self.next_token(); - let is_eof = matches!(token, Token::Eof); - tokens.push(token); - if is_eof { - break; - } - } - - tokens - } -} - -struct Parser { - tokens: Vec<Token>, - position: usize, -} - -impl Parser { - fn new(tokens: Vec<Token>) -> Self { - Self { - tokens, - position: 0, - } - } - - fn current_token(&self) -> &Token { - self.tokens.get(self.position).unwrap_or(&Token::Eof) - } - - fn advance(&mut self) { - if self.position < self.tokens.len() { - self.position += 1; - } - } - - fn expect_word(&mut self) -> Result<String> { - match self.current_token() { - Token::Word(word) => { - let result = word.clone(); - self.advance(); - Ok(result) - }, - token => Err(eyre::eyre!("Expected word, found {:?}", token)), - } - } - - fn parse_command(&mut self) -> Result<Command> { - self.parse_pipeline() - } - - fn parse_pipeline(&mut self) -> Result<Command> { - let mut left = self.parse_redirected_command()?; - - while matches!(self.current_token(), Token::Pipe) { - self.advance(); // consume | - let right = self.parse_redirected_command()?; - left = Command::Pipe { - left: Box::new(left), - right: Box::new(right), - }; - } - - Ok(left) - } - - fn parse_redirected_command(&mut self) -> Result<Command> { - let mut command = self.parse_simple_command()?; - let mut redirects = Vec::new(); - - while matches!(self.current_token(), Token::RedirectOut | Token::RedirectAppend | Token::RedirectIn) { - let redirect_type = match self.current_token() { - Token::RedirectOut => RedirectType::OutputOverwrite, - Token::RedirectAppend => RedirectType::OutputAppend, - Token::RedirectIn => RedirectType::InputFrom, - _ => unreachable!(), - }; - - self.advance(); // consume redirect token - - let target_str = self.expect_word()?; - let target = parse_filespec(&target_str); - - redirects.push(Redirect { - redirect_type, - target, - }); - } - - if redirects.is_empty() { - Ok(command) - } else { - Ok(Command::Redirect { - command: Box::new(command), - redirects, - }) - } - } - - fn parse_simple_command(&mut self) -> Result<Command> { - use BuiltinCommand::*; - use Command::*; - - if matches!(self.current_token(), Token::Eof | Token::Newline) { - return Ok(Empty); - } - - let command_name = self.expect_word()?; - let mut args = Vec::new(); - - // Collect arguments until we hit a special token - while matches!(self.current_token(), Token::Word(_)) { - if let Token::Word(arg) = self.current_token() { - args.push(arg.clone()); - self.advance(); - } - } - - match command_name.to_uppercase().as_str() { - "ECHO" => { - if args.is_empty() { - Ok(Builtin(EchoPlain)) - } else { - match args[0].to_uppercase().as_str() { - "ON" if args.len() == 1 => Ok(Builtin(EchoOn)), - "OFF" if args.len() == 1 => Ok(Builtin(EchoOff)), - _ => Ok(Builtin(EchoText { - message: args.join(" "), - })), - } - } - }, - - "CLS" => Ok(Builtin(Cls)), - - "EXIT" => Ok(Builtin(Exit)), - - "MORE" => Ok(Builtin(More)), - - "VERIFY" => Ok(Builtin(Verify)), - - // File-oriented commands - "COPY" => { - if args.len() >= 2 { - Ok(Builtin(Copy { - from: parse_filespec(&args[0]), - to: parse_filespec(&args[1]), - })) - } else { - Err(eyre::eyre!("COPY requires source and destination")) - } - }, - - "DIR" => Ok(Builtin(Dir { - path: if args.is_empty() { - PathBuf::from(".") - } else { - PathBuf::from(&args[0]) - }, - })), - - "TYPE" => { - if args.is_empty() { - Err(eyre::eyre!("TYPE requires a filename")) - } else { - Ok(Builtin(Type { - file: parse_filespec(&args[0]), - })) - } - }, - - "CD" | "CHDIR" => Ok(Builtin(Chdir { - path: if args.is_empty() { - std::env::current_dir()? - } else { - PathBuf::from(&args[0]) - }, - })), - - "MKDIR" | "MD" => { - if args.is_empty() { - Err(eyre::eyre!("MKDIR requires a directory name")) - } else { - Ok(Builtin(Mkdir { - path: PathBuf::from(&args[0]), - })) - } - }, - - "RMDIR" | "RD" => { - if args.is_empty() { - Err(eyre::eyre!("RMDIR requires a directory name")) - } else { - Ok(Builtin(Rmdir { - path: PathBuf::from(&args[0]), - })) - } - }, - - "DEL" | "ERASE" => { - if args.is_empty() { - Err(eyre::eyre!("DEL requires a filename")) - } else { - Ok(Builtin(Remove { - path: PathBuf::from(&args[0]), - })) - } - }, - - "REM" => Ok(Builtin(Rem { - message: args.join(" "), - })), - - "VER" => Ok(Builtin(Ver)), - - "DATE" => Ok(Builtin(Date)), - - "TIME" => Ok(Builtin(Time)), - - "PATH" => { - if args.is_empty() { - Ok(Builtin(PathGet)) - } else { - Ok(Builtin(PathSet { - value: args.join(" "), - })) - } - }, - - "SET" => { - if args.is_empty() { - Err(eyre::eyre!("SET requires variable name")) - } else { - let full_arg = args.join(" "); - if let Some(eq_pos) = full_arg.find('=') { - let name = full_arg[..eq_pos].trim().to_string(); - let value = full_arg[eq_pos + 1..].trim().to_string(); - Ok(Builtin(Set { name, value })) - } else { - Err(eyre::eyre!("SET requires variable=value format")) - } - } - }, - - // Drive change (like C:, D:, etc.) - _ if command_name.len() == 2 && command_name.ends_with(':') => Ok(Empty), - - // External command - _ => Ok(External { - program: command_name, - args, - }), - } - } -} - -fn parse_filespec(p0: &str) -> FileSpec { - match p0 { - "CON" => FileSpec::Con, - "LPT1" => FileSpec::Lpt1, - "LPT2" => FileSpec::Lpt2, - "LPT3" => FileSpec::Lpt3, - "PRN" => FileSpec::Prn, - _ => FileSpec::Path(PathBuf::from(p0)), - } -} - -impl Command { - fn parse(input: &str) -> Result<Command> { - let trimmed = input.trim(); - if trimmed.is_empty() { - return Ok(Command::Empty); - } - - let mut lexer = Lexer::new(trimmed); - let tokens = lexer.tokenize(); - - let mut parser = Parser::new(tokens); - parser.parse_command() - } -} |