Compare commits
50 Commits
parse-tree
...
main
Author | SHA1 | Date |
---|---|---|
Jordan Orelli | 17d51617e0 | 9 months ago |
Jordan Orelli | fd730452ca | 9 months ago |
Jordan Orelli | 8ed0868e56 | 9 months ago |
Jordan Orelli | b48e0b65d0 | 9 months ago |
Jordan Orelli | af181b372c | 9 months ago |
Jordan Orelli | e71c3f89f7 | 9 months ago |
Jordan Orelli | d25382ee10 | 10 months ago |
Jordan Orelli | 3d71f25528 | 10 months ago |
Jordan Orelli | 08b2b3be52 | 10 months ago |
Jordan Orelli | cdfa848f46 | 10 months ago |
Jordan Orelli | 90f8a8c8f2 | 10 months ago |
Jordan Orelli | 11a3911f28 | 10 months ago |
Jordan Orelli | 70e40cb7ae | 10 months ago |
Jordan Orelli | 1153c2dc49 | 10 months ago |
Jordan Orelli | 437afd9d76 | 10 months ago |
Jordan Orelli | 447dd3edc1 | 10 months ago |
Jordan Orelli | 965cb45437 | 10 months ago |
Jordan Orelli | 1d504ff36d | 10 months ago |
Jordan Orelli | 0a821851a3 | 10 months ago |
Jordan Orelli | 731bd69477 | 10 months ago |
Jordan Orelli | 5676c6ddcc | 11 months ago |
Jordan Orelli | 11088138af | 11 months ago |
Jordan Orelli | 3d231156e3 | 11 months ago |
Jordan Orelli | a847b56b05 | 11 months ago |
Jordan Orelli | 5505cb48c6 | 11 months ago |
Jordan Orelli | be5bebf25b | 11 months ago |
Jordan Orelli | 678b5f7932 | 11 months ago |
Jordan Orelli | 9c7d64e512 | 11 months ago |
Jordan Orelli | 621f64d3f1 | 11 months ago |
Jordan Orelli | 71f148139e | 11 months ago |
Jordan Orelli | 948326df4f | 11 months ago |
Jordan Orelli | 680adc5882 | 11 months ago |
Jordan Orelli | f39a22d408 | 11 months ago |
Jordan Orelli | 68909d22a8 | 11 months ago |
Jordan Orelli | 27ebbf7c97 | 11 months ago |
Jordan Orelli | 5108e4457f | 11 months ago |
Jordan Orelli | 97602bf42e | 11 months ago |
Jordan Orelli | f0dc5f2e64 | 11 months ago |
Jordan Orelli | aa6bcedcf0 | 11 months ago |
Jordan Orelli | 7004de4e82 | 11 months ago |
Jordan Orelli | 56bfdcc9fc | 11 months ago |
Jordan Orelli | cb53fb9195 | 11 months ago |
Jordan Orelli | cd51f4cce1 | 11 months ago |
Jordan Orelli | d152c4092a | 1 year ago |
Jordan Orelli | 4a0db72d4e | 1 year ago |
Jordan Orelli | 522668be28 | 2 years ago |
Jordan Orelli | 05793bd42c | 2 years ago |
Jordan Orelli | 1715ba037f | 2 years ago |
Jordan Orelli | 845679eebc | 2 years ago |
Jordan Orelli | 5de73c2c06 | 2 years ago |
@ -0,0 +1,44 @@
|
||||
# clyde
|
||||
|
||||
A command-line shell for Windows.
|
||||
|
||||
## examples
|
||||
|
||||
In all examples, the Clyde prompt is written as follows:
|
||||
|
||||
```
|
||||
▷
|
||||
```
|
||||
|
||||
### Run a built-in command
|
||||
|
||||
Built-in commands are invoked by executing them with a bare string
|
||||
|
||||
```
|
||||
c:\dev\clyde ▷ cd src
|
||||
c:\dev\clyde\src ▷
|
||||
```
|
||||
|
||||
### Run an external command
|
||||
|
||||
External commands are invoked in the same way:
|
||||
|
||||
```
|
||||
c:\dev\clyde ▷ git status
|
||||
On branch main
|
||||
Your branch is up to date with 'origin/main'.
|
||||
|
||||
nothing to commit, working tree clean
|
||||
```
|
||||
|
||||
### Run commands back to back
|
||||
|
||||
To run two commands back to back, separate them with a semicolon. Note that
|
||||
this interaction is entirely unconditional: the second command executes
|
||||
regardless of whether the first command succeeded or not.
|
||||
|
||||
```
|
||||
c:\dev\clyde ▷ echo one; echo two
|
||||
one
|
||||
two
|
||||
```
|
@ -0,0 +1,82 @@
|
||||
/// changes directory
|
||||
mod cd;
|
||||
|
||||
mod ls;
|
||||
|
||||
/// prints stuff
|
||||
mod echo;
|
||||
|
||||
/// prints environment variables
|
||||
mod printenv;
|
||||
|
||||
mod pwd;
|
||||
|
||||
/// removes files
|
||||
mod rm;
|
||||
|
||||
mod mkdir;
|
||||
|
||||
/// the butt of a file
|
||||
mod tail;
|
||||
|
||||
/// locates a binary on our PATH
|
||||
mod which;
|
||||
|
||||
use crate::{
|
||||
error::ExecError,
|
||||
run::{Call, Context, Value},
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// All of the builtin commands that exist
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Builtin {
|
||||
Changedir,
|
||||
Echo,
|
||||
Ls,
|
||||
Mkdir,
|
||||
Printenv,
|
||||
Pwd,
|
||||
Rm,
|
||||
Tail,
|
||||
Which,
|
||||
}
|
||||
|
||||
impl Builtin {
|
||||
fn resolve(&self) -> &dyn Call {
|
||||
use Builtin::*;
|
||||
match self {
|
||||
Changedir => &cd::Changedir,
|
||||
Echo => &echo::Echo,
|
||||
Ls => &ls::Ls,
|
||||
Mkdir => &mkdir::Mkdir,
|
||||
Printenv => &printenv::Printenv,
|
||||
Pwd => &pwd::Pwd,
|
||||
Rm => &rm::Rm,
|
||||
Tail => &tail::Tail,
|
||||
Which => &which::Which,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Call for Builtin {
|
||||
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
|
||||
self.resolve().call(ctx, args)
|
||||
}
|
||||
}
|
||||
|
||||
/// A mapping of all of the builtin command values to their names
|
||||
pub fn all() -> HashMap<&'static str, Builtin> {
|
||||
use Builtin::*;
|
||||
HashMap::from([
|
||||
("cd", Changedir),
|
||||
("echo", Echo),
|
||||
("ls", Ls),
|
||||
("mkdir", Mkdir),
|
||||
("printenv", Printenv),
|
||||
("pwd", Pwd),
|
||||
("rm", Rm),
|
||||
("tail", Tail),
|
||||
("which", Which),
|
||||
])
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
use crate::{
|
||||
error::ExecError,
|
||||
run::{Call, Context, Value},
|
||||
};
|
||||
use std::env;
|
||||
|
||||
pub struct Changedir;
|
||||
|
||||
impl Call for Changedir {
|
||||
fn call(&self, _ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
|
||||
match args.len() {
|
||||
0 => {
|
||||
todo!()
|
||||
}
|
||||
1 => {
|
||||
let name = args[0].try_as_str()?;
|
||||
env::set_current_dir(name).map_err(|e| ExecError::Misc(e.to_string()))?;
|
||||
}
|
||||
_ => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
use crate::{
|
||||
error::ExecError,
|
||||
run::{Call, Context, Value},
|
||||
};
|
||||
use std::io::Write;
|
||||
|
||||
pub struct Echo;
|
||||
|
||||
impl Call for Echo {
|
||||
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
|
||||
let args = args
|
||||
.into_iter()
|
||||
.map(|v| v.try_as_str())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
_ = write!(ctx.stdout, "{}\n", args.join(" "));
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
use crate::{
|
||||
error::ExecError,
|
||||
run::{Call, Context, Value},
|
||||
};
|
||||
use std::{env, fs, io::Write};
|
||||
|
||||
pub struct Ls;
|
||||
|
||||
impl Call for Ls {
|
||||
fn call(&self, ctx: &mut Context, _args: &[Value]) -> Result<Value, ExecError> {
|
||||
let cwd = env::current_dir()?;
|
||||
let dir = fs::read_dir(&cwd)?;
|
||||
for child in dir {
|
||||
let child_path = child?.path();
|
||||
let fname = child_path
|
||||
.file_name()
|
||||
.ok_or_else(|| ExecError::new("no file name"))?
|
||||
.to_str()
|
||||
.ok_or_else(|| ExecError::new("non-unicode file name"))?;
|
||||
_ = write!(ctx.stdout, "{}\n", fname);
|
||||
}
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
use crate::{
|
||||
error::ExecError,
|
||||
run::{Call, Context, Value},
|
||||
};
|
||||
use std::fs;
|
||||
|
||||
pub struct Mkdir;
|
||||
|
||||
impl Call for Mkdir {
|
||||
fn call(&self, _ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
|
||||
match args.len() {
|
||||
0 => {
|
||||
todo!()
|
||||
}
|
||||
1 => {
|
||||
let path = args[0].try_as_str()?;
|
||||
fs::create_dir(path)?;
|
||||
}
|
||||
_ => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
use crate::{
|
||||
error::ExecError,
|
||||
run::{Call, Context, Value},
|
||||
};
|
||||
use std::{env, io::Write};
|
||||
|
||||
pub struct Printenv;
|
||||
|
||||
impl Call for Printenv {
|
||||
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
|
||||
if args.len() > 0 {
|
||||
for arg in args {
|
||||
let name = arg.try_as_str()?;
|
||||
_ = match env::var(name) {
|
||||
Ok(val) => writeln!(ctx.stdout, "{}", val),
|
||||
Err(e) => writeln!(ctx.stderr, "ERROR {}", e),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
for (name, val) in env::vars() {
|
||||
_ = writeln!(ctx.stdout, "{name}={val}");
|
||||
}
|
||||
}
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
use crate::{
|
||||
error::ExecError,
|
||||
run::{Call, Context, Value},
|
||||
};
|
||||
use std::{env, io::Write};
|
||||
|
||||
pub struct Pwd;
|
||||
|
||||
impl Call for Pwd {
|
||||
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
|
||||
let cwd = env::current_dir()?;
|
||||
_ = write!(ctx.stdout, "{}\n", cwd.display());
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
use crate::{
|
||||
error::ExecError,
|
||||
run::{Call, Context, Value},
|
||||
};
|
||||
use std::fs;
|
||||
|
||||
pub struct Rm;
|
||||
|
||||
impl Call for Rm {
|
||||
fn call(&self, _ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
|
||||
match args.len() {
|
||||
0 => {
|
||||
todo!()
|
||||
}
|
||||
1 => {
|
||||
let path = args[0].try_as_str()?;
|
||||
fs::remove_file(path).map_err(|err| ExecError::Misc(err.to_string()))?;
|
||||
}
|
||||
_ => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
use crate::{
|
||||
error::ExecError,
|
||||
run::{Call, Context, Value},
|
||||
};
|
||||
use std::{env, io::Write};
|
||||
|
||||
pub struct Which;
|
||||
|
||||
impl Call for Which {
|
||||
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError> {
|
||||
let path = env::var("path").unwrap_or_default();
|
||||
let dirs: Vec<&str> = path.split(";").collect();
|
||||
|
||||
for arg in args {
|
||||
let name = arg.try_as_str()?;
|
||||
for d in dirs.iter() {
|
||||
let dir = std::path::Path::new(d);
|
||||
let fname = dir.join(name).with_extension("exe");
|
||||
if fname.exists() && fname.is_file() {
|
||||
_ = writeln!(ctx.stdout, "{}", fname.to_str().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Value::None)
|
||||
}
|
||||
}
|
@ -0,0 +1,474 @@
|
||||
use crate::{
|
||||
history::History,
|
||||
input::{self, ControlCharacter, Escape},
|
||||
log::*,
|
||||
output,
|
||||
prompt::Prompt,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use std::{cmp::min, io::Write};
|
||||
|
||||
/// The text buffer of a text editor.
|
||||
pub struct Buffer {
|
||||
/// the current text of the edit buffer
|
||||
chars: Vec<char>,
|
||||
|
||||
/// the position of our edit cursor within [Self::chars]
|
||||
cursor: usize,
|
||||
}
|
||||
|
||||
impl Buffer {
|
||||
/// an empty buffer
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
chars: Vec::new(),
|
||||
cursor: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// clears our edit buffer
|
||||
pub fn clear(&mut self) {
|
||||
self.cursor = 0;
|
||||
self.chars.clear();
|
||||
self.show_debug();
|
||||
}
|
||||
|
||||
/// takes the text out of the editor, leaving an empty buffer in its place
|
||||
fn pop(&mut self) -> String {
|
||||
let s: String = self.chars.iter().collect();
|
||||
self.clear();
|
||||
self.show_debug();
|
||||
s
|
||||
}
|
||||
|
||||
fn show(&self) -> String {
|
||||
self.chars.iter().collect()
|
||||
}
|
||||
|
||||
/// moves the edit cursor back by a fixed number of characters without modifying the contents
|
||||
/// of the buffer
|
||||
fn back(&mut self, n: usize) -> bool {
|
||||
if self.cursor > 0 {
|
||||
if n > self.cursor {
|
||||
self.cursor = n;
|
||||
} else {
|
||||
self.cursor -= n;
|
||||
}
|
||||
self.show_debug();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// drops a character from the text buffer at the cursor's location
|
||||
fn backspace(&mut self) -> bool {
|
||||
if self.chars.len() > 0 {
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
}
|
||||
self.chars.remove(self.cursor);
|
||||
self.show_debug();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// removes the elements left of the cursor, returning the count of removed elements
|
||||
fn clear_left(&mut self) -> usize {
|
||||
if self.cursor > 0 {
|
||||
let n = self.chars.drain(..self.cursor).count();
|
||||
self.cursor = 0;
|
||||
self.show_debug();
|
||||
n
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// moves the edit cursor to the beginning of the buffer. The returned value is the index of
|
||||
/// the edit cursor before the move.
|
||||
fn seek_left(&mut self) -> usize {
|
||||
let n = self.cursor;
|
||||
self.cursor = 0;
|
||||
self.show_debug();
|
||||
n
|
||||
}
|
||||
|
||||
/// moves the edit cursor to the end of the buffer. The returned value is the index of th eedit
|
||||
/// cursor before the move.
|
||||
fn seek_right(&mut self) -> usize {
|
||||
let n = self.chars.len() - self.cursor;
|
||||
self.cursor = self.chars.len();
|
||||
self.show_debug();
|
||||
n
|
||||
}
|
||||
|
||||
/// moves the edit cursor forward by n positions
|
||||
fn forward(&mut self, n: usize) -> bool {
|
||||
if self.cursor < self.chars.len() {
|
||||
self.cursor = min(self.chars.len(), self.cursor + n);
|
||||
self.show_debug();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// inserts a character into the edit buffer at the cursor's current location
|
||||
fn insert(&mut self, c: char) {
|
||||
self.chars.insert(self.cursor, c);
|
||||
self.cursor += 1;
|
||||
self.show_debug();
|
||||
}
|
||||
|
||||
/// the portion of the text buffer from the edit cursor to the end of the buffer
|
||||
fn tail(&self) -> String {
|
||||
let mut start = self.cursor;
|
||||
if start > 0 {
|
||||
start -= 1;
|
||||
}
|
||||
|
||||
let chars = &self.chars[start..];
|
||||
chars.iter().collect()
|
||||
}
|
||||
|
||||
/// the current position of the edit cursor
|
||||
fn pos(&self) -> usize {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
/// the length of the text buffer
|
||||
fn len(&self) -> usize {
|
||||
self.chars.len()
|
||||
}
|
||||
|
||||
fn show_debug(&self) {
|
||||
let before = if self.chars.len() > 0 {
|
||||
self.chars[0..self.cursor].to_vec().iter().collect()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let after = if self.chars.len() > 0 {
|
||||
self.chars[self.cursor..].to_vec().iter().collect()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
debug!(
|
||||
" {: <5} {: >5} {}·{}",
|
||||
self.cursor,
|
||||
self.chars.len(),
|
||||
before,
|
||||
after
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Describes how we handle an action from the editor
|
||||
enum Status {
|
||||
/// Editing is still in progress
|
||||
Ongoing,
|
||||
|
||||
/// A command has been entered in the editor and should be sent to the interpreter
|
||||
Submit(String),
|
||||
|
||||
/// The edit session has ended and the shell should shut down
|
||||
Done,
|
||||
}
|
||||
|
||||
/// A primitive terminal-based text editor
|
||||
pub struct Editor {
|
||||
/// the incoming terminal events
|
||||
input: input::Reader,
|
||||
|
||||
/// the in-memory representation of the command that we're currently editing
|
||||
buffer: Buffer,
|
||||
|
||||
/// our outgoing terminal events, which is used as the display portion of the editor
|
||||
display: output::Writer,
|
||||
|
||||
/// this thing draws our prompt
|
||||
prompt: Prompt,
|
||||
|
||||
/// Our current position when scrolling through the history. A hist_pos of 0 means that we're
|
||||
/// not looking at the history. hist_pos of 1 means that we're looking at the first most-recent
|
||||
/// command entered.
|
||||
hist_pos: usize,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
input: input::Reader::new()?,
|
||||
buffer: Buffer::new(),
|
||||
display: output::Writer::stdout()?,
|
||||
prompt: Prompt::new(),
|
||||
hist_pos: 0,
|
||||
})
|
||||
}
|
||||
|
||||
/// reads one command from the editor. This function blocks until the user has entered an
|
||||
/// entire command.
|
||||
pub fn read_command(&mut self, history: &mut History) -> Result<Option<String>> {
|
||||
use input::Event::*;
|
||||
|
||||
loop {
|
||||
let event = self.input.next()?;
|
||||
let status = match event {
|
||||
Key(e) => self.handle_key_press(e)?,
|
||||
Focus(true) => {
|
||||
self.focus_start();
|
||||
Status::Ongoing
|
||||
}
|
||||
Focus(false) => {
|
||||
self.focus_end();
|
||||
Status::Ongoing
|
||||
}
|
||||
Control(cc) => self.handle_control(cc)?,
|
||||
Escape(escape) => self.handle_escape(escape, history)?,
|
||||
Size => {
|
||||
debug!("ignoring size event");
|
||||
Status::Ongoing
|
||||
}
|
||||
Menu(_) => {
|
||||
debug!("ignoring menu event");
|
||||
Status::Ongoing
|
||||
}
|
||||
Mouse { x, y } => {
|
||||
debug!("ignoring mouse event {x}, {y}");
|
||||
Status::Ongoing
|
||||
}
|
||||
};
|
||||
match status {
|
||||
Status::Ongoing => {}
|
||||
Status::Done => return Ok(None),
|
||||
Status::Submit(e) => {
|
||||
self.hist_pos = 0;
|
||||
return Ok(Some(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_key_press(&mut self, event: crate::key::Event) -> Result<Status> {
|
||||
use crate::key::codes::*;
|
||||
match event {
|
||||
_ if event.code == ENTER || event.char == '\r' => return self.submit(),
|
||||
_ if event.code == TAB || event.char == '\t' => {}
|
||||
_ if event.char == '\u{7f}' => self.backspace()?,
|
||||
_ if event.ctrl && event.code == A => self.seek_left()?,
|
||||
_ if event.ctrl && event.code == D => return Ok(Status::Done),
|
||||
_ if event.ctrl && event.code == E => self.seek_right()?,
|
||||
_ if event.ctrl && event.code == U => self.clear_left()?,
|
||||
_ if event.down && !event.char.is_control() => self.insert(event.char)?,
|
||||
_ => debug!("ignored key press: {event:?}"),
|
||||
}
|
||||
Ok(Status::Ongoing)
|
||||
}
|
||||
|
||||
fn handle_escape(&mut self, esc: Escape, history: &History) -> Result<Status> {
|
||||
use Escape::*;
|
||||
match esc {
|
||||
Left => self.back(1)?,
|
||||
Right => self.forward(1)?,
|
||||
Up => self.prev_history(history)?,
|
||||
Down => self.next_history(history)?,
|
||||
Home => self.seek_left()?,
|
||||
End => self.seek_right()?,
|
||||
esc => debug!(" Ignored escape: {esc:?}"),
|
||||
}
|
||||
Ok(Status::Ongoing)
|
||||
}
|
||||
|
||||
fn handle_control(&mut self, cc: ControlCharacter) -> Result<Status> {
|
||||
use ControlCharacter::*;
|
||||
match cc {
|
||||
StartOfHeading => self.seek_left()?,
|
||||
EndOfTransmission => return Ok(Status::Done),
|
||||
Enquiry => self.seek_right()?,
|
||||
Formfeed => self.clear_screen()?,
|
||||
Linefeed => self.insert_dot()?,
|
||||
NegativeAcknowledge => self.clear_left()?,
|
||||
cc => debug!("ignored control character: {cc:?}"),
|
||||
}
|
||||
Ok(Status::Ongoing)
|
||||
}
|
||||
|
||||
fn insert_dot(&mut self) -> Result<()> {
|
||||
self.insert('\u{2022}')?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn submit(&mut self) -> Result<Status> {
|
||||
self.display.newline()?;
|
||||
let text = self.buffer.pop();
|
||||
info!("◇ {}", text);
|
||||
Ok(Status::Submit(text))
|
||||
}
|
||||
|
||||
fn focus_start(&mut self) {}
|
||||
|
||||
fn focus_end(&mut self) {}
|
||||
|
||||
/// moves the edit cursor one character to the left
|
||||
pub fn back(&mut self, n: usize) -> Result<()> {
|
||||
debug!("⛬ ←");
|
||||
if self.buffer.back(n) {
|
||||
self.display.back(n)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// moves the edit cursor one character to the right
|
||||
pub fn forward(&mut self, n: usize) -> Result<()> {
|
||||
debug!("⛬ →");
|
||||
if self.buffer.forward(n) {
|
||||
self.display.forward(n)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// moves the cursor position to the end of the line
|
||||
pub fn seek_right(&mut self) -> Result<()> {
|
||||
info!("»");
|
||||
let n = self.buffer.seek_right();
|
||||
if n > 0 {
|
||||
// move right by the distance seeked
|
||||
self.display.forward(n)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// moves the cursor position to the beginning of the line
|
||||
pub fn seek_left(&mut self) -> Result<()> {
|
||||
info!("«");
|
||||
let n = self.buffer.seek_left();
|
||||
if n > 0 {
|
||||
// move left by the distance seeked
|
||||
self.display.back(n)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// clears the line from the current cursor position to the beginning of the line
|
||||
pub fn clear_left(&mut self) -> Result<()> {
|
||||
info!("» clear left");
|
||||
let n = self.buffer.clear_left();
|
||||
if n > 0 {
|
||||
// move left by the number of elements removed
|
||||
self.display.back(n)?;
|
||||
// draw the elements remaining, followed by a space for each removed
|
||||
// element
|
||||
let kept = self.buffer.show();
|
||||
let text = format!("{}{:width$}", kept, "", width = n);
|
||||
self.display.write(text.as_bytes())?;
|
||||
self.display.back(n + kept.chars().count())?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// clears the scrollback buffer, moving the current edit line to the top of the screen, but
|
||||
/// leaving the edit cursor in place
|
||||
pub fn clear_screen(&mut self) -> Result<()> {
|
||||
info!("» clear");
|
||||
self.display.clear()?;
|
||||
self.prompt.print(&mut self.display)?;
|
||||
self.display.write(self.buffer.show().as_bytes())?;
|
||||
self.display.back(self.buffer.len() - self.buffer.pos())?;
|
||||
self.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn show_prompt(&mut self) -> Result<()> {
|
||||
self.reset()?;
|
||||
self.prompt.print(&mut self.display)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// inserts a character at edit cursor's current position
|
||||
pub fn insert(&mut self, c: char) -> Result<()> {
|
||||
self.buffer.insert(c);
|
||||
|
||||
let tail = self.buffer.tail();
|
||||
let n = tail.chars().count();
|
||||
|
||||
// write everything from the current line cursor out to the output buffer.
|
||||
self.display.write(tail.as_bytes())?;
|
||||
if n > 1 {
|
||||
// if we wrote more than one character, because we weren't at the end, we
|
||||
// need to rewind the terminal cursor to where it was.
|
||||
self.display.back(n - 1)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) -> Result<()> {
|
||||
if !self.buffer.backspace() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// move cursor back two spaces
|
||||
self.display.back(2)?;
|
||||
let tail = format!("{} ", self.buffer.tail());
|
||||
let n = tail.chars().count();
|
||||
self.display.write(tail.as_bytes())?;
|
||||
|
||||
// after writing out the tail, rewind by the number of characters in
|
||||
// the tail
|
||||
if n > 1 {
|
||||
self.display.back(n - 1)?;
|
||||
} else {
|
||||
// honestly I can't remember how I figured this out
|
||||
self.display.write(b" \x1b[1D")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> Result<()> {
|
||||
self.buffer.clear();
|
||||
self.display.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn prev_history(&mut self, history: &History) -> Result<()> {
|
||||
let text = match history.look_back(self.hist_pos + 1) {
|
||||
Some(s) => {
|
||||
self.hist_pos += 1;
|
||||
s
|
||||
}
|
||||
None => return Ok(()),
|
||||
};
|
||||
self.seek_right()?;
|
||||
self.clear_left()?;
|
||||
for c in text.chars() {
|
||||
self.insert(c)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_history(&mut self, history: &History) -> Result<()> {
|
||||
if self.hist_pos <= 1 {
|
||||
self.seek_right()?;
|
||||
self.clear_left()?;
|
||||
self.hist_pos = 0;
|
||||
return Ok(());
|
||||
}
|
||||
let text = match history.look_back(self.hist_pos - 1) {
|
||||
Some(s) => {
|
||||
self.hist_pos -= 1;
|
||||
s
|
||||
}
|
||||
None => return Ok(()),
|
||||
};
|
||||
self.seek_right()?;
|
||||
self.clear_left()?;
|
||||
for c in text.chars() {
|
||||
self.insert(c)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
mod command;
|
||||
mod tail;
|
||||
mod which;
|
||||
mod printenv;
|
||||
mod echo;
|
||||
|
||||
pub use command::Command;
|
||||
pub use echo::Echo;
|
||||
pub use tail::Tail;
|
||||
pub use which::Which;
|
||||
pub use printenv::Printenv;
|
||||
|
||||
/*
|
||||
Posix Shell builtins:
|
||||
|
||||
alias bg cd command false fc fg getopts hash jobs kill
|
||||
newgrp pwd read true type ulimit umask unalias wait
|
||||
|
||||
Bourne Shell builtins:
|
||||
|
||||
: - do nothing except expand arguments and perform redirections
|
||||
. - read and execute a file in the current shell context
|
||||
break - exit from a loop
|
||||
cd - change the current working directory to directory
|
||||
continue - resume the next iteration of a loop
|
||||
eval - evaluate the arguments as a single command
|
||||
exec - replace the shell without creating a new process
|
||||
exit - exits the process with a status
|
||||
export - exports environment variables
|
||||
getopts - used by shell scripts to parse arguments
|
||||
hash - displays the hash table used by the shell to map command names to files
|
||||
pwd - prints the absolute path of the current directory
|
||||
readonly - mark each name as readonly so that they cannot be reassigned
|
||||
return - causes a shell function to stop execution and return a value
|
||||
shift - shifts the position parameters to the left by n
|
||||
test - evaluate a conditional expression and return a status of 0 or 1
|
||||
times - print out the user and system times used by the shell and its children. sorry, what the fuck is this
|
||||
trap - execute commands when a given signal is received
|
||||
umask - set the shell process's file creation mask
|
||||
unset - clears variable or function names
|
||||
|
||||
Bash Builtins:
|
||||
|
||||
alias - creates an alias for a command
|
||||
bind - adds key bindings
|
||||
builtin - access a builtin function even if it has been masked by another function
|
||||
caller - tells you what line number you're executing?
|
||||
command - runs a command with a given name, ignoring shell functions
|
||||
declare - declare variables and give them attributes
|
||||
echo - echos the outputs
|
||||
enable - enable and disable builtin shell commands
|
||||
help - display help text about builtin commands
|
||||
let - perform arithmetic on shell variables
|
||||
local - create local variables
|
||||
logout - exits a login shell with a status
|
||||
mapfile - read lines from stdin or a file into an array
|
||||
printf - formats arguments using a format string
|
||||
read - reads one line from stdin or from a file
|
||||
readarray - read lines from stdin into an array or something
|
||||
source - executes the script in the current shell
|
||||
type - tells you if a thing is an alias, function, builtin, or file command
|
||||
typeset - synonym for declare, included for korn shell compatibility
|
||||
ulimit - provides control over the resources available to the processes started by the shell
|
||||
unalias - clears an alias
|
||||
|
||||
*/
|
@ -1,7 +0,0 @@
|
||||
use anyhow::Result;
|
||||
|
||||
pub trait Command {
|
||||
fn name() -> String;
|
||||
fn create() -> Self;
|
||||
fn exec(&mut self, args: Vec<&str>) -> Result<bool>;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
use crate::ext::Command;
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct Echo {}
|
||||
|
||||
impl Command for Echo {
|
||||
fn name() -> String {
|
||||
String::from("echo")
|
||||
}
|
||||
|
||||
fn create() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn exec(&mut self, args: Vec<&str>) -> Result<bool> {
|
||||
println!("{}", args.join(" "));
|
||||
Ok(true)
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
use crate::ext::Command;
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct Printenv {}
|
||||
|
||||
impl Command for Printenv {
|
||||
fn name() -> String {
|
||||
String::from("printenv")
|
||||
}
|
||||
|
||||
fn create() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn exec(&mut self, args: Vec<&str>) -> Result<bool> {
|
||||
if args.len() > 0 {
|
||||
let name = args[0];
|
||||
match std::env::var(name) {
|
||||
Ok(val) => {
|
||||
println!("{}", val);
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
println!("ERROR {}", e);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("which variable you fucking dork");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +0,0 @@
|
||||
use crate::ext::Command;
|
||||
use anyhow::Result;
|
||||
|
||||
pub struct Which {}
|
||||
|
||||
impl Command for Which {
|
||||
fn name() -> String {
|
||||
String::from("which")
|
||||
}
|
||||
|
||||
fn create() -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
fn exec(&mut self, args: Vec<&str>) -> Result<bool> {
|
||||
if args.len() > 0 {
|
||||
let path = std::env::var("path").unwrap();
|
||||
let dirs: Vec<&str> = path.split(";").collect();
|
||||
for d in dirs {
|
||||
let dir = std::path::Path::new(d);
|
||||
let fname = dir.join(args[0]).with_extension("exe");
|
||||
if fname.exists() && fname.is_file() {
|
||||
println!("{}", fname.to_str().unwrap());
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
println!("not found: {}", args[0]);
|
||||
Ok(false)
|
||||
} else {
|
||||
println!("what do you want to look for?");
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
pub struct History {
|
||||
events: Vec<String>,
|
||||
}
|
||||
|
||||
impl History {
|
||||
pub fn new() -> Self {
|
||||
Self { events: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn add<S: Into<String>>(&mut self, event: S) {
|
||||
let text = event.into();
|
||||
if text.is_empty() {
|
||||
return;
|
||||
}
|
||||
self.events.push(text);
|
||||
}
|
||||
|
||||
pub fn look_back(&self, idx: usize) -> Option<&str> {
|
||||
if idx == 0 || idx > self.events.len() {
|
||||
return None;
|
||||
}
|
||||
Some(&self.events[self.events.len() - idx])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn new_history() {
|
||||
let mut history = History::new();
|
||||
assert_eq!(history.look_back(1), None);
|
||||
assert_eq!(history.look_back(0), None);
|
||||
|
||||
history.add("Hello");
|
||||
assert_eq!(history.look_back(1), Some("Hello"));
|
||||
assert_eq!(history.look_back(0), None);
|
||||
|
||||
history.add("Hello again");
|
||||
assert_eq!(history.look_back(2), Some("Hello"));
|
||||
assert_eq!(history.look_back(1), Some("Hello again"));
|
||||
assert_eq!(history.look_back(0), None);
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
use crate::{
|
||||
edit,
|
||||
history::History,
|
||||
log::*,
|
||||
output,
|
||||
run::{self, Eval},
|
||||
syntax::parse,
|
||||
};
|
||||
|
||||
use std::{
|
||||
error::Error,
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use dirs;
|
||||
|
||||
/// An interactive session. The Session object is the top-level object used to control an
|
||||
/// interactive terminal session.
|
||||
pub struct Session {
|
||||
pub editor: edit::Editor,
|
||||
pub stdout: output::Writer,
|
||||
pub stderr: output::Writer,
|
||||
}
|
||||
|
||||
impl Session {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
editor: edit::Editor::new()?,
|
||||
stdout: output::Writer::stdout()?,
|
||||
stderr: output::Writer::stderr()?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(mut self) -> Result<()> {
|
||||
let mut state = run::State::new();
|
||||
let mut ctx = run::Context {
|
||||
stdout: self.stdout.clone(),
|
||||
stderr: self.stderr.clone(),
|
||||
state: &mut state,
|
||||
};
|
||||
let mut history = History::new();
|
||||
|
||||
info!("» shell session start --------");
|
||||
loop {
|
||||
self.editor.show_prompt()?;
|
||||
let text = match self.editor.read_command(&mut history)? {
|
||||
Some(text) => text,
|
||||
None => break,
|
||||
};
|
||||
history.add(text.clone());
|
||||
let command = match parse(&text) {
|
||||
Ok(ast) => ast,
|
||||
Err(e) => {
|
||||
self.render_error(e)?;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
if let Err(e) = command.eval(&mut ctx) {
|
||||
self.render_error(e)?;
|
||||
}
|
||||
}
|
||||
info!("» exit");
|
||||
self.stdout.newline()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn enable_logging<P>(&self, path: P)
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let log_path = self.expand_path(path);
|
||||
match Log::file(log_path) {
|
||||
Ok(f) => {
|
||||
let target = Box::leak(Box::new(f));
|
||||
_ = set_logger(target).map(|()| set_max_level(LevelFilter::Debug));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("did not open log file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expand_path<P: AsRef<Path>>(&self, p: P) -> PathBuf {
|
||||
let p = p.as_ref();
|
||||
|
||||
match p.to_str() {
|
||||
Some(s) => {
|
||||
if s.len() == 0 || !s.starts_with('~') {
|
||||
return p.to_path_buf();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return p.to_path_buf();
|
||||
}
|
||||
}
|
||||
|
||||
let mut parts = p.components();
|
||||
let first = parts.next().unwrap().as_os_str().to_str().unwrap();
|
||||
if first == "~" {
|
||||
dirs::home_dir().unwrap().join(parts)
|
||||
} else {
|
||||
let my_home = dirs::home_dir().unwrap();
|
||||
let home_root = my_home.parent().unwrap();
|
||||
let first = &first[1..];
|
||||
let other_home = home_root.join(first);
|
||||
other_home.join(parts)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_error<E: Error>(&mut self, e: E) -> io::Result<()> {
|
||||
self.render_error_helper(e, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_error_helper<E: Error>(&mut self, e: E, depth: u8) -> io::Result<()> {
|
||||
if depth > 0 {
|
||||
writeln!(self.stdout, " {e}")?;
|
||||
} else {
|
||||
writeln!(self.stdout, "{e}:")?;
|
||||
}
|
||||
if let Some(cause) = e.source() {
|
||||
self.render_error_helper(cause, depth + 1)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,380 @@
|
||||
use crate::{
|
||||
error::LexError,
|
||||
topo::{Glyph, Glyphs, Position},
|
||||
};
|
||||
use std::{collections::VecDeque, fmt, ops::Range};
|
||||
|
||||
/// A Lexeme is the text of a given [Token]. The lexeme contains no information about the Token's
|
||||
/// meaning or type; it is just a fancy string with position information.
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct Lexeme {
|
||||
elems: Vec<Glyph>,
|
||||
}
|
||||
|
||||
impl Lexeme {
|
||||
/// The area of the input text covered by this lexeme. This might return none of the Lexeme is
|
||||
/// an empty string.
|
||||
fn span(&self) -> Option<Range<Position>> {
|
||||
if self.elems.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Range {
|
||||
start: self.elems[0].position,
|
||||
end: self.elems[self.elems.len() - 1].position,
|
||||
})
|
||||
}
|
||||
|
||||
fn text(&self) -> String {
|
||||
self.elems.as_slice().iter().map(|tg| tg.glyph).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Lexeme {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
let span = match self.span() {
|
||||
Some(span) => span,
|
||||
None => return write!(f, "<empty Lexeme>"),
|
||||
};
|
||||
|
||||
write!(
|
||||
f,
|
||||
"<{text} @{start_line}:{start_column}-{end_line}:{end_column}>",
|
||||
start_line = span.start.line,
|
||||
start_column = span.start.column,
|
||||
end_line = span.end.line,
|
||||
end_column = span.end.column,
|
||||
text = self.text(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Lexeme {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(f, "{}", self.text())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Glyph>> for Lexeme {
|
||||
fn from(v: Vec<Glyph>) -> Self {
|
||||
Self { elems: v }
|
||||
}
|
||||
}
|
||||
|
||||
/// The smallest lexical unit of our language. A token represents a single element of program
|
||||
/// source text, such as an identifier or a piece of punctuation.
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum Token {
|
||||
/// A bare word: a sequence of characters without any quotes. A bare word is always not a glob.
|
||||
Word(Lexeme),
|
||||
|
||||
/// A bare word containing 1 or more of the special characters ? or *
|
||||
Glob(Lexeme),
|
||||
|
||||
Semi(Glyph),
|
||||
|
||||
Pipe(Glyph),
|
||||
}
|
||||
|
||||
impl Token {
|
||||
/// Checks whether or not another token has the same contents as this Token. The eq
|
||||
/// implementation will also consider positional information, whereas the `same` function does
|
||||
/// not.
|
||||
#[allow(unused)]
|
||||
fn same(&self, other: &Self) -> bool {
|
||||
use Token::*;
|
||||
|
||||
println!(
|
||||
"check same self: {self:?} other: {other:?}",
|
||||
self = self.text(),
|
||||
other = other.text()
|
||||
);
|
||||
|
||||
match (self, other) {
|
||||
(Word(a), Word(b)) => a.text() == b.text(),
|
||||
(Glob(a), Glob(b)) => a.text() == b.text(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn text(&self) -> String {
|
||||
use Token::*;
|
||||
|
||||
match self {
|
||||
Word(lexeme) | Glob(lexeme) => lexeme.text(),
|
||||
Pipe(glyph) | Semi(glyph) => String::from(glyph.glyph),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tokenizer splits some input [Glyphs] into [Token] values. The Tokenize has no lookahead or
|
||||
/// buffering.
|
||||
struct Tokenizer<'text> {
|
||||
source: Glyphs<'text>,
|
||||
}
|
||||
|
||||
impl<'text> Tokenizer<'text> {
|
||||
/// creates a new tokenizer for some given input source text
|
||||
pub fn new(text: &'text str) -> Self {
|
||||
Self {
|
||||
source: Glyphs::new(text),
|
||||
}
|
||||
}
|
||||
|
||||
/// produces the next token in our input source text. If we've hit EOF, this will return None.
|
||||
fn next_token(&mut self) -> Option<Result<Token, LexError>> {
|
||||
self.source.yeet_whitespace();
|
||||
let next = self.source.next()?;
|
||||
|
||||
match next.glyph {
|
||||
_ if next.is_word() => Some(self.lex_bare_string(vec![next])),
|
||||
_ if next.is_glob() => Some(self.lex_glob(vec![next])),
|
||||
'|' => Some(Ok(Token::Pipe(next))),
|
||||
'@' => Some(self.lex_var(vec![next])),
|
||||
'\'' => Some(self.lex_raw_string(vec![next])),
|
||||
'"' => Some(self.lex_string_literal(vec![])),
|
||||
';' => Some(Ok(Token::Semi(next))),
|
||||
_ => Some(Err(LexError::UnexpectedCharacter(next))),
|
||||
}
|
||||
}
|
||||
|
||||
fn lex_bare_string(&mut self, mut progress: Vec<Glyph>) -> Result<Token, LexError> {
|
||||
while let Some(next) = self.source.peek() {
|
||||
match next.glyph {
|
||||
_ if next.glyph.is_whitespace() => break,
|
||||
_ if next.is_word() => progress.push(self.source.pop()?),
|
||||
_ if next.is_glob() => {
|
||||
progress.push(self.source.pop()?);
|
||||
return self.lex_glob(progress);
|
||||
}
|
||||
';' | '|' => break,
|
||||
_ => {
|
||||
return Err(LexError::UnexpectedCharacterInBareString(
|
||||
self.source.pop()?,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if progress.is_empty() {
|
||||
Err(LexError::UnexpectedEOF(self.source.next_position))
|
||||
} else {
|
||||
Ok(Token::Word(progress.into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn lex_glob(&mut self, mut progress: Vec<Glyph>) -> Result<Token, LexError> {
|
||||
while let Some(next) = self.source.peek() {
|
||||
match next.glyph {
|
||||
';' => break,
|
||||
_ if next.glyph.is_whitespace() => break,
|
||||
_ if next.is_glob() => progress.push(self.source.pop()?),
|
||||
// '\\' => {
|
||||
// self.source.pop()?;
|
||||
// progress.push(self.source.pop()?);
|
||||
// }
|
||||
_ => return Err(LexError::UnexpectedCharacterInGlob(self.source.pop()?)),
|
||||
}
|
||||
}
|
||||
|
||||
if progress.is_empty() {
|
||||
Err(LexError::UnexpectedEOF(self.source.next_position))
|
||||
} else {
|
||||
Ok(Token::Glob(progress.into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn lex_raw_string(&mut self, _progress: Vec<Glyph>) -> Result<Token, LexError> {
|
||||
Err(LexError::not_yet(
|
||||
self.source.next_position,
|
||||
"raw strings not done yet",
|
||||
))
|
||||
}
|
||||
|
||||
fn lex_string_literal(&mut self, mut progress: Vec<Glyph>) -> Result<Token, LexError> {
|
||||
while let Some(next) = self.source.peek() {
|
||||
match next.glyph {
|
||||
_ if next.glyph.is_whitespace() => progress.push(self.source.pop()?),
|
||||
_ if next.is_word() => progress.push(self.source.pop()?),
|
||||
_ if next.is_glob() => progress.push(self.source.pop()?),
|
||||
'"' => {
|
||||
self.source.pop()?;
|
||||
break;
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
if progress.is_empty() {
|
||||
Err(LexError::UnexpectedEOF(self.source.next_position))
|
||||
} else {
|
||||
Ok(Token::Word(progress.into()))
|
||||
}
|
||||
}
|
||||
|
||||
fn lex_var(&mut self, _progress: Vec<Glyph>) -> Result<Token, LexError> {
|
||||
Err(LexError::not_yet(
|
||||
self.source.next_position,
|
||||
"variables are not done yet",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'text> Iterator for Tokenizer<'text> {
|
||||
type Item = Result<Token, LexError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.next_token()
|
||||
}
|
||||
}
|
||||
|
||||
/// A Lexer is responsible for converting a piece of source text into a sequence of tokens.
|
||||
pub struct Lexer<'text> {
|
||||
source: Tokenizer<'text>,
|
||||
lookahead: VecDeque<Token>,
|
||||
}
|
||||
|
||||
impl<'text> Lexer<'text> {
|
||||
pub fn new(source: &'text str) -> Self {
|
||||
Self {
|
||||
source: Tokenizer::new(source),
|
||||
lookahead: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_lookahead(&mut self, n: usize) -> Result<bool, LexError> {
|
||||
while self.lookahead.len() < n {
|
||||
let token = match self.source.next() {
|
||||
Some(res) => res?,
|
||||
None => return Ok(false),
|
||||
};
|
||||
self.lookahead.push_back(token);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn peek_at(&mut self, idx: usize) -> Result<Option<&Token>, LexError> {
|
||||
self.fill_lookahead(idx + 1)?;
|
||||
Ok(self.lookahead.get(idx))
|
||||
}
|
||||
|
||||
pub fn peek(&mut self) -> Result<Option<&Token>, LexError> {
|
||||
self.peek_at(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'text> Iterator for Lexer<'text> {
|
||||
type Item = Result<Token, LexError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match self.lookahead.pop_front() {
|
||||
Some(token) => Some(Ok(token)),
|
||||
None => self.source.next(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// splits a corpus into Tokens.
|
||||
#[cfg(test)]
|
||||
pub fn lex(source: &str) -> Result<Vec<Token>, LexError> {
|
||||
Lexer::new(source).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::iter::zip;
|
||||
|
||||
fn lexeme(txt: &str) -> Lexeme {
|
||||
let x: Vec<Glyph> = txt
|
||||
.chars()
|
||||
.map(|c| Glyph {
|
||||
glyph: c,
|
||||
position: Position::origin(),
|
||||
bytes: 0..0,
|
||||
})
|
||||
.collect();
|
||||
x.into()
|
||||
}
|
||||
|
||||
fn glob(txt: &str) -> Token {
|
||||
Token::Glob(lexeme(txt))
|
||||
}
|
||||
|
||||
fn word(txt: &str) -> Token {
|
||||
Token::Word(lexeme(txt))
|
||||
}
|
||||
|
||||
macro_rules! accept {
|
||||
( $($test_name:ident $input_text:literal [ $( $token:expr )* ])+) => {$(
|
||||
#[test]
|
||||
fn $test_name() -> Result<(), LexError> {
|
||||
#[allow(unused_mut)]
|
||||
let mut expected: Vec<Token> = Vec::new();
|
||||
$( expected.push($token); )*
|
||||
|
||||
let lexed = lex($input_text)?;
|
||||
assert_eq!(expected.len(), lexed.len());
|
||||
|
||||
for pair in zip(expected, lexed) {
|
||||
println!("pair: {pair:?}");
|
||||
assert!(pair.0.same(&pair.1));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
macro_rules! reject {
|
||||
($($test_name:ident: $input_text:literal;)+) => {$(
|
||||
#[test]
|
||||
fn $test_name() {
|
||||
match lex($input_text) {
|
||||
Ok(tokens) => {
|
||||
println!("output tokens: {tokens:?}");
|
||||
panic!("Did not encounter an expected lex error");
|
||||
}
|
||||
Err(e) => println!("output error: {e:?}"),
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
reject! {
|
||||
// Vars aren't done yet
|
||||
var: "@name";
|
||||
|
||||
// Single-quoted strings arne't done yet
|
||||
strings: r"echo 'one' two";
|
||||
|
||||
}
|
||||
|
||||
accept! {
|
||||
empty "" []
|
||||
spaces " " []
|
||||
identifier "a" [ word("a") ]
|
||||
identifier_2 " a" [ word("a") ]
|
||||
identifier_3 "a " [ word("a") ]
|
||||
identifier_4 " a " [ word("a") ]
|
||||
double_quoted_strings r#"echo "one" two"# [
|
||||
word("echo")
|
||||
word("one")
|
||||
word("two")
|
||||
]
|
||||
file_name "poop.exe" [ word("poop.exe") ]
|
||||
multi_idents "one two three four " [
|
||||
word("one")
|
||||
word("two")
|
||||
word("three")
|
||||
word("four")
|
||||
]
|
||||
glob_1 "*" [ glob("*") ]
|
||||
glob_2 " * " [ glob("*") ]
|
||||
glob_3 "x*" [ glob("x*") ]
|
||||
glob_4 "*x" [ glob("*x") ]
|
||||
glob_5 "*.py" [ glob("*.py") ]
|
||||
mixed_1 "ls *.py" [ word("ls") glob("*.py") ]
|
||||
abs "cd c:\\one\\two" [ word("cd") word("c:\\one\\two")]
|
||||
home "cd ~\\stuff" [ word("cd") word("~\\stuff") ]
|
||||
}
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
use crate::log::*;
|
||||
use std::cmp::min;
|
||||
|
||||
pub struct Line {
|
||||
/// the current contents of the line
|
||||
chars: Vec<char>,
|
||||
|
||||
/// the cursor position of our dit head within the vector of characters that we store as chars
|
||||
cursor: usize,
|
||||
}
|
||||
|
||||
impl Line {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
chars: Vec::new(),
|
||||
cursor: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.cursor = 0;
|
||||
self.chars.clear();
|
||||
self.show_debug();
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> String {
|
||||
let s: String = self.chars.iter().collect();
|
||||
self.clear();
|
||||
self.show_debug();
|
||||
s
|
||||
}
|
||||
|
||||
pub fn show(&self) -> String {
|
||||
self.chars.iter().collect()
|
||||
}
|
||||
|
||||
pub fn back(&mut self, n: usize) -> bool {
|
||||
if self.cursor > 0 {
|
||||
if n > self.cursor {
|
||||
self.cursor = n;
|
||||
} else {
|
||||
self.cursor -= n;
|
||||
}
|
||||
self.show_debug();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) -> bool {
|
||||
if self.chars.len() > 0 {
|
||||
if self.cursor > 0 {
|
||||
self.cursor -= 1;
|
||||
}
|
||||
self.chars.remove(self.cursor);
|
||||
self.show_debug();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// removes the elements left of the cursor, returning the count of removed elements
|
||||
pub fn clear_left(&mut self) -> usize {
|
||||
if self.cursor > 0 {
|
||||
let n = self.chars.drain(..self.cursor).count();
|
||||
self.cursor = 0;
|
||||
self.show_debug();
|
||||
n
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// moves the cursor to the start of the edit line, returning the old cursor value
|
||||
pub fn seek_left(&mut self) -> usize {
|
||||
let n = self.cursor;
|
||||
self.cursor = 0;
|
||||
self.show_debug();
|
||||
n
|
||||
}
|
||||
|
||||
pub fn seek_right(&mut self) -> usize {
|
||||
let n = self.chars.len() - self.cursor;
|
||||
self.cursor = self.chars.len();
|
||||
self.show_debug();
|
||||
n
|
||||
}
|
||||
|
||||
pub fn forward(&mut self, n: usize) -> bool {
|
||||
if self.cursor < self.chars.len() {
|
||||
self.cursor = min(self.chars.len(), self.cursor + n);
|
||||
self.show_debug();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert(&mut self, c: char) {
|
||||
self.chars.insert(self.cursor, c);
|
||||
self.cursor += 1;
|
||||
self.show_debug();
|
||||
}
|
||||
|
||||
pub fn tail(&self) -> String {
|
||||
let mut start = self.cursor;
|
||||
if start > 0 {
|
||||
start -= 1;
|
||||
}
|
||||
|
||||
let chars = &self.chars[start..];
|
||||
chars.iter().collect()
|
||||
}
|
||||
|
||||
pub fn pos(&self) -> usize {
|
||||
self.cursor
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.chars.len()
|
||||
}
|
||||
|
||||
fn show_debug(&self) {
|
||||
let before = if self.chars.len() > 0 {
|
||||
self.chars[0..self.cursor].to_vec().iter().collect()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let after = if self.chars.len() > 0 {
|
||||
self.chars[self.cursor..].to_vec().iter().collect()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
debug!(
|
||||
" {: <5} {: >5} {}·{}",
|
||||
self.cursor,
|
||||
self.chars.len(),
|
||||
before,
|
||||
after
|
||||
);
|
||||
}
|
||||
}
|
@ -1,205 +1,52 @@
|
||||
/// builtin functions
|
||||
mod builtin;
|
||||
|
||||
/// all of the errors for the clyde project live in this module
|
||||
mod error;
|
||||
mod ext;
|
||||
|
||||
/// handles input from terminals
|
||||
mod input;
|
||||
|
||||
/// defines the interactive mode
|
||||
mod interactive;
|
||||
|
||||
/// key presses, independent from input sources
|
||||
mod key;
|
||||
mod line;
|
||||
|
||||
/// lexical analysis
|
||||
mod lex;
|
||||
|
||||
/// a real primitive line editor
|
||||
mod edit;
|
||||
|
||||
mod history;
|
||||
|
||||
/// logging configuration
|
||||
mod log;
|
||||
|
||||
/// stdout control, output more generally
|
||||
mod output;
|
||||
|
||||
/// turns our tokens into parse trees
|
||||
mod parse;
|
||||
mod prompt;
|
||||
mod shell;
|
||||
|
||||
use crate::log::*;
|
||||
use prompt::Prompt;
|
||||
use shell::Shell;
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let mut shell = Shell::new()?;
|
||||
let log_path = shell.expand_path("~/clyde.log");
|
||||
match Log::file(log_path) {
|
||||
Ok(f) => {
|
||||
let target = Box::leak(Box::new(f));
|
||||
_ = set_logger(target).map(|()| set_max_level(LevelFilter::Debug));
|
||||
}
|
||||
Err(e) => {
|
||||
println!("did not open log file: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let prompt = Prompt::new();
|
||||
|
||||
prompt.print(&mut shell.output)?;
|
||||
info!("» enter");
|
||||
loop {
|
||||
match shell.input.next()? {
|
||||
input::Event::Key(event) => {
|
||||
if event.down {
|
||||
if event.code.val == 0 {
|
||||
debug!(" {}", event);
|
||||
} else {
|
||||
warn!(" {}", event);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
info!(" {}", event);
|
||||
|
||||
if event.code == key::LEFT {
|
||||
shell.back(1)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if event.code == key::RIGHT {
|
||||
shell.forward(1)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if event.code == key::ENTER {
|
||||
shell.output.newline()?;
|
||||
let s = shell.line.pop();
|
||||
info!("◇ {}", s);
|
||||
let tree = parse::parse(&s)?;
|
||||
debug!(" {:?}", tree);
|
||||
shell.exec(tree.into())?;
|
||||
// Some commands don't leave the terminal in a clean state, so we use reset
|
||||
// to ensure that our input and output modes are what we expect them to be.
|
||||
shell.reset()?;
|
||||
prompt.print(&mut shell.output)?;
|
||||
continue;
|
||||
}
|
||||
|
||||
if event.code == key::TAB {
|
||||
continue;
|
||||
}
|
||||
|
||||
if event.code == key::BACKSPACE {
|
||||
if shell.line.backspace() {
|
||||
// move cursor back two spaces
|
||||
shell.output.back(2)?;
|
||||
let tail = format!("{} ", shell.line.tail());
|
||||
let n = tail.chars().count();
|
||||
shell.output.write(tail.as_bytes())?;
|
||||
|
||||
// after writing out the tail, rewind by the number of characters in
|
||||
// the tail
|
||||
if n > 1 {
|
||||
// let text = format!("\x1b[{}D", n - 1);
|
||||
shell.output.back(n - 1)?;
|
||||
// output.write(text.as_bytes())?;
|
||||
} else {
|
||||
// honestly I can't remember how I figured this out
|
||||
shell.output.write(b" \x1b[1D")?;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// CTRL-D to exit
|
||||
if event.ctrl && event.code == key::D {
|
||||
info!("» exit");
|
||||
shell.output.close()?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// CTRL-J to draw a cool little dot
|
||||
if event.ctrl && event.code == key::J {
|
||||
debug!("⎈ j: dot");
|
||||
// red bullet
|
||||
shell
|
||||
.output
|
||||
.write(String::from("\x1b[31m\u{2022}\x1b[0m").as_bytes())?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// CTRL-L to clear the screen
|
||||
if event.ctrl && event.code == key::L {
|
||||
info!("» clear");
|
||||
shell.output.clear()?;
|
||||
prompt.print(&mut shell.output)?;
|
||||
shell.output.write(shell.line.show().as_bytes())?;
|
||||
shell.output.back(shell.line.len() - shell.line.pos())?;
|
||||
shell.reset()?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// CTRL-U to erase to the beginning of the line
|
||||
if event.ctrl && event.code == key::U {
|
||||
info!("» clear left");
|
||||
let n = shell.line.clear_left();
|
||||
if n > 0 {
|
||||
// move left by the number of elements removed
|
||||
shell.output.back(n)?;
|
||||
// draw the elements remaining, followed by a space for each removed
|
||||
// element
|
||||
let kept = shell.line.show();
|
||||
let text = format!("{}{:width$}", kept, "", width = n);
|
||||
shell.output.write(text.as_bytes())?;
|
||||
shell.output.back(n + kept.chars().count())?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// CTRL-A to move to the beginning of the line
|
||||
if event.ctrl && event.code == key::A {
|
||||
shell.seek_left()?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// CTRL-E to move to the end of the line
|
||||
if event.ctrl && event.code == key::E {
|
||||
shell.seek_right()?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: something better here, this is crappy. I should be checking characters
|
||||
// based on their unicode categories, not this garbo
|
||||
if !event.char.is_control() {
|
||||
shell.line.insert(event.char);
|
||||
|
||||
let tail = shell.line.tail();
|
||||
let n = tail.chars().count();
|
||||
|
||||
// write everything from the current line cursor out to the output buffer.
|
||||
shell.output.write(tail.as_bytes())?;
|
||||
if n > 1 {
|
||||
// if we wrote more than one character, because we weren't at the end, we
|
||||
// need to rewind the terminal cursor to where it was.
|
||||
shell.output.back(n - 1)?;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
warn!("‽ {}", event);
|
||||
}
|
||||
input::Event::Left => shell.back(1)?,
|
||||
input::Event::Right => shell.forward(1)?,
|
||||
input::Event::Up => debug!("⛬ ↑"),
|
||||
input::Event::Down => debug!("⛬ ↓"),
|
||||
input::Event::Home => shell.seek_left()?,
|
||||
input::Event::End => shell.seek_right()?,
|
||||
input::Event::Focus(true) => {}
|
||||
input::Event::Focus(false) => {}
|
||||
input::Event::Menu(_command_id) => {}
|
||||
input::Event::Drop(seq) => debug!("? {}", seq),
|
||||
input::Event::Mouse { .. } => {}
|
||||
input::Event::Size => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
/// why is prompt a whole module what am i doing
|
||||
mod prompt;
|
||||
|
||||
> ls
|
||||
foo
|
||||
bar
|
||||
whatever
|
||||
/// the runtime state of the shell process
|
||||
mod run;
|
||||
|
||||
> ls
|
||||
/// syntax and semantic analysis
|
||||
mod syntax;
|
||||
|
||||
/// topoglyph is a real word, i promise
|
||||
mod topo;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let session = interactive::Session::new()?;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
session.enable_logging("~/clyde.log");
|
||||
|
||||
*/
|
||||
session.run()
|
||||
}
|
||||
|
@ -1,463 +1,352 @@
|
||||
use log::debug;
|
||||
use crate::error::ParseError;
|
||||
use crate::lex::{Lexer, Token};
|
||||
use std::{
|
||||
fmt,
|
||||
ops::Deref,
|
||||
cell::RefCell,
|
||||
rc::Rc,
|
||||
rc::{Rc, Weak},
|
||||
sync::atomic::AtomicUsize,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error("Unexpected Token")]
|
||||
UnexpectedToken,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// The contents of a node in our parse tree. The parse tree consists of both terminal and
|
||||
/// nonterminal symbols.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Token {
|
||||
Ident(String),
|
||||
Pipe,
|
||||
}
|
||||
pub enum Value {
|
||||
/// The start symbol of our parse tree. Each parse tree is rooted in a node whose value is the
|
||||
/// start symbol. This is the only node in the tree that should utilize the start symbol.
|
||||
Start,
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn lex<S: AsRef<str>>(text: S) -> Result<Vec<Token>, Error> {
|
||||
Lexer::new(text.as_ref()).lex()
|
||||
}
|
||||
/// The children of a statement symbol make up the components of what will become a statement
|
||||
/// in our ast
|
||||
Command,
|
||||
|
||||
struct Lexer<'text> {
|
||||
chars: std::str::Chars<'text>,
|
||||
peeked: Option<char>,
|
||||
/// Each of the tokens from the lex stage becomes a terminal node on our tree
|
||||
Terminal(Token),
|
||||
}
|
||||
|
||||
impl<'text> Lexer<'text> {
|
||||
fn new(text: &'text str) -> Self {
|
||||
Self {
|
||||
chars: text.chars(),
|
||||
peeked: None,
|
||||
}
|
||||
impl Value {
|
||||
fn is_terminal(&self) -> bool {
|
||||
matches!(self, Value::Terminal(_))
|
||||
}
|
||||
}
|
||||
|
||||
fn lex(&mut self) -> Result<Vec<Token>, Error> {
|
||||
let mut tokens = vec![];
|
||||
while self.peek().is_some() {
|
||||
self.skip_whitespace();
|
||||
match self.peek() {
|
||||
None => return Ok(tokens),
|
||||
Some('|') => {
|
||||
tokens.push(Token::Pipe);
|
||||
self.skip();
|
||||
}
|
||||
Some(_) => {
|
||||
tokens.push(self.ident());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(tokens)
|
||||
}
|
||||
/// A node in a parse tree.
|
||||
#[derive(Debug)]
|
||||
pub struct Node {
|
||||
pub id: usize,
|
||||
|
||||
fn skip_whitespace(&mut self) {
|
||||
loop {
|
||||
match self.peek() {
|
||||
None => break,
|
||||
Some(c) => {
|
||||
if c.is_whitespace() {
|
||||
self.skip()
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/// A node may or may not have a parent node. If a node does not have a parent node, that node
|
||||
/// is the root node of a tree.
|
||||
parent: Option<Weak<Node>>,
|
||||
|
||||
fn peek(&mut self) -> Option<char> {
|
||||
match self.peeked {
|
||||
Some(c) => Some(c),
|
||||
None => match self.chars.next() {
|
||||
Some(c) => {
|
||||
self.peeked = Some(c);
|
||||
Some(c)
|
||||
}
|
||||
None => None,
|
||||
},
|
||||
}
|
||||
}
|
||||
/// The value of the element at this node
|
||||
pub value: Value,
|
||||
|
||||
fn skip(&mut self) {
|
||||
if self.peeked.is_some() {
|
||||
self.peeked = None;
|
||||
} else {
|
||||
self.chars.next();
|
||||
}
|
||||
}
|
||||
/// A node may or may not have children. Since an empty vector is a valid vector, a node
|
||||
/// without children is represented as having an empty children vector. A node having an empty
|
||||
/// list of children is a leaf node in a tree.
|
||||
pub children: RefCell<Vec<Rc<Node>>>,
|
||||
}
|
||||
|
||||
fn ident(&mut self) -> Token {
|
||||
let mut kept: Vec<char> = vec![];
|
||||
loop {
|
||||
match self.peek() {
|
||||
None => break,
|
||||
Some(c) => {
|
||||
if c.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
kept.push(c);
|
||||
self.skip();
|
||||
}
|
||||
}
|
||||
impl Node {
|
||||
fn new() -> Cursor {
|
||||
let root = Rc::new(Node {
|
||||
id: next_id(),
|
||||
parent: None,
|
||||
value: Value::Start,
|
||||
children: RefCell::new(Vec::new()),
|
||||
});
|
||||
Cursor {
|
||||
target: Rc::clone(&root),
|
||||
prev: root.id,
|
||||
root,
|
||||
}
|
||||
Token::Ident(kept.iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
// struct Parser {
|
||||
// state: Tree,
|
||||
// }
|
||||
//
|
||||
// impl Parser {
|
||||
// fn new() -> Self {
|
||||
// Self {
|
||||
// state: Tree::new(),
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fn parse(mut self, tokens: Vec<Token>) -> Result<Node, Error> {
|
||||
// Ok(Node::empty())
|
||||
// // for token in tokens {
|
||||
// // self.take(token);
|
||||
// // }
|
||||
// // let t = self.state.replace(Node::empty());
|
||||
// // Ok(t.root())
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub enum Element {
|
||||
Empty,
|
||||
Command(String),
|
||||
Literal(String),
|
||||
pub fn is_semi(&self) -> bool {
|
||||
matches!(self.value, Value::Terminal(Token::Semi(_)))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Element {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
match self {
|
||||
Element::Empty => write!(f, "()"),
|
||||
Element::Command(cmd) => write!(f, "cmd<{}>", cmd),
|
||||
Element::Literal(lit) => write!(f, "lit<{}>", lit),
|
||||
}
|
||||
impl PartialEq for Node {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Node {
|
||||
pub elem: Element,
|
||||
pub parent: Option<Rc<RefCell<Node>>>,
|
||||
pub children: Vec<Rc<RefCell<Node>>>,
|
||||
/// Iterates through the children of a cursor node, producing new cursor positions.
|
||||
pub struct ChildIter {
|
||||
target: Rc<Node>,
|
||||
root: Rc<Node>,
|
||||
idx: usize,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
pub fn new(elem: Element) -> Self {
|
||||
Self {
|
||||
elem,
|
||||
parent: None,
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self::new(Element::Empty)
|
||||
}
|
||||
|
||||
pub fn child_of(parent: Rc<RefCell<Self>>, elem: Element) -> Self {
|
||||
Self {
|
||||
elem,
|
||||
parent: Some(parent),
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
impl Iterator for ChildIter {
|
||||
type Item = Cursor;
|
||||
|
||||
pub fn visit(self) -> Tree {
|
||||
self.into()
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let children = self.target.children.borrow();
|
||||
let v = children.get(self.idx)?;
|
||||
self.idx += 1;
|
||||
Some(Cursor {
|
||||
target: Rc::clone(v),
|
||||
root: Rc::clone(&self.root),
|
||||
prev: self.target.id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Node {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(f, "{:?}", self.elem)?;
|
||||
if self.children.len() > 0 {
|
||||
write!(f, "[")?;
|
||||
for child in self.children.iter() {
|
||||
write!(f, "{:?}", child.borrow())?;
|
||||
}
|
||||
write!(f, "]")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
const LAST_ID: AtomicUsize = AtomicUsize::new(0);
|
||||
fn next_id() -> usize {
|
||||
LAST_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub struct Tree {
|
||||
target: Rc<RefCell<Node>>,
|
||||
/// Cursor values expose access to a parse tree. A cursor is logically a pointer to a single node
|
||||
/// within a parse tree. The cursor helps with the ownership structure: so long as there is a
|
||||
/// cursor to any node on the tree, the tree remains in memory. Once there are no cursors pointed
|
||||
/// at the tree, it is dropped.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Cursor {
|
||||
pub target: Rc<Node>,
|
||||
root: Rc<Node>,
|
||||
prev: usize,
|
||||
}
|
||||
|
||||
impl Tree {
|
||||
fn new() -> Self {
|
||||
Node::empty().into()
|
||||
impl Cursor {
|
||||
/// Climbs one level up a parse tree. The cursor is re-pointed from its current target node to
|
||||
/// the parent of its current target node. This method fails if the cursor is already at the
|
||||
/// root node of the parse tree.
|
||||
pub fn up(&mut self) -> Result<(), ParseError> {
|
||||
match &self.target.parent {
|
||||
None => Err(ParseError::AtRootAlready),
|
||||
Some(parent) => match parent.upgrade() {
|
||||
Some(parent) => {
|
||||
self.goto(parent);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds an element as a node as a child of the current node, then descends the tree to select
|
||||
/// that child node. This is similar to how pushing a value changes the top element of a stack.
|
||||
fn push(self, elem: Element) -> Self {
|
||||
if self.is_empty() {
|
||||
self.target.replace(Node::new(elem));
|
||||
self
|
||||
} else {
|
||||
let child = Node::child_of(Rc::clone(&self.target), elem);
|
||||
Tree{target: Rc::new(RefCell::new(child))}
|
||||
None => Err(ParseError::ParentIsGone),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a child node with a given element value to the currently selected node without chaning
|
||||
/// our selection.
|
||||
fn append(self, elem: Element) -> Self {
|
||||
if self.is_empty() {
|
||||
self.target.replace(Node::new(elem));
|
||||
} else {
|
||||
let node = Node::child_of(Rc::clone(&self.target), elem);
|
||||
let child = Rc::new(RefCell::new(node));
|
||||
self.target.borrow_mut().children.push(child);
|
||||
/// Adds a value to the children of the current target node, then descends to select that
|
||||
/// child.
|
||||
fn push(&mut self, v: Value) -> Result<(), ParseError> {
|
||||
if self.target.value.is_terminal() {
|
||||
return Err(ParseError::PushOntoTerminal);
|
||||
}
|
||||
self
|
||||
let node = Node {
|
||||
id: next_id(),
|
||||
parent: Some(Rc::downgrade(&self.target)),
|
||||
value: v,
|
||||
children: RefCell::new(Vec::new()),
|
||||
};
|
||||
let node = Rc::new(node);
|
||||
self.target
|
||||
.children
|
||||
.try_borrow_mut()?
|
||||
.push(Rc::clone(&node));
|
||||
self.goto(node);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tells us whether or not the currently selected node is an empty node
|
||||
fn is_empty(&self) -> bool {
|
||||
self.target.borrow().elem == Element::Empty
|
||||
/// moves the cursor up to the root of the tree in place
|
||||
pub fn up_to_root(&mut self) {
|
||||
self.goto(Rc::clone(&self.root));
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<Self> {
|
||||
self.target.borrow().parent.as_ref().map(|parent| Self {
|
||||
target: Rc::clone(parent),
|
||||
})
|
||||
pub fn value(&self) -> &Value {
|
||||
&self.target.value
|
||||
}
|
||||
|
||||
fn is_root(&self) -> bool {
|
||||
self.parent().is_none()
|
||||
pub fn iter_children(&self) -> ChildIter {
|
||||
ChildIter {
|
||||
target: Rc::clone(&self.target),
|
||||
root: Rc::clone(&self.root),
|
||||
idx: 0,
|
||||
}
|
||||
|
||||
pub fn root(self) -> Self {
|
||||
match self.parent() {
|
||||
Some(parent) => parent.root(),
|
||||
None => self,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn is_root(&self) -> bool {
|
||||
self.target.parent.is_none()
|
||||
}
|
||||
|
||||
fn peek(&self) -> Element {
|
||||
self.target.borrow().elem.clone()
|
||||
#[cfg(test)]
|
||||
fn into_root(self) -> Rc<Node> {
|
||||
Rc::clone(&self.root)
|
||||
}
|
||||
|
||||
fn into_node(self) -> Node {
|
||||
self.into()
|
||||
fn text_tree(&self) -> String {
|
||||
let mut p = self.clone();
|
||||
p.up_to_root();
|
||||
let mut buf = String::new();
|
||||
self.text_tree_helper(0, &mut buf);
|
||||
buf
|
||||
}
|
||||
|
||||
fn children(&self) -> ChildIter {
|
||||
ChildIter {
|
||||
parent: Rc::clone(&self.target),
|
||||
idx: 0,
|
||||
fn text_tree_helper(&self, depth: u32, mut buf: &mut String) {
|
||||
let line = format!("{:?}\n", self.value());
|
||||
*buf = buf.to_owned() + &line;
|
||||
for child in self.iter_children() {
|
||||
child.text_tree_helper(depth + 1, &mut buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Tree {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(f, "{:?}", self.target.borrow())
|
||||
fn goto(&mut self, next: Rc<Node>) {
|
||||
self.prev = self.target.id;
|
||||
self.target = next;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChildIter {
|
||||
/// pointer to the node in the tree whose children we are looking at.
|
||||
parent: Rc<RefCell<Node>>,
|
||||
idx: usize,
|
||||
/// The Parser is responsible for turning a sequence of Tokens into a Parse Tree.
|
||||
pub struct Parser<'text> {
|
||||
input: Lexer<'text>,
|
||||
output: Cursor,
|
||||
}
|
||||
|
||||
impl Iterator for ChildIter {
|
||||
type Item = Tree;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.idx >= self.parent.borrow().children.len() {
|
||||
None
|
||||
} else {
|
||||
let child = Rc::clone(&self.parent.borrow().children[self.idx]);
|
||||
self.idx += 1;
|
||||
Some(Tree{target: child})
|
||||
impl<'text> Parser<'text> {
|
||||
pub fn new(source: Lexer<'text>) -> Self {
|
||||
Self {
|
||||
input: source,
|
||||
output: Node::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Tree> for Node {
|
||||
fn from(value: Tree) -> Self {
|
||||
value.target.replace(Node::empty())
|
||||
pub fn parse(mut self) -> Result<Cursor, ParseError> {
|
||||
while self.step()? {}
|
||||
self.output.up_to_root();
|
||||
Ok(self.output)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Node> for Tree {
|
||||
fn from(value: Node) -> Self {
|
||||
Self {
|
||||
target: Rc::new(RefCell::new(value)),
|
||||
fn step(&mut self) -> Result<bool, ParseError> {
|
||||
match self.output.value() {
|
||||
Value::Start => self.step_start(),
|
||||
Value::Command => self.step_statement(),
|
||||
Value::Terminal(_) => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// I don't know how to write a parser lol
|
||||
|
||||
// ls
|
||||
// Command{name: "ls"}
|
||||
|
||||
// echo one two three
|
||||
// Command{name: "echo", args: [Lit("one"), Lit("two"), Lit("three")]}
|
||||
|
||||
// echo *.rs
|
||||
// Command{name: "echo", args: [Glob("*.rs")]}
|
||||
// Command{name: "echo", args: [Path("main.rs"), Path("parse.rs"), ...]}
|
||||
|
||||
// cat main.rs | wc -l
|
||||
// Pipe{
|
||||
// src: Command{name: "cat", args: [Lit("main.rs")]}
|
||||
// dest: Command{name: "wc", args: [Lit("-l")]}
|
||||
// }
|
||||
|
||||
// echo one ; echo two
|
||||
// Sequence{
|
||||
// children: [
|
||||
// Command{Name:
|
||||
// ]
|
||||
// }
|
||||
|
||||
pub fn parse<S: AsRef<str>>(text: S) -> Result<Tree, Error> {
|
||||
let mut tree = Tree::new();
|
||||
for token in lex(text)? {
|
||||
match (tree.peek(), token) {
|
||||
(Element::Empty, Token::Ident(cmd)) => {
|
||||
tree = tree.push(Element::Command(cmd));
|
||||
fn step_start(&mut self) -> Result<bool, ParseError> {
|
||||
assert!(matches!(self.output.value(), Value::Start));
|
||||
match self.input.peek()? {
|
||||
Some(Token::Word(_)) => {
|
||||
self.output.push(Value::Command)?;
|
||||
let token = self.input.next().unwrap()?;
|
||||
self.output.push(Value::Terminal(token))?;
|
||||
self.output.up()?;
|
||||
Ok(true)
|
||||
}
|
||||
(Element::Command(_), Token::Ident(arg)) => {
|
||||
tree = tree.append(Element::Literal(arg));
|
||||
Some(Token::Glob(_)) => {
|
||||
let token = self.input.next().unwrap()?;
|
||||
Err(ParseError::UnexpectedToken(token))
|
||||
}
|
||||
_ => todo!(),
|
||||
Some(Token::Semi(_)) => {
|
||||
self.output.push(Value::Command)?;
|
||||
let token = self.input.next().unwrap()?;
|
||||
self.output.push(Value::Terminal(token))?;
|
||||
self.output.up()?;
|
||||
self.output.up()?;
|
||||
Ok(true)
|
||||
}
|
||||
Some(Token::Pipe(_)) => {
|
||||
let token = self.input.next().unwrap()?;
|
||||
Err(ParseError::UnexpectedToken(token))
|
||||
}
|
||||
None => Ok(false),
|
||||
}
|
||||
Ok(tree.root())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use Token::Pipe;
|
||||
|
||||
fn ident<S: Into<String>>(s: S) -> Token {
|
||||
Token::Ident(s.into())
|
||||
}
|
||||
|
||||
macro_rules! lex {
|
||||
(
|
||||
$($name:ident: $line:literal $($token:expr)* ;)+
|
||||
) => {
|
||||
$(
|
||||
#[test]
|
||||
fn $name() -> Result<(), Error> {
|
||||
let mut tokens = lex($line)?;
|
||||
|
||||
$(
|
||||
assert!(tokens.len() > 0);
|
||||
let front = tokens.remove(0);
|
||||
assert_eq!($token, front);
|
||||
)*
|
||||
|
||||
Ok(())
|
||||
fn step_statement(&mut self) -> Result<bool, ParseError> {
|
||||
assert!(matches!(self.output.value(), Value::Command));
|
||||
match self.input.peek()? {
|
||||
Some(Token::Word(_) | Token::Glob(_)) => {
|
||||
let token = self.input.next().unwrap()?;
|
||||
self.output.push(Value::Terminal(token))?;
|
||||
self.output.up()?;
|
||||
Ok(true)
|
||||
}
|
||||
)*
|
||||
};
|
||||
Some(Token::Pipe(_)) => {
|
||||
todo!()
|
||||
}
|
||||
Some(Token::Semi(_)) => {
|
||||
let token = self.input.next().unwrap()?;
|
||||
self.output.push(Value::Terminal(token))?;
|
||||
self.output.up()?;
|
||||
self.output.up()?;
|
||||
Ok(true)
|
||||
}
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lex! {
|
||||
single_token:
|
||||
"one"
|
||||
ident("one");
|
||||
|
||||
two_tokens:
|
||||
"ls one two"
|
||||
ident("ls") ident("one") ident("two");
|
||||
|
||||
leading_whitespace:
|
||||
" one"
|
||||
ident("one");
|
||||
|
||||
trailing_whitespace:
|
||||
"one "
|
||||
ident("one");
|
||||
/*
|
||||
|
||||
surrounding_whitespace:
|
||||
" one "
|
||||
ident("one");
|
||||
a b
|
||||
command
|
||||
terminal
|
||||
a
|
||||
terminal
|
||||
b
|
||||
|
||||
internal_hyphen:
|
||||
"one-two"
|
||||
ident("one-two");
|
||||
a | b
|
||||
pipeline
|
||||
command
|
||||
terminal
|
||||
a
|
||||
terminal
|
||||
|
|
||||
command
|
||||
terminal
|
||||
b
|
||||
|
||||
pipe:
|
||||
"|"
|
||||
Pipe;
|
||||
|
||||
pipeline:
|
||||
"one | two"
|
||||
ident("one") Pipe ident("two");
|
||||
*/
|
||||
|
||||
pipeline_2:
|
||||
"one |two"
|
||||
ident("one") Pipe ident("two");
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::lex::lex;
|
||||
|
||||
#[test]
|
||||
fn empty_tree() {
|
||||
let root = Tree::new().root();
|
||||
assert!(root.is_root());
|
||||
assert_eq!(root.peek(), Element::Empty);
|
||||
assert_eq!(root.children().count(), 0);
|
||||
fn parse(source: &str) -> Result<Cursor, ParseError> {
|
||||
let tokens = Lexer::new(source);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.parse()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_root() {
|
||||
let root = Tree::new()
|
||||
.push(Element::Command(String::from("ls")))
|
||||
.root();
|
||||
assert!(root.is_root());
|
||||
assert_eq!(root.children().count(), 0);
|
||||
assert_eq!(root.into_node().elem, Element::Command(String::from("ls")));
|
||||
fn root() {
|
||||
let mut cursor = Node::new();
|
||||
assert!(cursor.up().is_err());
|
||||
assert!(cursor.target.value == Value::Start);
|
||||
assert!(cursor.is_root());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tree_push() {
|
||||
let tree = Tree::new()
|
||||
.push(Element::Command(String::from("ls")));
|
||||
assert_eq!(tree.peek(), Element::Command(String::from("ls")));
|
||||
|
||||
let tree = Tree::new()
|
||||
.push(Element::Command(String::from("ls")))
|
||||
.append(Element::Command(String::from("one")));
|
||||
assert_eq!(tree.peek(), Element::Command(String::from("ls")));
|
||||
fn single_val() {
|
||||
let mut cursor = Node::new();
|
||||
|
||||
let mut tokens = lex(" ls ").unwrap();
|
||||
let ls = tokens.pop().unwrap();
|
||||
assert!(cursor.push(Value::Command).is_ok());
|
||||
assert!(cursor.push(Value::Terminal(ls.clone())).is_ok());
|
||||
assert!(cursor.push(Value::Terminal(ls.clone())).is_err());
|
||||
assert!(cursor.target.value == Value::Terminal(ls));
|
||||
assert!(!cursor.is_root());
|
||||
assert!(cursor.up().is_ok());
|
||||
assert!(cursor.up().is_ok());
|
||||
assert!(cursor.is_root());
|
||||
assert!(cursor.up().is_err());
|
||||
assert!(cursor.value() == &Value::Start);
|
||||
let root = cursor.into_root();
|
||||
assert!(root.value == Value::Start);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_args() {
|
||||
let res = parse("ls one two three");
|
||||
assert!(res.is_ok());
|
||||
|
||||
let tree = res.unwrap();
|
||||
assert_eq!(tree.peek(), Element::Command(String::from("ls")));
|
||||
assert_eq!(tree.children().count(), 3);
|
||||
|
||||
let mut args = tree.children();
|
||||
assert_eq!(args.next().unwrap().peek(), Element::Literal(String::from("one")));
|
||||
assert_eq!(args.next().unwrap().peek(), Element::Literal(String::from("two")));
|
||||
assert_eq!(args.next().unwrap().peek(), Element::Literal(String::from("three")));
|
||||
assert!(args.next().is_none());
|
||||
fn test_parse() -> Result<(), ParseError> {
|
||||
let x = parse("ls one two three")?;
|
||||
println!("Parse tree: {x:?}");
|
||||
// parse("*")?;
|
||||
// parse("x* ls")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,78 @@
|
||||
use crate::{builtin, error::ExecError, output};
|
||||
use std::{collections::HashMap, process};
|
||||
|
||||
/// Eval represents anything that can be evaluated at runtime.
|
||||
pub trait Eval {
|
||||
/// Evaluates the receiver, given the current runtime state.
|
||||
fn eval(&self, ctx: &mut Context) -> Result<Value, ExecError>;
|
||||
}
|
||||
|
||||
/// An individual value at runtime.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Value {
|
||||
/// The empty value. This is somewhat analagous to () in Rust or void in C or null in languages
|
||||
/// that have null.
|
||||
None,
|
||||
|
||||
/// A textual value.
|
||||
Text(String),
|
||||
|
||||
/// The result of having executed some external OS process.
|
||||
ExitStatus(process::ExitStatus),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn try_to_string(self) -> Result<String, ExecError> {
|
||||
Ok(self.try_as_str()?.to_string())
|
||||
}
|
||||
|
||||
pub fn try_as_str(&self) -> Result<&str, ExecError> {
|
||||
match self {
|
||||
Value::None => Err(ExecError::type_error("expected text value, saw None value")),
|
||||
Value::Text(v) => Ok(&v),
|
||||
Value::ExitStatus(_) => Err(ExecError::type_error(
|
||||
"expected text value, saw ExitStatus value",
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Value {
|
||||
fn eval(&self, _: &mut Context) -> Result<Value, ExecError> {
|
||||
Ok(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// The state of the runtime. In an environment-passing interpreter, this would be the environment.
|
||||
/// We avoid the term "environment" because in the context of an OS shell it's confusing whether it
|
||||
/// means the state of the interpreter itself, or the environment variables of the process. This
|
||||
/// State type represents the state of the interpreter itself.
|
||||
pub struct State {
|
||||
#[allow(unused)]
|
||||
variables: HashMap<&'static str, Value>,
|
||||
|
||||
pub(crate) builtins: HashMap<&'static str, builtin::Builtin>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
variables: HashMap::new(),
|
||||
builtins: builtin::all(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn builtin(&self, name: &str) -> Option<builtin::Builtin> {
|
||||
self.builtins.get(name).copied()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Call {
|
||||
fn call(&self, ctx: &mut Context, args: &[Value]) -> Result<Value, ExecError>;
|
||||
}
|
||||
|
||||
pub struct Context<'s> {
|
||||
pub(crate) stdout: output::Writer,
|
||||
pub(crate) stderr: output::Writer,
|
||||
pub(crate) state: &'s mut State,
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
use crate::{
|
||||
ext::{Command, Echo, Printenv, Tail, Which},
|
||||
input,
|
||||
line::Line,
|
||||
log::*,
|
||||
output,
|
||||
// parse::parse,
|
||||
parse::{Element, Node},
|
||||
};
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::rc::Rc;
|
||||
|
||||
use anyhow::Result;
|
||||
use dirs;
|
||||
|
||||
pub struct Shell {
|
||||
pub input: input::Reader,
|
||||
pub output: output::Writer,
|
||||
pub line: Line,
|
||||
}
|
||||
|
||||
impl Shell {
|
||||
pub fn new() -> Result<Self> {
|
||||
Ok(Self {
|
||||
input: input::Reader::new()?,
|
||||
output: output::Writer::stdout()?,
|
||||
line: Line::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn back(&mut self, n: usize) -> Result<()> {
|
||||
debug!("⛬ ←");
|
||||
if self.line.back(n) {
|
||||
self.output.back(n)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn forward(&mut self, n: usize) -> Result<()> {
|
||||
debug!("⛬ →");
|
||||
if self.line.forward(n) {
|
||||
self.output.forward(n)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) -> Result<()> {
|
||||
self.input.reset()?;
|
||||
self.output.reset()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn seek_right(&mut self) -> Result<()> {
|
||||
info!("» seek right");
|
||||
let n = self.line.seek_right();
|
||||
if n > 0 {
|
||||
// move right by the distance seeked
|
||||
self.output.forward(n)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn seek_left(&mut self) -> Result<()> {
|
||||
info!("» seek left");
|
||||
let n = self.line.seek_left();
|
||||
if n > 0 {
|
||||
// move left by the distance seeked
|
||||
self.output.back(n)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn expand_path<P: AsRef<Path>>(&self, p: P) -> PathBuf {
|
||||
let p = p.as_ref();
|
||||
|
||||
match p.to_str() {
|
||||
Some(s) => {
|
||||
if s.len() == 0 || !s.starts_with('~') {
|
||||
return p.to_path_buf();
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return p.to_path_buf();
|
||||
}
|
||||
}
|
||||
|
||||
let mut parts = p.components();
|
||||
let first = parts.next().unwrap().as_os_str().to_str().unwrap();
|
||||
if first == "~" {
|
||||
dirs::home_dir().unwrap().join(parts)
|
||||
} else {
|
||||
let my_home = dirs::home_dir().unwrap();
|
||||
let home_root = my_home.parent().unwrap();
|
||||
let first = &first[1..];
|
||||
let other_home = home_root.join(first);
|
||||
other_home.join(parts)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn exec(&mut self, root: Node) -> Result<bool> {
|
||||
match root.elem {
|
||||
Element::Empty => Ok(true),
|
||||
Element::Command(cmd) => {
|
||||
let args: Vec<String> = root
|
||||
.children
|
||||
.iter()
|
||||
.map(|node| match &node.borrow().elem {
|
||||
Element::Literal(v) => v.clone(),
|
||||
_ => todo!(),
|
||||
})
|
||||
.collect();
|
||||
self.eval(
|
||||
cmd.to_string(),
|
||||
args.iter().map(|arg| arg.as_str()).collect(),
|
||||
)
|
||||
}
|
||||
Element::Literal(_) => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval(&mut self, cmd: String, args: Vec<&str>) -> Result<bool> {
|
||||
match cmd.as_str() {
|
||||
"pwd" => {
|
||||
let pb = std::env::current_dir()?;
|
||||
println!("{}", pb.as_path().as_os_str().to_str().unwrap());
|
||||
return Ok(true);
|
||||
}
|
||||
"cd" => {
|
||||
let cwd = std::env::current_dir()?;
|
||||
if args.len() > 0 {
|
||||
let target = cwd.join(args[0]);
|
||||
std::env::set_current_dir(target)?;
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
"printenv" => Printenv::create().exec(args),
|
||||
"which" => Which::create().exec(args),
|
||||
"tail" => Tail::create().exec(args),
|
||||
"echo" => Echo::create().exec(args),
|
||||
_ => {
|
||||
let mut proc = std::process::Command::new(cmd);
|
||||
if args.len() > 0 {
|
||||
proc.args(args);
|
||||
}
|
||||
match proc.spawn() {
|
||||
Ok(mut child) => {
|
||||
if let Err(e) = child.wait() {
|
||||
println!("error: {}", e);
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("error: {}", e);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,252 @@
|
||||
use crate::{
|
||||
builtin::Builtin,
|
||||
error::{ExecError, ParseError},
|
||||
lex::{Lexer, Token},
|
||||
parse,
|
||||
run::{Call, Context, Eval, Value},
|
||||
};
|
||||
use std::{collections::HashSet, env, ffi::OsString, process};
|
||||
|
||||
/// The differnt types of nodes that may appear in our AST
|
||||
#[derive(Debug)]
|
||||
pub enum Element {
|
||||
/// A Block is a list of statements
|
||||
Block(Block),
|
||||
|
||||
/// A Command represents the desire to execute a command
|
||||
Command(Command),
|
||||
|
||||
/// A literal is a ... literal value
|
||||
Literal(Value),
|
||||
}
|
||||
|
||||
impl Eval for Element {
|
||||
fn eval(&self, ctx: &mut Context) -> Result<Value, ExecError> {
|
||||
use Element::*;
|
||||
match self {
|
||||
Block(block) => block.eval(ctx),
|
||||
Command(cmd) => cmd.eval(ctx),
|
||||
Literal(v) => v.eval(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Block {
|
||||
commands: Vec<Command>,
|
||||
}
|
||||
|
||||
impl Block {
|
||||
pub fn new() -> Self {
|
||||
Block {
|
||||
commands: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Block {
|
||||
fn eval(&self, ctx: &mut Context) -> Result<Value, ExecError> {
|
||||
let mut v = Value::None;
|
||||
for cmd in self.commands.iter() {
|
||||
v = cmd.eval(ctx)?;
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Command {
|
||||
name: Box<Element>,
|
||||
args: Vec<Element>,
|
||||
}
|
||||
|
||||
impl Command {
|
||||
fn exec_builtin(&self, ctx: &mut Context, builtin: Builtin) -> Result<Value, ExecError> {
|
||||
let args = self
|
||||
.args
|
||||
.iter()
|
||||
.map(|elem| elem.eval(ctx))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
builtin.call(ctx, &args)
|
||||
}
|
||||
|
||||
fn exec_subprocess(&self, ctx: &mut Context, name: &str) -> Result<Value, ExecError> {
|
||||
let args = self
|
||||
.args
|
||||
.iter()
|
||||
.map(|elem| elem.eval(ctx)?.try_to_string())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
let mut proc = process::Command::new(&name);
|
||||
if args.len() > 0 {
|
||||
proc.args(args);
|
||||
}
|
||||
|
||||
let mut child = proc.spawn().map_err(|e| ExecError::ProcessSpawnError {
|
||||
name: name.to_string(),
|
||||
source: e,
|
||||
})?;
|
||||
let pid = child.id();
|
||||
let status = child.wait().map_err(|e| ExecError::ProcessWaitError {
|
||||
name: name.to_string(),
|
||||
pid,
|
||||
source: e,
|
||||
})?;
|
||||
Ok(Value::ExitStatus(status))
|
||||
}
|
||||
}
|
||||
|
||||
impl Eval for Command {
|
||||
fn eval(&self, ctx: &mut Context) -> Result<Value, ExecError> {
|
||||
let name = self.name.eval(ctx)?.try_to_string()?;
|
||||
match ctx.state.builtin(&name) {
|
||||
Some(builtin) => self.exec_builtin(ctx, builtin),
|
||||
None => self.exec_subprocess(ctx, &name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// the TreeBuilder is responsible for converting a parse tree into an abstract syntax tree. This
|
||||
/// process leaves us with a tree that can be executed directly
|
||||
struct TreeBuilder {
|
||||
visited: HashSet<usize>,
|
||||
}
|
||||
|
||||
impl TreeBuilder {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
visited: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(&mut self, source: &mut parse::Cursor) -> Result<Element, ParseError> {
|
||||
let e = match source.value() {
|
||||
parse::Value::Start => {
|
||||
let mut root = Block::new();
|
||||
let children = source.iter_children();
|
||||
for mut child in children {
|
||||
if child.target.is_semi() {
|
||||
continue;
|
||||
}
|
||||
let e = self.parse(&mut child)?;
|
||||
match e {
|
||||
Element::Command(cmd) => root.commands.push(cmd),
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
Element::Block(root)
|
||||
}
|
||||
parse::Value::Command => {
|
||||
let mut children = source.iter_children();
|
||||
let mut first = match children.next() {
|
||||
Some(child) => child,
|
||||
None => return Err(ParseError::StatementIsEmpty),
|
||||
};
|
||||
if first.target.is_semi() {
|
||||
return Err(ParseError::StatementIsEmpty);
|
||||
}
|
||||
let name = self.parse(&mut first)?;
|
||||
let mut cmd = Command {
|
||||
name: Box::new(name),
|
||||
args: Vec::new(),
|
||||
};
|
||||
|
||||
// we set complete to true when we find a semicolon. If there are any parse nodes
|
||||
// that appear -after- a semicolon, that indicates a bug in the prior stage.
|
||||
let mut complete = false;
|
||||
for mut child in children {
|
||||
if child.target.is_semi() {
|
||||
complete = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if complete {
|
||||
return Err(ParseError::DanglingElements);
|
||||
} else {
|
||||
let e = self.parse(&mut child)?;
|
||||
cmd.args.push(e);
|
||||
}
|
||||
}
|
||||
Element::Command(cmd)
|
||||
}
|
||||
parse::Value::Terminal(Token::Word(word)) => {
|
||||
let text = word.to_string();
|
||||
let text = expand(&text)?;
|
||||
Element::Literal(Value::Text(text))
|
||||
}
|
||||
parse::Value::Terminal(Token::Glob(_)) => {
|
||||
todo!()
|
||||
}
|
||||
parse::Value::Terminal(Token::Pipe(_)) => {
|
||||
todo!()
|
||||
}
|
||||
parse::Value::Terminal(Token::Semi(_)) => {
|
||||
return Err(ParseError::WhyParseSemicolon);
|
||||
}
|
||||
};
|
||||
self.visited.insert(source.target.id);
|
||||
Ok(e)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(source: &str) -> Result<Element, ParseError> {
|
||||
let tokens = Lexer::new(source);
|
||||
let parser = parse::Parser::new(tokens);
|
||||
let mut parse_tree = parser.parse()?;
|
||||
let mut builder = TreeBuilder::new();
|
||||
let root = builder.parse(&mut parse_tree)?;
|
||||
Ok(root)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn home_dir() -> Result<OsString, ExpandError> {
|
||||
env::var_os("HOMEPATH").ok_or(ExpandError::EmptyEnvironmentVar(String::from("HOMEPATH")))
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ExpandError {
|
||||
#[error("environment var is not set: {0}")]
|
||||
EmptyEnvironmentVar(String),
|
||||
|
||||
#[error("home dir is not utf-8")]
|
||||
NotUnicode,
|
||||
}
|
||||
|
||||
fn expand(text: &str) -> Result<String, ExpandError> {
|
||||
let chars = text.chars();
|
||||
// let mut escape = false;
|
||||
let mut s = String::new();
|
||||
for c in chars {
|
||||
// if !escape && c == '\\' {
|
||||
// escape = true;
|
||||
// continue;
|
||||
// }
|
||||
|
||||
// if escape {
|
||||
// s.push(c);
|
||||
// escape = false;
|
||||
// continue;
|
||||
// }
|
||||
|
||||
if c == '~' {
|
||||
let p = home_dir()?;
|
||||
s.push_str(p.to_str().ok_or(ExpandError::NotUnicode)?);
|
||||
continue;
|
||||
}
|
||||
|
||||
s.push(c);
|
||||
}
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hi() -> Result<(), ParseError> {
|
||||
let e = parse("ls one two three")?;
|
||||
print!("{:?}", e);
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -0,0 +1,207 @@
|
||||
use crate::error::LexError;
|
||||
use std::{collections::VecDeque, fmt, ops::Range, str::Chars};
|
||||
|
||||
/// A position a some two dimensional space defined by lines and columns, specifically concerned
|
||||
/// with positions of characters within text
|
||||
#[derive(PartialEq, Clone, Copy)]
|
||||
pub struct Position {
|
||||
/// The visual line in which this glyph appears in the source text
|
||||
pub line: u64,
|
||||
|
||||
/// The visual column in which this glyph appears in the source text
|
||||
pub column: u64,
|
||||
}
|
||||
|
||||
impl Position {
|
||||
/// The 0,0 position: the very first character of a document is at the 0,0 position
|
||||
pub fn origin() -> Self {
|
||||
Self { line: 0, column: 0 }
|
||||
}
|
||||
|
||||
/// Increments position by column, going from the current line,column position to the next
|
||||
/// column on the same line.
|
||||
pub fn incr(&mut self) -> Position {
|
||||
let p = *self;
|
||||
self.column += 1;
|
||||
p
|
||||
}
|
||||
|
||||
/// Increments the position by line, going from the current line,column position to the
|
||||
/// beginning of the next line.
|
||||
pub fn incr_line(&mut self) -> Position {
|
||||
let p = *self;
|
||||
self.column = 0;
|
||||
self.line += 1;
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Position {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(
|
||||
f,
|
||||
"({line}, {column})",
|
||||
line = self.line,
|
||||
column = self.column
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Position {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
(self as &dyn fmt::Debug).fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
/// A Glyph represents a character occurring at some specific position in some input text.
|
||||
#[derive(PartialEq, Clone)]
|
||||
pub struct Glyph {
|
||||
/// the unicode code point of the glyph
|
||||
pub glyph: char,
|
||||
|
||||
/// The visual position in which the glyph appears; i.e., the human-comprehensible location
|
||||
/// of the glyph in the source text
|
||||
pub position: Position,
|
||||
|
||||
/// The byte offsets corresponding to this topoglyph in the source data; i.e., the
|
||||
/// machine-comprehensible location of the glyph in the source text
|
||||
pub bytes: Range<u64>,
|
||||
}
|
||||
|
||||
impl Glyph {
|
||||
/// checks to see whether or not the character for this glyph is a word character
|
||||
pub fn is_word(&self) -> bool {
|
||||
match self.glyph {
|
||||
'-' | '/' | '\\' | '.' | ':' | '~' => true,
|
||||
_ => self.glyph.is_alphanumeric(),
|
||||
}
|
||||
}
|
||||
|
||||
/// checks to see whether or not the character for this glyph is a glob character
|
||||
pub fn is_glob(&self) -> bool {
|
||||
self.is_word() || self.glyph == '*'
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Glyph {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||
write!(
|
||||
f,
|
||||
"[{char} ({pos:?})]",
|
||||
char = self.glyph,
|
||||
pos = self.position
|
||||
)
|
||||
}
|
||||
}
|
||||
/// Glyphs produces [glyphs](Glyph) for a source text; i.e., it is an iterator of [Glyph] values.
|
||||
/// Glyphs is used to control reading from the source text and keeps a lookahead buffer of glyphs
|
||||
/// that have not been processed. While a [crate::lex::Lexer] is responsible for the creation and
|
||||
/// iteration of [tokens](crate::lex::Token), Glyphs is responsible for the creation and iteration
|
||||
/// of glyphs.
|
||||
pub struct Glyphs<'text> {
|
||||
/// the characters of the input text that we're interested in reading
|
||||
source: Chars<'text>,
|
||||
|
||||
/// the position of the next character in the source text
|
||||
pub(crate) next_position: Position,
|
||||
|
||||
/// the number of bytes we've read from our input document so far
|
||||
bytes_read: u64,
|
||||
|
||||
/// lookahead buffer
|
||||
lookahead: VecDeque<Glyph>,
|
||||
}
|
||||
|
||||
impl<'text> Glyphs<'text> {
|
||||
pub fn new(source: &'text str) -> Self {
|
||||
// neat
|
||||
Self {
|
||||
source: source.chars(),
|
||||
next_position: Position::origin(),
|
||||
bytes_read: 0,
|
||||
lookahead: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// reads the next n characters from the source text into our lookahead buffer
|
||||
fn fill_lookahead(&mut self, n: usize) -> bool {
|
||||
while self.lookahead.len() < n {
|
||||
let c = match self.source.next() {
|
||||
Some(c) => c,
|
||||
None => break,
|
||||
};
|
||||
|
||||
let len = c.len_utf8();
|
||||
let start = self.bytes_read;
|
||||
self.bytes_read += len as u64;
|
||||
let position = if c == '\n' {
|
||||
self.next_position.incr_line()
|
||||
} else {
|
||||
self.next_position.incr()
|
||||
};
|
||||
self.lookahead.push_back(Glyph {
|
||||
glyph: c,
|
||||
position,
|
||||
bytes: Range {
|
||||
start,
|
||||
end: self.bytes_read,
|
||||
},
|
||||
})
|
||||
}
|
||||
self.lookahead.len() == n
|
||||
}
|
||||
|
||||
/// returns a reference to the next character from the source text, advancing our internal
|
||||
/// lookahead buffer if necessary. Returns None if we're already at the end of our source text.
|
||||
pub fn peek(&mut self) -> Option<&Glyph> {
|
||||
self.peek_at(0)
|
||||
}
|
||||
|
||||
/// takes the next character from our input text
|
||||
pub fn pop(&mut self) -> Result<Glyph, LexError> {
|
||||
self.next()
|
||||
.ok_or(LexError::UnexpectedEOF(self.next_position))
|
||||
}
|
||||
|
||||
/// returns a reference to a character in our lookahead buffer at a given position. This allows
|
||||
/// us to perform a lookahead read without consuming any tokens, maintaining our current
|
||||
/// position and keeping our unconsumed characters safe.
|
||||
fn peek_at(&mut self, idx: usize) -> Option<&Glyph> {
|
||||
self.fill_lookahead(idx + 1);
|
||||
self.lookahead.get(idx)
|
||||
}
|
||||
|
||||
/// discards characters from our current position so long as the upcoming characters match some
|
||||
/// predicate. This is called yeet_while instead of skip_while in order to avoid conflicting
|
||||
/// with the
|
||||
/// [skip_while](https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.skip_while)
|
||||
/// method of the stdlib Iterator trait.
|
||||
pub fn yeet_while<F>(&mut self, mut pred: F)
|
||||
where
|
||||
F: FnMut(&Glyph) -> bool,
|
||||
{
|
||||
while let Some(g) = self.peek() {
|
||||
if pred(&g) {
|
||||
self.next();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// discards all of the upcoming whitespace characters. After calling yeet_whitespace, the next
|
||||
/// Glyp is guaranteed to be either non-whitespace or None (None would be because we've hit
|
||||
/// EOF)
|
||||
pub fn yeet_whitespace(&mut self) {
|
||||
self.yeet_while(|tg| tg.glyph.is_whitespace());
|
||||
}
|
||||
}
|
||||
|
||||
impl<'text> Iterator for Glyphs<'text> {
|
||||
type Item = Glyph;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.fill_lookahead(1);
|
||||
self.lookahead.pop_front()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue