From 56db51ac99466df9b6fb87cdc54cc859c200065f Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 9 Jun 2019 17:35:29 -0400 Subject: provide a framework for shell builtins --- src/builtins.rs | 71 +++++++++++++++++++++++++++++++++++++++++++++++ src/eval.rs | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 2 ++ src/process.rs | 29 ++++++-------------- src/repl.rs | 27 +++++++++--------- 5 files changed, 180 insertions(+), 34 deletions(-) create mode 100644 src/builtins.rs create mode 100644 src/eval.rs (limited to 'src') diff --git a/src/builtins.rs b/src/builtins.rs new file mode 100644 index 0000000..f4c8c44 --- /dev/null +++ b/src/builtins.rs @@ -0,0 +1,71 @@ +use snafu::{ResultExt, Snafu}; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("unknown builtin {}", cmd))] + UnknownBuiltin { cmd: String }, + + #[snafu(display("failed to cd to {}: {}", dir, source))] + Chdir { dir: String, source: nix::Error }, + + #[snafu(display("cd requires a directory to be given"))] + ChdirParams, +} + +pub fn exec(cmd: &str, args: &[String]) -> Result { + Builtin::new(cmd, args) +} + +pub struct Builtin { + cmd: String, + args: Vec, + done: bool, +} + +impl Builtin { + fn new(cmd: &str, args: &[String]) -> Result { + match cmd { + "cd" => Ok(Builtin { + cmd: cmd.to_string(), + args: args.to_vec(), + done: false, + }), + _ => Err(Error::UnknownBuiltin { + cmd: cmd.to_string(), + }), + } + } +} + +#[must_use = "streams do nothing unless polled"] +impl futures::stream::Stream for Builtin { + type Item = crate::eval::CommandEvent; + type Error = Error; + + fn poll(&mut self) -> futures::Poll, Self::Error> { + if self.done { + return Ok(futures::Async::Ready(None)); + } + + self.done = true; + let res = match self.cmd.as_ref() { + "cd" => cd(&self.args), + _ => Err(Error::UnknownBuiltin { + cmd: self.cmd.clone(), + }), + }; + res.map(|_| { + futures::Async::Ready(Some( + crate::eval::CommandEvent::BuiltinExit, + )) + }) + } +} + +fn cd(args: &[String]) -> Result<(), Error> { + if let Some(dir) = args.get(0) { + nix::unistd::chdir(dir.as_str()).context(Chdir { dir: dir.clone() }) + } else { + Err(Error::ChdirParams) + } +} diff --git a/src/eval.rs b/src/eval.rs new file mode 100644 index 0000000..a46045a --- /dev/null +++ b/src/eval.rs @@ -0,0 +1,85 @@ +use futures::stream::Stream; +use snafu::{ResultExt, Snafu}; + +#[derive(Debug, Snafu)] +pub enum Error { + #[snafu(display("failed to parse command line '{}': {}", line, source))] + ParserError { + line: String, + source: crate::parser::Error, + }, + + #[snafu(display("failed to find command `{}`: {}", cmd, source))] + CommandError { + cmd: String, + source: crate::process::Error, + }, + + #[snafu(display("failed to run builtin command `{}`: {}", cmd, source))] + BuiltinExecution { + cmd: String, + source: crate::builtins::Error, + }, + + #[snafu(display("failed to run executable `{}`: {}", cmd, source))] + ProcessExecution { + cmd: String, + source: crate::process::Error, + }, +} + +pub fn eval(line: &str) -> Result { + Eval::new(line) +} + +pub enum CommandEvent { + Output(Vec), + ProcessExit(std::process::ExitStatus), + BuiltinExit, +} + +pub struct Eval { + stream: Box< + dyn futures::stream::Stream + + Send, + >, +} + +impl Eval { + fn new(line: &str) -> Result { + let (cmd, args) = + crate::parser::parse(line).context(ParserError { line })?; + let builtin_stream = crate::builtins::exec(&cmd, &args); + let stream: Box< + dyn futures::stream::Stream + + Send, + > = if let Ok(s) = builtin_stream { + Box::new(s.map_err(move |e| Error::BuiltinExecution { + cmd: cmd.clone(), + source: e, + })) + } else { + let process_stream = crate::process::spawn(&cmd, &args); + match process_stream { + Ok(s) => { + Box::new(s.map_err(move |e| Error::ProcessExecution { + cmd: cmd.clone(), + source: e, + })) + } + Err(e) => return Err(e).context(CommandError { cmd }), + } + }; + Ok(Eval { stream }) + } +} + +#[must_use = "streams do nothing unless polled"] +impl futures::stream::Stream for Eval { + type Item = CommandEvent; + type Error = Error; + + fn poll(&mut self) -> futures::Poll, Self::Error> { + self.stream.poll() + } +} diff --git a/src/main.rs b/src/main.rs index d283021..3656616 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,5 @@ +mod builtins; +mod eval; mod parser; mod process; mod readline; diff --git a/src/process.rs b/src/process.rs index 9d6582c..d059803 100644 --- a/src/process.rs +++ b/src/process.rs @@ -12,12 +12,6 @@ pub enum Error { #[snafu(display("failed to spawn process for `{}`: {}", cmd, source))] SpawnProcess { cmd: String, source: std::io::Error }, - #[snafu(display("failed to parse command line '{}': {}", line, source))] - ParserError { - line: String, - source: crate::parser::Error, - }, - #[snafu(display("failed to write to pty: {}", source))] WriteToPty { source: std::io::Error }, @@ -40,13 +34,8 @@ pub enum Error { IntoRawMode { source: std::io::Error }, } -pub fn spawn(line: &str) -> Result { - RunningProcess::new(line) -} - -pub enum ProcessEvent { - Output(Vec), - Exit(std::process::ExitStatus), +pub fn spawn(cmd: &str, args: &[String]) -> Result { + RunningProcess::new(cmd, args) } pub struct RunningProcess { @@ -62,14 +51,12 @@ pub struct RunningProcess { } impl RunningProcess { - fn new(line: &str) -> Result { + fn new(cmd: &str, args: &[String]) -> Result { let pty = tokio_pty_process::AsyncPtyMaster::open().context(OpenPty)?; - let (cmd, args) = - crate::parser::parse(line).context(ParserError { line })?; - let process = std::process::Command::new(cmd.clone()) - .args(&args) + let process = std::process::Command::new(cmd) + .args(args) .spawn_pty_async(&pty) .context(SpawnProcess { cmd })?; @@ -92,7 +79,7 @@ impl RunningProcess { #[must_use = "streams do nothing unless polled"] impl futures::stream::Stream for RunningProcess { - type Item = ProcessEvent; + type Item = crate::eval::CommandEvent; type Error = Error; fn poll(&mut self) -> futures::Poll, Self::Error> { @@ -141,7 +128,7 @@ impl futures::stream::Stream for RunningProcess { acc }); return Ok(futures::Async::Ready(Some( - ProcessEvent::Output(bytes), + crate::eval::CommandEvent::Output(bytes), ))); } Ok(futures::Async::NotReady) => { @@ -162,7 +149,7 @@ impl futures::stream::Stream for RunningProcess { Ok(futures::Async::Ready(status)) => { self.exit_done = true; return Ok(futures::Async::Ready(Some( - ProcessEvent::Exit(status), + crate::eval::CommandEvent::ProcessExit(status), ))); } Ok(futures::Async::NotReady) => { diff --git a/src/repl.rs b/src/repl.rs index af87a0f..53e897c 100644 --- a/src/repl.rs +++ b/src/repl.rs @@ -9,7 +9,7 @@ enum Error { ReadError { source: crate::readline::Error }, #[snafu(display("error during eval: {}", source))] - EvalError { source: crate::process::Error }, + EvalError { source: crate::eval::Error }, #[snafu(display("error during print: {}", source))] PrintError { source: std::io::Error }, @@ -24,25 +24,26 @@ pub fn repl() { let repl = read().and_then(|line| { eprint!("running '{}'\r\n", line); eval(&line).fold(None, |acc, event| match event { - crate::process::ProcessEvent::Output(out) => { - match print(&out) { - Ok(()) => futures::future::ok(acc), - Err(e) => futures::future::err(e), - } + crate::eval::CommandEvent::Output(out) => match print(&out) { + Ok(()) => futures::future::ok(acc), + Err(e) => futures::future::err(e), + }, + crate::eval::CommandEvent::ProcessExit(status) => { + futures::future::ok(Some(format!("{}", status))) } - crate::process::ProcessEvent::Exit(status) => { - futures::future::ok(Some(status)) + crate::eval::CommandEvent::BuiltinExit => { + futures::future::ok(Some(format!("success"))) } }) }); Some(repl.then(move |res| match res { Ok(Some(status)) => { - eprint!("process exited with status {}\r\n", status); + eprint!("command exited: {}\r\n", status); return Ok((done, false)); } Ok(None) => { - eprint!("process exited weirdly?\r\n"); + eprint!("command exited weirdly?\r\n"); return Ok((done, false)); } Err(Error::ReadError { @@ -52,7 +53,7 @@ pub fn repl() { } Err(Error::EvalError { source: - crate::process::Error::ParserError { + crate::eval::Error::ParserError { source: crate::parser::Error::CommandRequired, line: _, }, @@ -81,9 +82,9 @@ fn read() -> impl futures::future::Future { fn eval( line: &str, -) -> impl futures::stream::Stream +) -> impl futures::stream::Stream { - crate::process::spawn(line) + crate::eval::eval(line) .into_future() .flatten_stream() .map_err(|e| Error::EvalError { source: e }) -- cgit v1.2.3-54-g00ecf