diff --git a/README.md b/README.md index 61af4d3..b0a7837 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,37 @@ Rust shell. Inspired by Ion. In case you're reading this: rush is in the works and not a priority. Features may be missing even if defined below. +## Scopes + +Variables are block scoped. + +Block scope creation: +- `if` +- `while` +- `else` +- `for..` +- `$(expr)` + +Functions have a copy of their scope. + +Files create file scopes, to which functions are scoped. + ## Syntax `;` is 'alias' for new line. Syntax and type errors crash the program. -Scopes are for each block. Functions won't have access to variables it wouldn't have access if it wasn't a function: - -```sh -fn testing - echo $t -end - -if true - testing # Error! t is not defined -end -``` +Variables are scoped to their block, and immediately freed when their block is left. ### Variables -String variables using `$`, arrays using `@`. +String variable value can be obtained using `$`, arrays using `@`. + +When an array is stringified (referred to with `$`), it's contents are joined with space. +No special treatment of `PATH`. + +Currently, the shell doesn't error out when variable doesn't exist, instead, it's replaced by an empty string. Assigned using `let`. Left side is evaluated to a string as well. @@ -37,8 +47,9 @@ echo $d # c ``` Arrays are assigned using `[ var ]`. You can join arrays and strings by simply passing them there, like `[ $var @var ]`. +Arrays and maps cannot be nested during definition (`[ $var [@var] ]` should have the same effect). -All assignments are done via the `let` keyword. +All assignments are done via the `let` keyword. If the variable exists, it is overwritten (even in upper scopes). Instead of `=`, other operations are supported: * `*=` - multiply @@ -55,29 +66,9 @@ Instead of `=`, other operations are supported: `env::` namespace contains the environment (and doesn't error out if the variable doesn't exist, instead, empty string is returned) `color::` (alias `c::`) has a number of colors -#### Types - -Based on the value set in the set `let` (the one with just `=`), a type is infered (unless specificaly set using `let x:type = ...`). This type is then used for the operations after. - -Supported types: - -* `i32` (alias int) -* `i64` -* `i128` -* `u32` -* `u64` -* `u128` -* `f32` -* `f64` -* `str` -* `hmap[T]` (where T is one of the other types, except array) -* `[T]` (where T is one of the other types, except hmap) - -HashMap is basically array, but with string keys (instead of numbers) in random order. - ### Return -Sets the exit code (and possibly exits function/script early). If no return is set, the return code is set to the return code of the last expression. +Sets the exit code (and possibly exits function/script early). If no return is set, the return code is set to the return code of the last expression (`$?`). ### Math @@ -99,29 +90,32 @@ Slices: `[x..y]` gets a substring (or subarray) of the variable. When `x` ommite Bracketless. Scopes are ended by the keyword `end`. -`if` - Runs it's scope if the command returns `0`. Useful in pair with `test` builtin. -`for $val of @arr` - Runs for each value of the array (or hashmap) -`for $val of ...` - Runs for each number in the range. -`while` - Runs in loop as long as the command returns `0` +- `if` - Runs it's scope if the command returns `0`. Useful in pair with `test` builtin. `else` supported. `else if` doesn't require another `end`. +- `for $val of @arr` - Runs for each value of the array (or hashmap) +- `for $val of X..Y` - Runs for each number in the range `X` and `Y` (both inclusive). +- `while` - Runs in loop as long as the command returns `0` ### Functions -Defined by `fn name arg -- desc`. `arg` can be ommited, or repeated. `desc` will be printed when the `arg` is missing (or when `describe` command is used). +Defined by `fn name [...arg] [--flags]`. `arg` can be ommited, or repeated. +`--flags` can be used to add additional functionality. -#### Special +Functions are scoped per file, even if they use `on-event` or similar to be triggered. +Use `source` to load external files with functions to be triggered. -From config (defined by `~/.rushrc`), special functions can be defined. - -* `PROMPT` will be run to render the prompt -* `HIGHLIGHT` will run (for each key - make it fast) to highlight the text. +- `--desc` sets the functions description. +- `--on-event` will run the function when an event is run #### Builtins -* `let` for assigning variables -* `export` for exporting variables to env +`let` is a special case which cannot be dynamically addressed (i.e. using `$(echo let) var = value`). + +* `let` for assigning variables (`let var = value`) +* `export` for exporting variables to env (`export var` to export var, or `export var = value`) * `test` tests for evaluation (`=` for equality, `>`, `<`, `<=`, `>=` for number comparisons) * `exists` for existance of a given string, or if given a flag (`-F`unctions, `-v`ariables, `-e`nv, `-f`ile, `-d`irectory, `-r`eadable file, `-w`ritable file, e`-x`ecutable file), existence of the selected object * `true` returns `0` * `false` returns `1` +* `source` to run another file in the same file scope Some GNU standard utils may be overwritten by rush builtins, but must be made compatible. diff --git a/src/main.rs b/src/main.rs index 8c99cdf..36c4de8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -71,19 +71,82 @@ impl Shell { let v = stdin.lock().lines().next().unwrap().unwrap(); self.term.input = v; } -} -fn start_shell() { - let mut shell = Shell::new(); - loop { - print!("$: "); - io::stdout().flush().unwrap(); - shell.collect(); - shell.term.input += "\n"; - let res = parser::exec(&mut shell.term.input.as_bytes(), &mut shell.ctx); - match res { - Err(err) => eprintln!("rush: {}", err), - Ok(_) => {} + + fn edit(&mut self) { + let stdin = io::stdin(); + let mut stdout = io::stdout().into_raw_mode().unwrap(); + for c in stdin.keys() { + let c = c.unwrap(); + match c { + Key::Char('\n') => { + if self.term.input.chars().nth(self.term.idx).unwrap_or(' ') == '\\' { + self.term.insert_str(self.term.idx, "\\\n"); + } else { + break; + } + } + Key::Backspace => { + if self.term.input.len() > 0 && self.term.idx > 0 { + if self.term.idx == self.term.input.len() - 1 { + self.term.input.pop(); + } else { + self.term.remove(self.term.idx - 1); + } + self.term.idx -= 1; + } + } + Key::Delete => { + if self.term.idx < self.term.input.len() { + self.term.remove(self.term.idx); + } + } + Key::End => { + self.term.idx = cmp::max(self.term.input.len(), 1) - 1; + } + Key::Home => { + self.term.idx = 0; + } + Key::Left => { + if self.term.idx > 0 { + self.term.idx -= 1; + } + } + Key::Right => { + if self.term.idx < self.term.input.len() - 1 { + self.term.idx += 1; + } + } + Key::Ctrl('c') => { + process::exit(1); + } + Key::Ctrl('d') => { + process::exit(0); + } + Key::Char(char) => { + self.term.insert(self.term.idx, char); + self.term.idx += 1; + } + _ => {} + } + self.term.print(&mut stdout); + stdout.flush().unwrap(); + } + stdout.suspend_raw_mode().unwrap(); + } + + fn start() { + let mut shell = Shell::new(); + loop { + print!("$: "); + io::stdout().flush().unwrap(); + shell.collect(); + shell.term.input += "\n"; + let res = parser::exec(&mut shell.term.input.as_bytes(), &mut shell.ctx); + match res { + Err(err) => eprintln!("rush: {}", err), + Ok(_) => {} + } } } } @@ -127,7 +190,7 @@ fn main() { }, None => {} }; - start_shell(); + Shell::start(); } #[cfg(test)] @@ -169,64 +232,3 @@ mod test { load_and_run("test/while.rush") } } - -fn editor() -> Shell { - let stdin = io::stdin(); - let mut stdout = io::stdout().into_raw_mode().unwrap(); - let mut shell = Shell::new(); - for c in stdin.keys() { - let c = c.unwrap(); - match c { - Key::Char('\n') => { - if shell.term.input.chars().nth(shell.term.idx).unwrap_or(' ') == '\\' { - shell.term.insert_str(shell.term.idx, "\\\n"); - } else { - break; - } - } - Key::Backspace => { - if shell.term.input.len() > 0 && shell.term.idx > 0 { - if shell.term.idx == shell.term.input.len() - 1 { - shell.term.input.pop(); - } else { - shell.term.remove(shell.term.idx - 1); - } - shell.term.idx -= 1; - } - } - Key::Delete => { - if shell.term.idx < shell.term.input.len() { - shell.term.remove(shell.term.idx); - } - } - Key::End => { - shell.term.idx = cmp::max(shell.term.input.len(), 1) - 1; - } - Key::Home => { - shell.term.idx = 0; - } - Key::Left => { - if shell.term.idx > 0 { - shell.term.idx -= 1; - } - } - Key::Right => { - if shell.term.idx < shell.term.input.len() - 1 { - shell.term.idx += 1; - } - } - Key::Ctrl('c') => { - process::exit(1); - } - Key::Char(char) => { - shell.term.insert(shell.term.idx, char); - shell.term.idx += 1; - } - _ => {} - } - shell.term.print(&mut stdout); - stdout.flush().unwrap(); - } - stdout.suspend_raw_mode().unwrap(); - shell -} diff --git a/src/parser/ast.rs b/src/parser/ast.rs index f329bcb..2e3e7a1 100644 --- a/src/parser/ast.rs +++ b/src/parser/ast.rs @@ -1,4 +1,4 @@ -use crate::parser::tokens::Tokens; +use crate::parser::tokens::{Token, Tokens}; use anyhow::{bail, Context, Result}; #[derive(Debug)] @@ -126,7 +126,7 @@ pub enum Expression { #[derive(Debug)] struct Tree { - tokens: Vec, + tokens: Vec, i: usize } @@ -177,7 +177,7 @@ impl Tree { buf.push(val); if self.i >= end - 1 { break } self.i += 1; - token = self.tokens.get(self.i).unwrap(); + token = &self.tokens.get(self.i).unwrap().token; if matches!(token, Tokens::CommandEnd(_)) { break } } match &token { @@ -196,7 +196,7 @@ impl Tree { self.inc(); let mut len = 0; for token in &self.tokens[self.i..] { - match token { + match token.token { Tokens::ExportSet => { break }, _ => len += 1 } @@ -218,7 +218,7 @@ impl Tree { let mut found_first = false; for token in &self.tokens[self.i..] { val_end += 1; - match token { + match token.token { Tokens::Space => if found_first { break }, Tokens::CommandEnd(_) => if !found_first { bail!("Unexpected command end") } else { break }, Tokens::FileRead => bail!("Unexpected file read (<)"), @@ -242,7 +242,7 @@ impl Tree { let mut found_first = false; for token in &self.tokens[self.i..] { val_end += 1; - match token { + match token.token { Tokens::Space => if found_first { break }, Tokens::CommandEnd(_) => if !found_first { bail!("Unexpected command end") } else { break }, Tokens::FileRead => bail!("Unexpected file read (<)"), @@ -381,7 +381,7 @@ impl Tree { let mut lvl = 1; self.inc(); for token in &self.tokens[self.i..] { - match token { + match token.token { Tokens::SubStart => lvl += 1, Tokens::StringFunction(_) => lvl += 1, Tokens::ArrayFunction(_) => lvl += 1, @@ -468,7 +468,7 @@ impl Tree { let mut lvl = 1; self.inc(); for token in &self.tokens[self.i..] { - match token { + match token.token { Tokens::ParenthesisStart => lvl += 1, Tokens::ParenthesisEnd => lvl -= 1, _ => {} @@ -547,11 +547,11 @@ impl Tree { self.i += 1; self } - fn get_current_token(&self) -> &Tokens { self.tokens.get(self.i).unwrap() } - fn get_next_token(&self) -> &Tokens { self.tokens.get(self.i + 1).unwrap() } + fn get_current_token(&self) -> &Tokens { &self.tokens.get(self.i).unwrap().token } + fn get_next_token(&self) -> &Tokens { &self.tokens.get(self.i + 1).unwrap().token } } -pub fn build_tree(tokens: Vec) -> Result> { +pub fn build_tree(tokens: Vec) -> Result> { let mut expressions: Vec = Vec::new(); let mut tree = Tree { tokens, i: 0 }; loop { diff --git a/src/parser/tokens.rs b/src/parser/tokens.rs index c7e2872..6d9a974 100644 --- a/src/parser/tokens.rs +++ b/src/parser/tokens.rs @@ -1,5 +1,12 @@ use anyhow::{Result, bail}; +#[derive(Debug)] +pub struct Token { + pub token: Tokens, + pub start: usize, + pub end: usize +} + #[derive(Debug)] pub enum Tokens { Space, @@ -86,7 +93,7 @@ impl Tokens { } -fn read_var_ahead(i: usize, text: &String) -> (usize, Tokens) { +fn read_var_ahead(i: usize, text: &String) -> (usize, Token) { let mut x = i; let mut buf = String::new(); let parens_mode = text.chars().nth(x + 1).unwrap() == '{'; @@ -113,14 +120,14 @@ fn read_var_ahead(i: usize, text: &String) -> (usize, Tokens) { } } let token = match text.chars().nth(i).unwrap() { - '$' => Tokens::StringVariable(buf, parens_mode), - '@' => Tokens::ArrayVariable(buf, parens_mode), + '$' => Token { token: Tokens::StringVariable(buf, parens_mode), start: i, end: i + x }, + '@' => Token { token: Tokens::ArrayVariable(buf, parens_mode), start:i , end: i+x }, a => panic!("Invalid value {}", a) }; (x - i - 1, token) } -pub fn tokenize(reader: &mut dyn std::io::BufRead) -> Result> { +pub fn tokenize(reader: &mut dyn std::io::BufRead) -> Result> { let mut quote_active = false; let mut double_quote_active = false; let mut escape_active = false; @@ -128,10 +135,10 @@ pub fn tokenize(reader: &mut dyn std::io::BufRead) -> Result> { reader.read_to_string(&mut text)?; let mut text_length = text.len(); - let mut tokens: Vec = Vec::new(); + let mut tokens: Vec = Vec::new(); - fn save_buf(buf: &mut String, tokens: &mut Vec) { - if buf.len() > 0 { tokens.push(Tokens::detect(std::mem::take(buf))) } + fn save_buf(buf: &mut String, tokens: &mut Vec, i: usize) { + if buf.len() > 0 { tokens.push(Token { token: Tokens::detect(std::mem::take(buf)), end: i, start: i - buf.len() }) } } let mut buf = String::new(); @@ -147,22 +154,22 @@ pub fn tokenize(reader: &mut dyn std::io::BufRead) -> Result> { '"' => if !escape_active && !quote_active { double_quote_active = !double_quote_active; buf_add = false }, '\'' => if !escape_active && !double_quote_active { quote_active = !quote_active; buf_add = false }, '$' => if !escape_active && !quote_active { - save_buf(&mut buf, &mut tokens); + save_buf(&mut buf, &mut tokens, i); if text_length > i && text.chars().nth(i + 1).unwrap() == '(' { - tokens.push(Tokens::SubStart); + tokens.push(Token { token: Tokens::SubStart, start: i, end: i+1 }); skipper = 1; buf_add = false; } else { let (skippers, mut token) = read_var_ahead(i, &text); - match token { + match token.token { Tokens::StringVariable(ref str, bool) => if !bool && !double_quote_active { if text.len() > i + skippers && text.chars().nth(i + skippers).unwrap() == '(' { - token = Tokens::StringFunction(str.clone()); + token = Token { token: Tokens::StringFunction(str.clone()), end: i + skippers, start: i }; } }, Tokens::ArrayVariable(ref str, bool) => if !bool && !double_quote_active { if text.len() > i + skippers && text.chars().nth(i + skippers).unwrap() == '(' { - token = Tokens::ArrayFunction(str.clone()); + token = Token { token: Tokens::ArrayFunction(str.clone()), end: i+skippers, start: i }; } } _ => bail!("Cannot happen") @@ -173,8 +180,8 @@ pub fn tokenize(reader: &mut dyn std::io::BufRead) -> Result> { } }, ';' | '\r' | '\n' => if !escape_active && !quote_active && !double_quote_active { - save_buf(&mut buf, &mut tokens); - tokens.push(Tokens::CommandEnd(letter.clone())); + save_buf(&mut buf, &mut tokens, i); + tokens.push(Token { token: Tokens::CommandEnd(letter.clone()), start: i, end: i }); let mut x = 0; while x < text.len() - 1 && matches!(text.chars().nth(x).unwrap(), '\n' | '\r' | ';' | ' ') { x += 1; @@ -185,28 +192,28 @@ pub fn tokenize(reader: &mut dyn std::io::BufRead) -> Result> { buf_add = false; }, '&' => if !escape_active && !quote_active && !double_quote_active { - save_buf(&mut buf, &mut tokens); + save_buf(&mut buf, &mut tokens, i); if i + 1 < text.len() && text.chars().nth(i+1).unwrap() == '&' { - tokens.push(Tokens::And); + tokens.push(Token { token: Tokens::And, start: i, end: i+1 }); skipper = 1; } else { - tokens.push(Tokens::JobCommandEnd); + tokens.push(Token { token: Tokens::JobCommandEnd, start: i , end: i }); } buf_add = false; }, '|' => if !escape_active && !quote_active && !double_quote_active { - save_buf(&mut buf, &mut tokens); + save_buf(&mut buf, &mut tokens, i); if i + 1 < text.len() && text.chars().nth(i+1).unwrap() == '|' { - tokens.push(Tokens::Or); + tokens.push(Token { token: Tokens::Or, start: i, end: i+1 }); skipper = 1; } else { - tokens.push(Tokens::RedirectInto); + tokens.push(Token { token: Tokens::RedirectInto, start: i, end: i }); } buf_add = false; }, ' ' => if !escape_active && !quote_active && !double_quote_active { - save_buf(&mut buf, &mut tokens); - tokens.push(Tokens::Space); + save_buf(&mut buf, &mut tokens, i); + tokens.push(Token { token: Tokens::Space, start: i, end: i }); let mut x = i; while text.chars().nth(x).unwrap() == ' ' { x += 1; @@ -215,13 +222,13 @@ pub fn tokenize(reader: &mut dyn std::io::BufRead) -> Result> { buf_add = false; }, '(' => if !quote_active && !double_quote_active && !escape_active { - save_buf(&mut buf, &mut tokens); - tokens.push(Tokens::ParenthesisStart); + save_buf(&mut buf, &mut tokens, i); + tokens.push(Token { token: Tokens::ParenthesisStart, start: i, end: i }); buf_add = false; } ')' => if !quote_active && !double_quote_active && !escape_active { - save_buf(&mut buf, &mut tokens); - tokens.push(Tokens::ParenthesisEnd); + save_buf(&mut buf, &mut tokens, i); + tokens.push(Token { token: Tokens::ParenthesisEnd, start: i, end: i }); buf_add = false; }, '\\' => if !escape_active { @@ -231,12 +238,12 @@ pub fn tokenize(reader: &mut dyn std::io::BufRead) -> Result> { escape_active = false; }, '=' => if !escape_active && !quote_active && !double_quote_active { - save_buf(&mut buf, &mut tokens); - tokens.push(Tokens::ExportSet); + save_buf(&mut buf, &mut tokens, i); + tokens.push(Token { token: Tokens::ExportSet, start: i, end: i }); buf_add = false; }, '#' => if !escape_active && !quote_active && !double_quote_active { - save_buf(&mut buf, &mut tokens); + save_buf(&mut buf, &mut tokens, i); buf_add = false; let mut x = 0; while x + i + 1 < text.len() && text.chars().nth(x + i + 1).unwrap() != '\n' { @@ -251,7 +258,7 @@ pub fn tokenize(reader: &mut dyn std::io::BufRead) -> Result> { buf.push(*letter); } } - save_buf(&mut buf, &mut tokens); + save_buf(&mut buf, &mut tokens, text.len()); Ok(tokens) }