diff options
author | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2025-07-29 06:54:55 +0200 |
---|---|---|
committer | Matthias Andreas Benkard <code@mail.matthias.benkard.de> | 2025-07-29 06:54:55 +0200 |
commit | f291892643dd00d38612347c3c007e00fab8666e (patch) | |
tree | 9f3053090a87a75dfe538bd74faff6f54c5859d3 | |
parent | 39eabc3cbc1930c0b7b7afa6d21482d663f6c0a9 (diff) |
Implement ECHO.
-rw-r--r-- | Cargo.lock | 39 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/main.rs | 161 |
3 files changed, 195 insertions, 7 deletions
@@ -18,6 +18,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -208,6 +217,7 @@ dependencies = [ "crossterm", "eyre", "prettytable-rs", + "regex", "rustyline", "thiserror 2.0.12", ] @@ -527,6 +537,35 @@ dependencies = [ ] [[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] name = "rustc-demangle" version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -17,4 +17,6 @@ thiserror = "2" crossterm = "0.29.0" prettytable-rs = "0.10.0" rustyline = "16" +regex = "1.11.1" +#crossbeam = "0.8.4" #termwiz = "0.23" diff --git a/src/main.rs b/src/main.rs index 20ca930..0662c98 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,31 +1,155 @@ -use eyre::Result; +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 rl = DefaultEditor::new()?; + let mut line_editor = DefaultEditor::new()?; + loop { - let line = rl.readline(">>> ")?; - println!("{line}"); + 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<Command>, right: Box<Command> }, Redirect { command: Box<Command>, target: FileSpec }, External { program: String, args: Vec<String> }, - Builtin { name: BuiltinCommand }, + Builtin(BuiltinCommand), + Empty, } +struct CommandReceivers { + stdout: Receiver<Vec<u8>>, + stderr: Receiver<Vec<u8>>, + exit_status: Receiver<u16>, +} + +struct CommandSenders { + stdout: SyncSender<Vec<u8>>, + stderr: SyncSender<Vec<u8>>, + exit_status: SyncSender<u16>, +} + +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<CommandReceivers> { + 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)?; + } + + _ => bail!("Command::run not implemented for {:?}", self), + } + + Ok(receivers) + } +} + +#[derive(Debug)] enum BuiltinCommand { // File-oriented Type { file: FileSpec }, @@ -80,7 +204,30 @@ enum BuiltinCommand { } impl Command { - fn parse(_input: &str) -> Result<Command> { - Err(eyre::eyre!("Not implemented")) + fn parse(input: &str) -> Result<Command> { + 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() }, + })), + + _ => + Err(eyre::eyre!("parse not implemented for {:?}", input)) + } } } |