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