summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthias Andreas Benkard <code@mail.matthias.benkard.de>2025-07-29 06:54:55 +0200
committerMatthias Andreas Benkard <code@mail.matthias.benkard.de>2025-07-29 06:54:55 +0200
commitf291892643dd00d38612347c3c007e00fab8666e (patch)
tree9f3053090a87a75dfe538bd74faff6f54c5859d3
parent39eabc3cbc1930c0b7b7afa6d21482d663f6c0a9 (diff)
Implement ECHO.
-rw-r--r--Cargo.lock39
-rw-r--r--Cargo.toml2
-rw-r--r--src/main.rs161
3 files changed, 195 insertions, 7 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e478679..feca255 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index d46c2c1..4ba529e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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))
+ }
}
}