Compare commits
2 Commits
main
...
parse-tree
Author | SHA1 | Date |
---|---|---|
Jordan Orelli | 0befb4e48a | 2 years ago |
Jordan Orelli | 7abb60a3bc | 2 years ago |
@ -1,44 +0,0 @@
|
|||||||
# 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
|
|
||||||
```
|
|
@ -1,82 +0,0 @@
|
|||||||
/// 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),
|
|
||||||
])
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,24 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,26 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,474 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,66 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
*/
|
@ -0,0 +1,7 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
pub trait Command {
|
||||||
|
fn name() -> String;
|
||||||
|
fn create() -> Self;
|
||||||
|
fn exec(&mut self, args: Vec<&str>) -> Result<bool>;
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,45 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,128 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,380 +0,0 @@
|
|||||||
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") ]
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,146 @@
|
|||||||
|
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,52 +1,205 @@
|
|||||||
/// builtin functions
|
|
||||||
mod builtin;
|
|
||||||
|
|
||||||
/// all of the errors for the clyde project live in this module
|
|
||||||
mod error;
|
mod error;
|
||||||
|
mod ext;
|
||||||
/// handles input from terminals
|
|
||||||
mod input;
|
mod input;
|
||||||
|
|
||||||
/// defines the interactive mode
|
|
||||||
mod interactive;
|
|
||||||
|
|
||||||
/// key presses, independent from input sources
|
|
||||||
mod key;
|
mod key;
|
||||||
|
mod line;
|
||||||
/// lexical analysis
|
|
||||||
mod lex;
|
|
||||||
|
|
||||||
/// a real primitive line editor
|
|
||||||
mod edit;
|
|
||||||
|
|
||||||
mod history;
|
|
||||||
|
|
||||||
/// logging configuration
|
|
||||||
mod log;
|
mod log;
|
||||||
|
|
||||||
/// stdout control, output more generally
|
|
||||||
mod output;
|
mod output;
|
||||||
|
|
||||||
/// turns our tokens into parse trees
|
|
||||||
mod parse;
|
mod parse;
|
||||||
|
|
||||||
/// why is prompt a whole module what am i doing
|
|
||||||
mod prompt;
|
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 => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// the runtime state of the shell process
|
/*
|
||||||
mod run;
|
|
||||||
|
|
||||||
/// syntax and semantic analysis
|
> ls
|
||||||
mod syntax;
|
foo
|
||||||
|
bar
|
||||||
|
whatever
|
||||||
|
|
||||||
/// topoglyph is a real word, i promise
|
> ls
|
||||||
mod topo;
|
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
|
||||||
let session = interactive::Session::new()?;
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
session.enable_logging("~/clyde.log");
|
|
||||||
|
|
||||||
session.run()
|
|
||||||
}
|
*/
|
||||||
|
@ -1,352 +1,463 @@
|
|||||||
use crate::error::ParseError;
|
use log::debug;
|
||||||
use crate::lex::{Lexer, Token};
|
|
||||||
use std::{
|
use std::{
|
||||||
|
fmt,
|
||||||
|
ops::Deref,
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
rc::{Rc, Weak},
|
rc::Rc,
|
||||||
sync::atomic::AtomicUsize,
|
|
||||||
};
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
/// The contents of a node in our parse tree. The parse tree consists of both terminal and
|
#[derive(Debug, Error)]
|
||||||
/// nonterminal symbols.
|
pub enum Error {
|
||||||
#[derive(Debug, PartialEq)]
|
#[error("Unexpected Token")]
|
||||||
pub enum Value {
|
UnexpectedToken,
|
||||||
/// 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,
|
|
||||||
|
|
||||||
/// The children of a statement symbol make up the components of what will become a statement
|
#[allow(dead_code)]
|
||||||
/// in our ast
|
#[derive(Debug, PartialEq)]
|
||||||
Command,
|
pub enum Token {
|
||||||
|
Ident(String),
|
||||||
|
Pipe,
|
||||||
|
}
|
||||||
|
|
||||||
/// Each of the tokens from the lex stage becomes a terminal node on our tree
|
#[allow(dead_code)]
|
||||||
Terminal(Token),
|
fn lex<S: AsRef<str>>(text: S) -> Result<Vec<Token>, Error> {
|
||||||
|
Lexer::new(text.as_ref()).lex()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Value {
|
struct Lexer<'text> {
|
||||||
fn is_terminal(&self) -> bool {
|
chars: std::str::Chars<'text>,
|
||||||
matches!(self, Value::Terminal(_))
|
peeked: Option<char>,
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A node in a parse tree.
|
impl<'text> Lexer<'text> {
|
||||||
#[derive(Debug)]
|
fn new(text: &'text str) -> Self {
|
||||||
pub struct Node {
|
Self {
|
||||||
pub id: usize,
|
chars: text.chars(),
|
||||||
|
peeked: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A node may or may not have a parent node. If a node does not have a parent node, that node
|
fn lex(&mut self) -> Result<Vec<Token>, Error> {
|
||||||
/// is the root node of a tree.
|
let mut tokens = vec![];
|
||||||
parent: Option<Weak<Node>>,
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
/// The value of the element at this node
|
fn skip_whitespace(&mut self) {
|
||||||
pub value: Value,
|
loop {
|
||||||
|
match self.peek() {
|
||||||
|
None => break,
|
||||||
|
Some(c) => {
|
||||||
|
if c.is_whitespace() {
|
||||||
|
self.skip()
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A node may or may not have children. Since an empty vector is a valid vector, a node
|
fn peek(&mut self) -> Option<char> {
|
||||||
/// without children is represented as having an empty children vector. A node having an empty
|
match self.peeked {
|
||||||
/// list of children is a leaf node in a tree.
|
Some(c) => Some(c),
|
||||||
pub children: RefCell<Vec<Rc<Node>>>,
|
None => match self.chars.next() {
|
||||||
}
|
Some(c) => {
|
||||||
|
self.peeked = Some(c);
|
||||||
|
Some(c)
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Node {
|
fn skip(&mut self) {
|
||||||
fn new() -> Cursor {
|
if self.peeked.is_some() {
|
||||||
let root = Rc::new(Node {
|
self.peeked = None;
|
||||||
id: next_id(),
|
} else {
|
||||||
parent: None,
|
self.chars.next();
|
||||||
value: Value::Start,
|
|
||||||
children: RefCell::new(Vec::new()),
|
|
||||||
});
|
|
||||||
Cursor {
|
|
||||||
target: Rc::clone(&root),
|
|
||||||
prev: root.id,
|
|
||||||
root,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_semi(&self) -> bool {
|
fn ident(&mut self) -> Token {
|
||||||
matches!(self.value, Value::Terminal(Token::Semi(_)))
|
let mut kept: Vec<char> = vec![];
|
||||||
|
loop {
|
||||||
|
match self.peek() {
|
||||||
|
None => break,
|
||||||
|
Some(c) => {
|
||||||
|
if c.is_whitespace() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
kept.push(c);
|
||||||
|
self.skip();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for Node {
|
impl fmt::Debug for Element {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
self.id == other.id
|
match self {
|
||||||
|
Element::Empty => write!(f, "()"),
|
||||||
|
Element::Command(cmd) => write!(f, "cmd<{}>", cmd),
|
||||||
|
Element::Literal(lit) => write!(f, "lit<{}>", lit),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterates through the children of a cursor node, producing new cursor positions.
|
pub struct Node {
|
||||||
pub struct ChildIter {
|
pub elem: Element,
|
||||||
target: Rc<Node>,
|
pub parent: Option<Rc<RefCell<Node>>>,
|
||||||
root: Rc<Node>,
|
pub children: Vec<Rc<RefCell<Node>>>,
|
||||||
idx: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for ChildIter {
|
impl Node {
|
||||||
type Item = Cursor;
|
pub fn new(elem: Element) -> Self {
|
||||||
|
Self {
|
||||||
|
elem,
|
||||||
|
parent: None,
|
||||||
|
children: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
pub fn empty() -> Self {
|
||||||
let children = self.target.children.borrow();
|
Self::new(Element::Empty)
|
||||||
let v = children.get(self.idx)?;
|
}
|
||||||
self.idx += 1;
|
|
||||||
Some(Cursor {
|
pub fn child_of(parent: Rc<RefCell<Self>>, elem: Element) -> Self {
|
||||||
target: Rc::clone(v),
|
Self {
|
||||||
root: Rc::clone(&self.root),
|
elem,
|
||||||
prev: self.target.id,
|
parent: Some(parent),
|
||||||
})
|
children: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn visit(self) -> Tree {
|
||||||
|
self.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const LAST_ID: AtomicUsize = AtomicUsize::new(0);
|
impl fmt::Debug for Node {
|
||||||
fn next_id() -> usize {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
LAST_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
|
write!(f, "{:?}", self.elem)?;
|
||||||
|
if self.children.len() > 0 {
|
||||||
|
write!(f, "[")?;
|
||||||
|
for child in self.children.iter() {
|
||||||
|
write!(f, "{:?}", child.borrow())?;
|
||||||
|
}
|
||||||
|
write!(f, "]")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cursor values expose access to a parse tree. A cursor is logically a pointer to a single node
|
pub struct Tree {
|
||||||
/// within a parse tree. The cursor helps with the ownership structure: so long as there is a
|
target: Rc<RefCell<Node>>,
|
||||||
/// 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 Cursor {
|
impl Tree {
|
||||||
/// Climbs one level up a parse tree. The cursor is re-pointed from its current target node to
|
fn new() -> Self {
|
||||||
/// the parent of its current target node. This method fails if the cursor is already at the
|
Node::empty().into()
|
||||||
/// 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(())
|
|
||||||
}
|
}
|
||||||
None => Err(ParseError::ParentIsGone),
|
|
||||||
},
|
/// 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))}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a value to the children of the current target node, then descends to select that
|
/// Adds a child node with a given element value to the currently selected node without chaning
|
||||||
/// child.
|
/// our selection.
|
||||||
fn push(&mut self, v: Value) -> Result<(), ParseError> {
|
fn append(self, elem: Element) -> Self {
|
||||||
if self.target.value.is_terminal() {
|
if self.is_empty() {
|
||||||
return Err(ParseError::PushOntoTerminal);
|
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);
|
||||||
}
|
}
|
||||||
let node = Node {
|
self
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// moves the cursor up to the root of the tree in place
|
/// Tells us whether or not the currently selected node is an empty node
|
||||||
pub fn up_to_root(&mut self) {
|
fn is_empty(&self) -> bool {
|
||||||
self.goto(Rc::clone(&self.root));
|
self.target.borrow().elem == Element::Empty
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn value(&self) -> &Value {
|
fn parent(&self) -> Option<Self> {
|
||||||
&self.target.value
|
self.target.borrow().parent.as_ref().map(|parent| Self {
|
||||||
|
target: Rc::clone(parent),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_children(&self) -> ChildIter {
|
fn is_root(&self) -> bool {
|
||||||
ChildIter {
|
self.parent().is_none()
|
||||||
target: Rc::clone(&self.target),
|
|
||||||
root: Rc::clone(&self.root),
|
|
||||||
idx: 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
pub fn root(self) -> Self {
|
||||||
fn is_root(&self) -> bool {
|
match self.parent() {
|
||||||
self.target.parent.is_none()
|
Some(parent) => parent.root(),
|
||||||
|
None => self,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
fn peek(&self) -> Element {
|
||||||
fn into_root(self) -> Rc<Node> {
|
self.target.borrow().elem.clone()
|
||||||
Rc::clone(&self.root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_tree(&self) -> String {
|
fn into_node(self) -> Node {
|
||||||
let mut p = self.clone();
|
self.into()
|
||||||
p.up_to_root();
|
|
||||||
let mut buf = String::new();
|
|
||||||
self.text_tree_helper(0, &mut buf);
|
|
||||||
buf
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_tree_helper(&self, depth: u32, mut buf: &mut String) {
|
fn children(&self) -> ChildIter {
|
||||||
let line = format!("{:?}\n", self.value());
|
ChildIter {
|
||||||
*buf = buf.to_owned() + &line;
|
parent: Rc::clone(&self.target),
|
||||||
for child in self.iter_children() {
|
idx: 0,
|
||||||
child.text_tree_helper(depth + 1, &mut buf);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn goto(&mut self, next: Rc<Node>) {
|
impl fmt::Debug for Tree {
|
||||||
self.prev = self.target.id;
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
self.target = next;
|
write!(f, "{:?}", self.target.borrow())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The Parser is responsible for turning a sequence of Tokens into a Parse Tree.
|
pub struct ChildIter {
|
||||||
pub struct Parser<'text> {
|
/// pointer to the node in the tree whose children we are looking at.
|
||||||
input: Lexer<'text>,
|
parent: Rc<RefCell<Node>>,
|
||||||
output: Cursor,
|
idx: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'text> Parser<'text> {
|
impl Iterator for ChildIter {
|
||||||
pub fn new(source: Lexer<'text>) -> Self {
|
type Item = Tree;
|
||||||
Self {
|
|
||||||
input: source,
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
output: Node::new(),
|
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})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn parse(mut self) -> Result<Cursor, ParseError> {
|
impl From<Tree> for Node {
|
||||||
while self.step()? {}
|
fn from(value: Tree) -> Self {
|
||||||
self.output.up_to_root();
|
value.target.replace(Node::empty())
|
||||||
Ok(self.output)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn step(&mut self) -> Result<bool, ParseError> {
|
impl From<Node> for Tree {
|
||||||
match self.output.value() {
|
fn from(value: Node) -> Self {
|
||||||
Value::Start => self.step_start(),
|
Self {
|
||||||
Value::Command => self.step_statement(),
|
target: Rc::new(RefCell::new(value)),
|
||||||
Value::Terminal(_) => panic!(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn step_start(&mut self) -> Result<bool, ParseError> {
|
// I don't know how to write a parser lol
|
||||||
assert!(matches!(self.output.value(), Value::Start));
|
|
||||||
match self.input.peek()? {
|
// ls
|
||||||
Some(Token::Word(_)) => {
|
// Command{name: "ls"}
|
||||||
self.output.push(Value::Command)?;
|
|
||||||
let token = self.input.next().unwrap()?;
|
// echo one two three
|
||||||
self.output.push(Value::Terminal(token))?;
|
// Command{name: "echo", args: [Lit("one"), Lit("two"), Lit("three")]}
|
||||||
self.output.up()?;
|
|
||||||
Ok(true)
|
// echo *.rs
|
||||||
}
|
// Command{name: "echo", args: [Glob("*.rs")]}
|
||||||
Some(Token::Glob(_)) => {
|
// Command{name: "echo", args: [Path("main.rs"), Path("parse.rs"), ...]}
|
||||||
let token = self.input.next().unwrap()?;
|
|
||||||
Err(ParseError::UnexpectedToken(token))
|
// cat main.rs | wc -l
|
||||||
}
|
// Pipe{
|
||||||
Some(Token::Semi(_)) => {
|
// src: Command{name: "cat", args: [Lit("main.rs")]}
|
||||||
self.output.push(Value::Command)?;
|
// dest: Command{name: "wc", args: [Lit("-l")]}
|
||||||
let token = self.input.next().unwrap()?;
|
// }
|
||||||
self.output.push(Value::Terminal(token))?;
|
|
||||||
self.output.up()?;
|
// echo one ; echo two
|
||||||
self.output.up()?;
|
// Sequence{
|
||||||
Ok(true)
|
// 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));
|
||||||
}
|
}
|
||||||
Some(Token::Pipe(_)) => {
|
(Element::Command(_), Token::Ident(arg)) => {
|
||||||
let token = self.input.next().unwrap()?;
|
tree = tree.append(Element::Literal(arg));
|
||||||
Err(ParseError::UnexpectedToken(token))
|
|
||||||
}
|
}
|
||||||
None => Ok(false),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(tree.root())
|
||||||
|
}
|
||||||
|
|
||||||
fn step_statement(&mut self) -> Result<bool, ParseError> {
|
#[cfg(test)]
|
||||||
assert!(matches!(self.output.value(), Value::Command));
|
mod tests {
|
||||||
match self.input.peek()? {
|
use super::*;
|
||||||
Some(Token::Word(_) | Token::Glob(_)) => {
|
use Token::Pipe;
|
||||||
let token = self.input.next().unwrap()?;
|
|
||||||
self.output.push(Value::Terminal(token))?;
|
fn ident<S: Into<String>>(s: S) -> Token {
|
||||||
self.output.up()?;
|
Token::Ident(s.into())
|
||||||
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),
|
|
||||||
|
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(())
|
||||||
}
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
lex! {
|
||||||
|
single_token:
|
||||||
|
"one"
|
||||||
|
ident("one");
|
||||||
|
|
||||||
a b
|
two_tokens:
|
||||||
command
|
"ls one two"
|
||||||
terminal
|
ident("ls") ident("one") ident("two");
|
||||||
a
|
|
||||||
terminal
|
|
||||||
b
|
|
||||||
|
|
||||||
a | b
|
leading_whitespace:
|
||||||
pipeline
|
" one"
|
||||||
command
|
ident("one");
|
||||||
terminal
|
|
||||||
a
|
|
||||||
terminal
|
|
||||||
|
|
|
||||||
command
|
|
||||||
terminal
|
|
||||||
b
|
|
||||||
|
|
||||||
|
trailing_whitespace:
|
||||||
|
"one "
|
||||||
|
ident("one");
|
||||||
|
|
||||||
*/
|
surrounding_whitespace:
|
||||||
|
" one "
|
||||||
|
ident("one");
|
||||||
|
|
||||||
#[cfg(test)]
|
internal_hyphen:
|
||||||
mod test {
|
"one-two"
|
||||||
use super::*;
|
ident("one-two");
|
||||||
use crate::lex::lex;
|
|
||||||
|
pipe:
|
||||||
|
"|"
|
||||||
|
Pipe;
|
||||||
|
|
||||||
fn parse(source: &str) -> Result<Cursor, ParseError> {
|
pipeline:
|
||||||
let tokens = Lexer::new(source);
|
"one | two"
|
||||||
let parser = Parser::new(tokens);
|
ident("one") Pipe ident("two");
|
||||||
parser.parse()
|
|
||||||
|
pipeline_2:
|
||||||
|
"one |two"
|
||||||
|
ident("one") Pipe ident("two");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn root() {
|
fn empty_tree() {
|
||||||
let mut cursor = Node::new();
|
let root = Tree::new().root();
|
||||||
assert!(cursor.up().is_err());
|
assert!(root.is_root());
|
||||||
assert!(cursor.target.value == Value::Start);
|
assert_eq!(root.peek(), Element::Empty);
|
||||||
assert!(cursor.is_root());
|
assert_eq!(root.children().count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn single_val() {
|
fn tree_root() {
|
||||||
let mut cursor = Node::new();
|
let root = Tree::new()
|
||||||
|
.push(Element::Command(String::from("ls")))
|
||||||
let mut tokens = lex(" ls ").unwrap();
|
.root();
|
||||||
let ls = tokens.pop().unwrap();
|
assert!(root.is_root());
|
||||||
assert!(cursor.push(Value::Command).is_ok());
|
assert_eq!(root.children().count(), 0);
|
||||||
assert!(cursor.push(Value::Terminal(ls.clone())).is_ok());
|
assert_eq!(root.into_node().elem, Element::Command(String::from("ls")));
|
||||||
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]
|
#[test]
|
||||||
fn test_parse() -> Result<(), ParseError> {
|
fn tree_push() {
|
||||||
let x = parse("ls one two three")?;
|
let tree = Tree::new()
|
||||||
println!("Parse tree: {x:?}");
|
.push(Element::Command(String::from("ls")));
|
||||||
// parse("*")?;
|
assert_eq!(tree.peek(), Element::Command(String::from("ls")));
|
||||||
// parse("x* ls")?;
|
|
||||||
Ok(())
|
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")));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
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,
|
|
||||||
}
|
|
@ -0,0 +1,162 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,252 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,207 +0,0 @@
|
|||||||
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