summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2025-08-13 19:06:31 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2025-08-13 19:06:31 +0200
commit0f00400df98327dc01681567dc2313a9c6e57953 (patch)
treece4a6af92cc6026b16a95945eb05f394933ec2aa /src/main.rs
parentab36c1b5178ee0e7934b1c5cdcf2ccfccc3514bb (diff)
Remove the Rust version.
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs844
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()
- }
-}