i'm redoing all the parsing lol
parent
d152c4092a
commit
cd51f4cce1
@ -1,3 +1,98 @@
|
||||
# clyde
|
||||
|
||||
A command-line shell. Initial development is for Windows because Windows has the worst starting point when it comes to CLI shells.
|
||||
A command-line shell. Initial development is for Windows because Windows has
|
||||
the worst starting point when it comes to CLI shells. Assuming nothing here
|
||||
works and it doesn't even compile.
|
||||
|
||||
## Background
|
||||
|
||||
The needs of this project reflect my professional experience working on
|
||||
multiplayer video games. Prior to working in the games industry, I worked in
|
||||
the web industry, primarily writing server software. The working practices of
|
||||
programmers in these industries differ substantially. The large differences in
|
||||
these working practices create a substantial and difficult social divide
|
||||
within studios that make multiplayer games. These social differences make an
|
||||
already-difficult category of software even more difficult to develop. The
|
||||
goal of this project is to reduce the tooling divide that exists between
|
||||
client and server developers at multiplayer game studios, with the expectation
|
||||
that shrinking this tooling gap can reduce the social divide that exists
|
||||
between game developers and server/infrastructure developers at game studios.
|
||||
|
||||
### Windows is a hard requirement
|
||||
|
||||
The first gap that appears within the greater software developer landscape is
|
||||
the question of using Windows at all. Let us dispense with this question
|
||||
immediately: Windows is a necesary tool within the game development industry.
|
||||
Supporting Windows is a hard requirement of this project.
|
||||
|
||||
Many necessary tools, such as tools provided by game console manufacturers and
|
||||
tools that integrate with the dominant game engines (Unity and Unreal) only
|
||||
support Windows. If your reaction to this is "don't use those tools" or "make
|
||||
your game in Godot", please just stop. This is a very common reaction from
|
||||
professional programmers who have never worked in gamedev, who don't
|
||||
understand the constraints that most game developers face. Unfortunately a
|
||||
large portion of programming discussion happens on Hacker News, Reddit, and
|
||||
Lobsters, each of which uses a democratic structure.
|
||||
|
||||
I will not attempt to convince you that Windows is broadly
|
||||
unavoidable in professional game development and that Windows is a necessary
|
||||
tool for game development studios, even studios that employ engineers for whom
|
||||
Windows is not necessary for their individual role. If you want to advocate
|
||||
for Linux superiority, please go bother Nintendo, Sony, Microsoft, etc, not
|
||||
individual game developers. If you really can't get over this and feel the
|
||||
need to yell at me, please walk a few hundred yards into a dense forest and
|
||||
scream your complaints into the wilderness to feel better.
|
||||
|
||||
The command-line environments built into Windows are incredibly primitive.
|
||||
These environments are so primitive that millions of professional programmers
|
||||
who work primarily on Windows are led to believe that command-line
|
||||
environments themselves are *inherently* primitive.
|
||||
|
||||
. The limitations of built-in
|
||||
command-line environments on Windows is so severe that there are entire
|
||||
categories of professional computer programmers who believe that they are "not
|
||||
terminal people", often without realizing that the terminal on Linux and MacOS
|
||||
is a vastly different experience and different tooling ecosystem than it is on
|
||||
Windows. Many professional Windows developers believe that CLI environments
|
||||
are inherently primitive, because their only exposure to CLI environments is
|
||||
through the built-in Windows shells.
|
||||
|
||||
Windows ships with two built-in
|
||||
shells. The first shell is the Command Prompt, which we'll refer to as
|
||||
[cmd.exe](https://en.wikipedia.org/wiki/Cmd.exe) as it is colloquially known,
|
||||
even when running inside of
|
||||
[terminal.exe](https://en.wikipedia.org/wiki/Windows_Terminal). The second
|
||||
shell is [PowerShell](https://en.wikipedia.org/wiki/PowerShell).
|
||||
|
||||
### Insufficiency of cmd.exe
|
||||
|
||||
The insufficiency of cmd.exe is widely understood to people with experience
|
||||
with Unix-like operating systems such as Linux and MacOS, which typically
|
||||
include either [bash](https://en.wikipedia.org/wiki/Bash_(Unix_shell)) or
|
||||
[zsh](https://en.wikipedia.org/wiki/Z_shell). This insufficiency is severe
|
||||
enough that it drives people off of Windows entirely.
|
||||
|
||||
In some industries, such as games, Windows is a required development
|
||||
environment, because many tools are available only on Windows. Free software
|
||||
purists who want to insist that you can make video games on Linux, please feel
|
||||
free to leave now, this project is not for you.
|
||||
|
||||
## Terimology
|
||||
|
||||
- **executable**: a shell built-in or an executable file (i.e., an .exe file)
|
||||
|
||||
## examples
|
||||
|
||||
$> a
|
||||
|
||||
Find an executable named `a` somewhere on `PATH` and execute it in the
|
||||
foreground.
|
||||
|
||||
$> ./a
|
||||
|
||||
Use an executable named `a` within the current directory and execute it in the
|
||||
foreground. `a` must be a file in the current directory. If `a` would refer to
|
||||
both a shell built-in and a file in the current directory, use the file in the
|
||||
current directory.
|
||||
|
||||
$>
|
||||
|
@ -0,0 +1,278 @@
|
||||
use crate::error::ParseError;
|
||||
use crate::lex::{Lexer, Token};
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
collections::VecDeque,
|
||||
rc::{Rc, Weak},
|
||||
};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
pub enum Value {
|
||||
/// The start symbol of our parse tree. Each parse tree is rooted in a node whose value is the
|
||||
/// start symbol. This is the only node in the tree that should utilize the start symbol.
|
||||
Start,
|
||||
|
||||
Statement,
|
||||
|
||||
Terminal(Token),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
fn is_terminal(&self) -> bool {
|
||||
matches!(self, Value::Terminal(_))
|
||||
}
|
||||
}
|
||||
|
||||
/// A node in a parse tree.
|
||||
pub struct Node {
|
||||
/// A node may or may not have a parent node. If a node does not have a parent node, that node
|
||||
/// is the root node of a tree.
|
||||
parent: Option<Weak<Node>>,
|
||||
|
||||
/// The value of the element at this node
|
||||
value: Value,
|
||||
|
||||
/// 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.
|
||||
children: RefCell<Vec<Rc<Node>>>,
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn new() -> Cursor {
|
||||
let root = Node {
|
||||
parent: None,
|
||||
value: Value::Start,
|
||||
children: RefCell::new(Vec::new()),
|
||||
};
|
||||
|
||||
let root = Rc::new(root);
|
||||
Cursor {
|
||||
target: Rc::clone(&root),
|
||||
root,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cursor values expose access to a parse tree.
|
||||
struct Cursor {
|
||||
target: Rc<Node>,
|
||||
root: Rc<Node>,
|
||||
}
|
||||
|
||||
impl Cursor {
|
||||
/// Climbs one level up a parse tree. The cursor is re-pointed from its current target node to
|
||||
/// the parent of its current target node. This method fails if the cursor is already at the
|
||||
/// root node of the parse tree.
|
||||
fn up(&mut self) -> Result<(), ParseError> {
|
||||
match &self.target.parent {
|
||||
None => Err(ParseError::AtRootAlready),
|
||||
Some(parent) => match parent.upgrade() {
|
||||
Some(parent) => {
|
||||
self.target = parent;
|
||||
Ok(())
|
||||
}
|
||||
None => Err(ParseError::ParentIsGone),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a value to the children of the current target node, then descends to select that
|
||||
/// child.
|
||||
fn push(&mut self, v: Value) -> Result<(), ParseError> {
|
||||
if self.target.value.is_terminal() {
|
||||
return Err(ParseError::PushOntoTerminal);
|
||||
}
|
||||
let node = Node {
|
||||
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.target = node;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_root(&self) -> bool {
|
||||
self.target.parent.is_none()
|
||||
}
|
||||
|
||||
fn into_root(self) -> Rc<Node> {
|
||||
Rc::clone(&self.root)
|
||||
}
|
||||
|
||||
fn value(&self) -> &Value {
|
||||
&self.target.value
|
||||
}
|
||||
}
|
||||
|
||||
struct Parser<'text> {
|
||||
source: Lexer<'text>,
|
||||
cursor: Cursor,
|
||||
}
|
||||
|
||||
impl<'text> Parser<'text> {
|
||||
pub fn new(source: Lexer<'text>) -> Self {
|
||||
Self {
|
||||
source,
|
||||
cursor: Node::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(mut self) -> Result<Rc<Node>, ParseError> {
|
||||
while self.step()? {}
|
||||
Ok(self.cursor.into_root())
|
||||
}
|
||||
|
||||
fn step(&mut self) -> Result<bool, ParseError> {
|
||||
match self.cursor.value() {
|
||||
Value::Start => self.step_start(),
|
||||
Value::Statement => self.step_statement(),
|
||||
Value::Terminal(_) => panic!(),
|
||||
}
|
||||
}
|
||||
|
||||
fn step_start(&mut self) -> Result<bool, ParseError> {
|
||||
assert!(matches!(self.cursor.value(), Value::Start));
|
||||
match self.source.peek()? {
|
||||
Some(Token::String(_)) => {
|
||||
self.cursor.push(Value::Statement)?;
|
||||
let token = self.source.next().unwrap()?;
|
||||
self.cursor.push(Value::Terminal(token))?;
|
||||
self.cursor.up()?;
|
||||
Ok(true)
|
||||
}
|
||||
Some(Token::Glob(_)) => {
|
||||
let token = self.source.next().unwrap()?;
|
||||
Err(ParseError::UnexpectedToken(token))
|
||||
}
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
fn step_statement(&mut self) -> Result<bool, ParseError> {
|
||||
assert!(matches!(self.cursor.value(), Value::Statement));
|
||||
match self.source.peek()? {
|
||||
Some(Token::String(_) | Token::Glob(_)) => {
|
||||
let token = self.source.next().unwrap()?;
|
||||
self.cursor.push(Value::Terminal(token))?;
|
||||
self.cursor.up()?;
|
||||
Ok(true)
|
||||
}
|
||||
None => Ok(false),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse(source: &str) -> Result<Rc<Node>, ParseError> {
|
||||
let tokens = Lexer::new(source);
|
||||
let parser = Parser::new(tokens);
|
||||
parser.parse()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::lex::lex;
|
||||
|
||||
#[test]
|
||||
fn root() {
|
||||
let mut cursor = Node::new();
|
||||
assert!(cursor.up().is_err());
|
||||
assert!(cursor.target.value == Value::Start);
|
||||
assert!(cursor.is_root());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_val() {
|
||||
let mut cursor = Node::new();
|
||||
|
||||
let mut tokens = lex(" ls ").unwrap();
|
||||
let ls = tokens.pop().unwrap();
|
||||
assert!(cursor.push(Value::Statement).is_ok());
|
||||
assert!(cursor.push(Value::Terminal(ls.clone())).is_ok());
|
||||
assert!(cursor.push(Value::Terminal(ls.clone())).is_err());
|
||||
assert!(cursor.target.value == Value::Terminal(ls));
|
||||
assert!(!cursor.is_root());
|
||||
assert!(cursor.up().is_ok());
|
||||
assert!(cursor.up().is_ok());
|
||||
assert!(cursor.is_root());
|
||||
assert!(cursor.up().is_err());
|
||||
assert!(cursor.value() == &Value::Start);
|
||||
let root = cursor.into_root();
|
||||
assert!(root.value == Value::Start);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse() -> Result<(), ParseError> {
|
||||
parse("ls")?;
|
||||
// parse("*")?;
|
||||
// parse("x* ls")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
> ls
|
||||
|
||||
start
|
||||
statement
|
||||
ls
|
||||
|
||||
> ls ;
|
||||
|
||||
start
|
||||
statement
|
||||
ls
|
||||
;
|
||||
|
||||
> ls ; ls
|
||||
|
||||
start
|
||||
statement
|
||||
ls
|
||||
;
|
||||
statement
|
||||
ls
|
||||
|
||||
> ls one two three
|
||||
|
||||
start
|
||||
statement
|
||||
ls
|
||||
one
|
||||
two
|
||||
three
|
||||
|
||||
> ls > files.txt ; echo files.txt
|
||||
|
||||
start
|
||||
statement
|
||||
ls
|
||||
>
|
||||
files.txt
|
||||
;
|
||||
statement
|
||||
echo
|
||||
files.txt
|
||||
|
||||
> if exists ~/.vimrc : echo you have a vimrc
|
||||
|
||||
> if $x == 3: echo hi
|
||||
|
||||
start
|
||||
if
|
||||
expression
|
||||
$x
|
||||
==
|
||||
3
|
||||
:
|
||||
statement
|
||||
echo
|
||||
hi
|
||||
*/
|
Loading…
Reference in New Issue