From 6b9c73cc03e4e7a6a9b33fd75eb488f7b8fd27fe Mon Sep 17 00:00:00 2001 From: Riley Apeldoorn Date: Mon, 5 Jul 2021 21:46:21 +0200 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.lock | 71 +++++++ Cargo.toml | 9 + src/lib.rs | 568 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 13 ++ 5 files changed, 662 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/lib.rs create mode 100644 src/main.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..32e7908 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,71 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" + +[[package]] +name = "bitflags" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" + +[[package]] +name = "cc" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "libc" +version = "0.2.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nix" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3728fec49d363a50a8828a190b379a446cc5cf085c06259bbbeb34447e4ec7" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "ptyprocess" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ea91820d916e8f4b2173dc2e960a14d6e90d769e97c691d46e9b8a86ee4471" +dependencies = [ + "nix", +] + +[[package]] +name = "rush" +version = "0.1.0" +dependencies = [ + "ptyprocess", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a374a16 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "rush" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ptyprocess = "*" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9f115cd --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,568 @@ +//! The Rush shell. + +use env::job; + +/// Attempt to execute a program, rush expression or builtin. +pub fn exec (prog: impl exec::Exec) -> Result> { + todo!() +} + +pub mod exec { + + //! Execution of commands, builtins and code. + + use super::{ + Result, + env::Env, + }; + + /// Execute the code in the given environment. + pub trait Exec { + + /// Run the executable. + fn exec (&self, env: &mut E) -> Result<()> + where + E: Env; + + } + + impl Exec for F + where + F: Fn (&mut dyn Env) -> Result<()> + { + fn exec (&self, env: &mut E) -> Result<()> + where + E: Env + { + self (env) + } + } + + /// A shell command. + pub enum Command { + Program (Program), + Builtin (Builtin), + } + + impl Exec for Command { + fn exec(&self, env: &mut E) -> Result<()> + where + E: Env + { + match self { + Command::Builtin (builtin) => builtin.exec(env), + Command::Program (program) => program.exec(env), + } + } + } + + /// A path to an executable file representing an external program. + pub struct Program (std::ffi::OsString); + + impl Program { + + /// Create a new [`Program`] struct. + pub (super) fn new (str: impl AsRef) -> Program { + Program (str.as_ref().to_owned()) + } + + /// Get the command name as an [`OsStr`](std::ffi::OsStr) + pub fn as_os_str (&self) -> &std::ffi::OsStr { + &self.0 + } + + /// Get the [`Path`](std::path::Path) of the file the name references. + pub fn as_path (&self) -> &std::path::Path { + self.as_ref() + } + } + + impl AsRef for Program { + fn as_ref (&self) -> &std::path::Path { + std::path::Path::new(&self.0) + } + } + + impl Exec for Program { + fn exec (&self, env: &mut E) -> Result<()> + where + E: Env + { + use ptyprocess::PtyProcess; + use std::io::{ BufRead, Read }; + + let mut cmd = std::process::Command::new(self.as_os_str()); + + cmd.current_dir(env.working_dir()?); + cmd.args(env.args()); + + let proc = PtyProcess::spawn(cmd) + .unwrap(); + + let mut pty = proc + .get_pty_handle() + .unwrap(); + + while let Ok (true) = proc.is_alive() { + let mut buf = String::new(); + pty.read_to_string(&mut buf); + println!("{}", buf); + } + + Ok (()) + } + } + + + /// Represents the case that a string fails to parse to a [`Builtin`] + /// because the associated builtin does not exist. Valid builtin names + /// are listed in the [`builtin`] module. + #[derive(Debug)] + pub struct NotFound; + + /// A shell builtin. See also the [`builtin`] module. + /// + /// # Examples + /// + /// The only way to get a `Builtin` is through [`FromStr`]. + /// + /// ``` + /// # use rush::exec::Builtin; + /// "type".parse::().unwrap(); + /// ``` + /// + /// Parsing an unknown builtin name yields an error. + /// + /// ```should_panic + /// # use rush::exec::Builtin; + /// "sjdkgfhgfkfg" + /// .parse::() + /// .unwrap(); + /// ``` + /// + /// [`FromStr`]: std::str::FromStr + pub struct Builtin (fn (&mut dyn Env) -> Result<()>); + + impl std::str::FromStr for Builtin { + type Err = NotFound; + + fn from_str (s: &str) -> Result { + let ret = Builtin (match s { + "disown" => builtin::disown, + "type" => builtin::r#type, + "exit" => builtin::exit, + "pwd" => builtin::pwd, + "fg" => builtin::fg, + "bg" => builtin::bg, + "cd" => builtin::cd, + _ => return Err (NotFound) + }); + + Ok (ret) + } + } + + impl Exec for Builtin { + fn exec (&self, env: &mut E) -> Result<()> + where + E: Env + { + self.0.exec(env) + } + } + + pub mod builtin { + + //! Shell builtins. + + use super::{ Env, Result, }; + + /// Change the current working directory. + pub fn cd (env: &mut dyn Env) -> Result<()> { + let arg = env.args().next().unwrap(); + env.set_working_dir(arg.as_ref()) + } + + /// Print the current working directory. + pub fn pwd (env: &mut dyn Env) -> Result<()> { + let cwd = env.working_dir()?; + println!("{}", cwd.display()); + Ok (()) + } + + /// Stop the program. Optionally takes an exit code. + pub fn exit (env: &mut dyn Env) -> Result<()> { + if let Some (code) = env.args().next().and_then(|s| s.parse::().ok()) { + std::process::exit(code) + } else { + std::process::exit(0) + } + } + + /// Display information about the type of a command. + pub fn r#type (env: &mut dyn Env) -> Result<()> { + use crate::exec::Builtin; + + for arg in env.args() { + if let Ok (_) = arg.parse::() { + println!("{} is a shell builtin", arg); + } else if let Some (path) = env.search(&arg) { + println!("{} is {}", arg, path.as_path().display()); + } else { + println!("type: {}: not found", arg); + } + } + + Ok (()) + } + + /// Send a job to the background. See also [`Job::bg`]. + /// + /// [`Job::bg`]: crate::job::Job::bg + pub fn bg (_: &mut dyn Env) -> Result<()> { + todo!() + } + + /// Bring a job to the foreground. See also [`Job::fg`]. + /// + /// [`Job::fg`]: crate::job::Job::fg + pub fn fg (_: &mut dyn Env) -> Result<()> { + todo!() + } + + /// Disown all background jobs. + pub fn disown (_: &mut dyn Env) -> Result<()> { + todo!() + } + + } + + +} + +pub mod eval { + + //! The Rush scripting language. + + use super::{ + Result, + env::Env, + }; + + /// The value of an expression. + pub enum Value { + String (String) + } + + /// An identifier. + pub struct Name (String); + + /// Evaluation of expressions in the Rush scripting language. + pub trait Eval { + + /// Evaluates the code in the context of the given [`Env`]. + fn eval (self, env: &E) -> Result + where + E: Env; + + } + + /// An iterator of parsed code generated from an iterator of raw + /// code fragments. + pub struct Parser; +} + +pub mod env { + + //! Defines tools for interacting with the environment + //! of the shell. + + use super::{ Error, Result, exec, }; + + use std::{ + iter::FromIterator, + collections::HashMap, + }; + + /// An environment used to [evaluate expressions](mod@super::eval) or + /// [execute programs](mod@super::exec). + pub trait Env { + + /// Get the arguments passed to the environment. + fn args (&self) -> Args; + + /// Bind a value to a name. + fn bind (&mut self, name: &str, value: &str); + + /// Get a bound value from the env. + fn get (&self, name: &str) -> Option; + + /// Get the current working directory. + fn working_dir (&self) -> Result; + + /// Change the working directory. + fn set_working_dir (&mut self, path: &std::path::Path) -> Result<()>; + + /// Search the `PATH` variable for the `query`. The default implementation + /// is derived from [`Env::get`]. + fn search (&self, query: &str) -> Option { + self.get("PATH")? + .split(':') + .filter_map(|path| { + std::fs::read_dir(path).ok() + }) + .flat_map(|dir| { + dir.filter_map(|entry| entry.ok()) + }) + .map(|file| file.path()) + .find_map(|path| { + if path.file_name()? == query { + Some (path) + } else { + None + } + }) + .map (exec::Program::new) + } + + /// Create a scoped environment to isolate variable + /// bindings. + fn scope (&mut self) -> Scope<'_, Self> + where + Self: Sized + { + Scope { + bindings: HashMap::new(), + parent: self, + } + } + + } + + /// A one-way barrier for variable bindings. Things executed in + /// a `Scope` can read the parent's bindings and shadow them, but + /// not mutate them. + pub struct Scope <'parent, E: Env> { + bindings: HashMap , + parent: &'parent mut E, + } + + impl Env for Scope<'_, E> { + + fn working_dir (&self) -> Result { + self.parent.working_dir() + } + + fn set_working_dir (&mut self, path: &std::path::Path) -> Result<()> { + self.parent.set_working_dir(path) + } + + fn search (&self, query: &str) -> Option { + self.parent.search(query) + } + + fn args (&self) -> Args { + self.parent.args() + } + + fn bind (&mut self, name: &str, value: &str) { + self.bindings.insert(name.to_string(), value.to_string()); + } + + fn get (&self, name: &str) -> Option { + self.bindings + .get(name) + .cloned() + .or_else(|| { + self.parent.get(name) + }) + } + } + + /// An iterator of arguments. + #[derive(Default)] + pub struct Args (Vec); + + impl FromIterator for Args { + fn from_iter > (iter: T) -> Args { + Args ( + iter.into_iter() + .map(|s| s.to_string()) + .collect() + ) + } + } + + impl Iterator for Args { + type Item = String; + + fn next (&mut self) -> Option { + if self.0.len() > 0 { + Some (self.0.remove(0)) + } else { + None + } + } + } + + /// A completely empty environment. + pub struct Pure { + bindings: HashMap , + cwd: std::path::PathBuf, + } + + impl Pure { + pub fn init (cwd: impl AsRef) -> Pure { + Pure { + cwd: cwd.as_ref().to_owned(), + bindings: HashMap::new(), + } + } + } + + impl Env for Pure { + fn args (&self) -> Args { + std::iter::empty::().collect() + } + + fn bind (&mut self, name: &str, value: &str) { + self.bindings.insert(name.to_string(), value.to_string()); + } + + fn get (&self, name: &str) -> Option { + self.bindings.get(name).cloned() + } + + fn working_dir (&self) -> Result { + if self.cwd.exists() { + Ok (self.cwd.clone()) + } else { + use std::io::ErrorKind; + Err (Error::Io (ErrorKind::NotFound.into())) + } + } + + fn set_working_dir (&mut self, path: &std::path::Path) -> Result<()> { + if path.is_dir() && path.exists() { + Ok (self.cwd = path.to_owned()) + } else { + use std::io::ErrorKind; + Err (Error::Io (ErrorKind::InvalidInput.into())) + } + } + } + + /// The default global environment, which inherits from the environment of the + /// shell process. + pub struct Inherit {} + + impl Env for Inherit { + fn args (&self) -> Args { + std::env::args().skip(1).collect() + } + + fn bind (&mut self, name: &str, value: &str) { + std::env::set_var(name, value); + } + + fn get (&self, name: &str) -> Option { + std::env::var(name).ok() + } + + fn working_dir (&self) -> Result { + std::env::current_dir() + .map_err(Error::Io) + } + + fn set_working_dir (&mut self, path: &std::path::Path) -> Result<()> { + std::env::set_current_dir(path) + .map_err(Error::Io) + } + } + + /// Initialize the default environment by inheriting from the environment of + /// the shell process. + pub fn inherit () -> impl Env { + Inherit {} + } + + pub mod history { + + //! History control. + + } + + pub mod io { + + //! Interaction with stdio and the file system. + + } + + pub mod job { + + //! Job scheduling. + + use super::Result; + + use std::marker::PhantomData; + + use sealed::Sealed; + mod sealed { + pub trait Sealed {} + } + + /// An empty type used to tag a [`Job`] as running in the background. + pub enum Stopped {} + impl Sealed for Stopped {} + + /// An empty type used to tag a [`Job`] as running in the foreground. + pub enum Running {} + impl Sealed for Running {} + + /// Represents a running process, either in the foreground + /// or in the background. + pub struct Job { + _t: PhantomData, + } + + impl Job { + + /// Attempt to send this job to the background. + pub fn bg (self) -> Result> { todo!() } + + } + + impl Job { + + /// Attempt to pull this job to the foreground. + pub fn fg (self) -> Result> { todo!() } + + } + + } +} + +pub use err::{ + Error, + Result, +}; + +mod err { + + /// A global union type of errors produced anywhere within the Rush shell. + #[derive(Debug)] + pub enum Error { + Io (std::io::Error) + } + + impl From for Error { + fn from (err: std::io::Error) -> Self { + Error::Io (err) + } + } + + /// A less verbose way to use the [`Result`](std::result::Result) type with + /// an [`Error`]. + pub type Result = std::result::Result; + +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..eb199a5 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,13 @@ +fn main() { + use rush::{ + exec::{ self, Exec }, + env::{ self, Env }, + }; + + let mut e = env::inherit(); + + let args: Vec = e.args().collect(); + + e.search("exa").unwrap().exec(&mut e).unwrap(); + +}