Initial commit

This commit is contained in:
Riley Apeldoorn 2021-07-05 21:46:21 +02:00
commit 6b9c73cc03
5 changed files with 662 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

71
Cargo.lock generated Normal file
View file

@ -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",
]

9
Cargo.toml Normal file
View file

@ -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 = "*"

568
src/lib.rs Normal file
View file

@ -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<job::Job<job::Running>> {
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 <E> (&self, env: &mut E) -> Result<()>
where
E: Env;
}
impl<F> Exec for F
where
F: Fn (&mut dyn Env) -> Result<()>
{
fn exec <E> (&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<E>(&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<std::ffi::OsStr>) -> 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<std::path::Path> for Program {
fn as_ref (&self) -> &std::path::Path {
std::path::Path::new(&self.0)
}
}
impl Exec for Program {
fn exec <E> (&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::<Builtin>().unwrap();
/// ```
///
/// Parsing an unknown builtin name yields an error.
///
/// ```should_panic
/// # use rush::exec::Builtin;
/// "sjdkgfhgfkfg"
/// .parse::<Builtin>()
/// .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<Self, Self::Err> {
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 <E> (&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::<i32>().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::<Builtin>() {
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 <E> (self, env: &E) -> Result<Value>
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<String>;
/// Get the current working directory.
fn working_dir (&self) -> Result<std::path::PathBuf>;
/// 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<exec::Program> {
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 <String, String>,
parent: &'parent mut E,
}
impl<E: Env> Env for Scope<'_, E> {
fn working_dir (&self) -> Result<std::path::PathBuf> {
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<exec::Program> {
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<String> {
self.bindings
.get(name)
.cloned()
.or_else(|| {
self.parent.get(name)
})
}
}
/// An iterator of arguments.
#[derive(Default)]
pub struct Args (Vec<String>);
impl<S: ToString> FromIterator<S> for Args {
fn from_iter <T: IntoIterator<Item = S>> (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<Self::Item> {
if self.0.len() > 0 {
Some (self.0.remove(0))
} else {
None
}
}
}
/// A completely empty environment.
pub struct Pure {
bindings: HashMap <String, String>,
cwd: std::path::PathBuf,
}
impl Pure {
pub fn init (cwd: impl AsRef<std::path::Path>) -> Pure {
Pure {
cwd: cwd.as_ref().to_owned(),
bindings: HashMap::new(),
}
}
}
impl Env for Pure {
fn args (&self) -> Args {
std::iter::empty::<String>().collect()
}
fn bind (&mut self, name: &str, value: &str) {
self.bindings.insert(name.to_string(), value.to_string());
}
fn get (&self, name: &str) -> Option<String> {
self.bindings.get(name).cloned()
}
fn working_dir (&self) -> Result<std::path::PathBuf> {
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<String> {
std::env::var(name).ok()
}
fn working_dir (&self) -> Result<std::path::PathBuf> {
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 <State: Sealed> {
_t: PhantomData<State>,
}
impl Job<Running> {
/// Attempt to send this job to the background.
pub fn bg (self) -> Result<Job<Stopped>> { todo!() }
}
impl Job<Stopped> {
/// Attempt to pull this job to the foreground.
pub fn fg (self) -> Result<Job<Running>> { 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<std::io::Error> 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<T, E = Error> = std::result::Result<T, E>;
}

13
src/main.rs Normal file
View file

@ -0,0 +1,13 @@
fn main() {
use rush::{
exec::{ self, Exec },
env::{ self, Env },
};
let mut e = env::inherit();
let args: Vec<String> = e.args().collect();
e.search("exa").unwrap().exec(&mut e).unwrap();
}