diff --git a/README.md b/README.md index 2c70dcdc0b04cbace0e5e0eabe3699073aa83e95..eada5d7a376885ac8895308d70f88a156c83e9ca 100644 --- a/README.md +++ b/README.md @@ -14,11 +14,12 @@ core functionality is complete. Features below: - [x] Variables (**$variable**) - [x] Functions - [x] Arrays (**@array**) -- [ ] Array Expressions (**[]**) -- [x] Array-based Command Substitution (**$()**) -- [x] String-based Command Substitution (**@[]**) +- [x] Array Expressions (**[]**) +- [x] Array-based Command Substitution (**@[]**) +- [x] String-based Command Substitution (**$()**) - [ ] String-to-Array Methods (**@split(var, ' ')**) - [ ] Array-to-String Methods (**$join(array, ', ')**) +- [ ] Array Splicing - [ ] Maps - [x] For Loops - [ ] Foreach Loops @@ -36,6 +37,7 @@ core functionality is complete. Features below: - [x] Multiline Comments and Commands - [ ] Multiline Editing - [x] Tab Completion (Needs Improvements) +- [ ] Syntax for Color Handling - [ ] Unescape specific character combinations, such as '\n' and '\t' - [ ] Builtin Plugins - [ ] Prompt Plugins @@ -147,7 +149,7 @@ echo ghi{one{a,b,c},two{d,e,f}} Arrays can be create with the let keyword when the supplied expression evaluates to a vector of values: -#### Array Syntax ( Not Implemented Yet ) +#### Array Syntax The basic syntax for creating an array of values is to wrap the values inbetween **[]** characters. The syntax within will be evaluated into a flat-mapped vector, and the result can therefor be stored as an array. @@ -156,6 +158,13 @@ will be evaluated into a flat-mapped vector, and the result can therefor be stor let array = [ one two 'three four' ] ``` +One particular use case for arrays is setting command arguments + +```ion +let lsflags = [ -l -a ] +ls @lsflags +``` + #### Braces Create Arrays Brace expansions actually create a vector of values under the hood, and thus they can be used to create an array. diff --git a/src/parser/arguments.rs b/src/parser/arguments.rs index e1e4fbed532610e19e36ced30a2eba517f8de20d..1d788182c68f4abb50a0794941528d622e394599 100644 --- a/src/parser/arguments.rs +++ b/src/parser/arguments.rs @@ -27,7 +27,7 @@ impl<'a> Iterator for ArgumentSplitter<'a> { type Item = String; fn next(&mut self) -> Option<String> { - let (mut level, mut array_level) = (0, 0); + let (mut level, mut array_level, mut array_process_level) = (0, 0, 0); for character in self.data.bytes().skip(self.read) { self.read += 1; match character { @@ -45,13 +45,16 @@ impl<'a> Iterator for ArgumentSplitter<'a> { self.buffer.push(character); continue }, - b'[' if self.flags & SINGLE == 0 && self.flags & COMM_2 != 0 => array_level += 1, - b']' if self.flags & SINGLE == 0 => array_level -= 1, - b'(' if self.flags & SINGLE == 0 && self.flags & COMM_2 != 0 => array_level += 1, + b'[' if self.flags & SINGLE == 0 && self.flags & COMM_2 != 0 => array_process_level += 1, + b'[' if self.flags & SINGLE == 0 => array_level += 1, + b']' if self.flags & SINGLE == 0 && array_level != 0 => array_level -= 1, + b']' if self.flags & SINGLE == 0 => array_process_level -= 1, + b'(' if self.flags & SINGLE == 0 && self.flags & COMM_1 != 0 => level += 1, b')' if self.flags & SINGLE == 0 => level -= 1, b'"' if self.flags & SINGLE == 0 => self.flags ^= DOUBLE, b'\'' if self.flags & DOUBLE == 0 => self.flags ^= SINGLE, - b' ' if !self.buffer.is_empty() && (self.flags & (SINGLE + DOUBLE) == 0) && level == 0 && array_level == 0 => break, + b' ' if !self.buffer.is_empty() && (self.flags & (SINGLE + DOUBLE) == 0) + && level == 0 && array_level == 0 && array_process_level == 0 => break, _ => () } self.buffer.push(character); diff --git a/src/parser/pipelines.rs b/src/parser/pipelines.rs index 4a62b2e6a8ad545fddeaa94b1839be9744a13714..9dbb15c69bec9196d246372c6fec1ac414b86219 100644 --- a/src/parser/pipelines.rs +++ b/src/parser/pipelines.rs @@ -1,6 +1,7 @@ #![allow(eq_op)] // Required as a macro sets this clippy warning off. // TODO: +// - Rewrite this module // - Implement Herestrings // - Implement Heredocs // - Fix the cyclomatic complexity issue @@ -8,12 +9,13 @@ use parser::peg::{Pipeline, Redirection, RedirectFrom}; use shell::{Job, JobKind}; -const BACKSLASH: u8 = 1; -const SINGLE_QUOTE: u8 = 2; -const DOUBLE_QUOTE: u8 = 4; -const WHITESPACE: u8 = 8; -const PROCESS_ONE: u8 = 64; -const PROCESS_TWO: u8 = 128; +const BACKSLASH: u8 = 1; +const SINGLE_QUOTE: u8 = 2; +const DOUBLE_QUOTE: u8 = 4; +const WHITESPACE: u8 = 8; +const ARRAY_PROCESS: u8 = 16; +const PROCESS_ONE: u8 = 64; +const PROCESS_TWO: u8 = 128; // Only valid if `SINGLE_QUOTE` and `DOUBLE_QUOTE` are not enabled const PROCESS_VAL: u8 = 255 ^ (BACKSLASH + WHITESPACE + 32); @@ -54,7 +56,7 @@ pub fn collect(possible_error: &mut Option<&str>, args: &str) -> Pipeline { let (mut in_file, mut out_file) = (None, None); let mut mode = RedirMode::False; - let mut levels = 0; + let (mut levels, mut array_levels, mut array_process_levels) = (0, 0, 0); macro_rules! redir_check { ($from:expr, $file:ident, $name:ident, $is_append:expr) => {{ @@ -112,9 +114,18 @@ pub fn collect(possible_error: &mut Option<&str>, args: &str) -> Pipeline { RedirMode::False => { while let Some(character) = args_iter.next() { match character { - _ if flags & BACKSLASH != 0 => flags ^= BACKSLASH, - b'\\' => flags ^= BACKSLASH, - b'$' if flags & PROCESS_VAL == 0 => flags |= PROCESS_ONE, + _ if flags & BACKSLASH != 0 => flags ^= BACKSLASH, + b'\\' => flags ^= BACKSLASH, + b'@' => { + flags |= ARRAY_PROCESS; + index += 1; + continue + }, + b'$' if flags & PROCESS_VAL == 0 => flags |= PROCESS_ONE, + b'[' if flags & ARRAY_PROCESS != 0 => array_process_levels += 1, + b'[' => array_levels += 1, + b']' if array_levels != 0 => array_levels -= 1, + b']' => array_process_levels -= 1, b'(' if flags & PROCESS_VAL == PROCESS_ONE => { flags ^= PROCESS_ONE; flags |= PROCESS_TWO; @@ -126,7 +137,9 @@ pub fn collect(possible_error: &mut Option<&str>, args: &str) -> Pipeline { }, b'\'' => flags ^= SINGLE_QUOTE, b'"' => flags ^= DOUBLE_QUOTE, - b' ' | b'\t' if (flags & IS_VALID == 0) => { + b' ' | b'\t' if (flags & IS_VALID == 0) && array_levels == 0 + && array_process_levels == 0 => + { if arg_start != index { arguments.push(args[arg_start..index].to_owned()); arg_start = index + 1; @@ -162,6 +175,7 @@ pub fn collect(possible_error: &mut Option<&str>, args: &str) -> Pipeline { _ if (flags >> 6 != 2) => flags &= 255 ^ (PROCESS_ONE + PROCESS_TWO), _ => (), } + flags &= 255 ^ ARRAY_PROCESS; index += 1; } break 'outer @@ -297,7 +311,7 @@ pub fn collect(possible_error: &mut Option<&str>, args: &str) -> Pipeline { } if arg_start != index { - arguments.push(args[arg_start..index].to_owned()); + arguments.push(args[arg_start..].to_owned()); } if !arguments.is_empty() { diff --git a/src/parser/shell_expand/mod.rs b/src/parser/shell_expand/mod.rs index 2d44113c0cc95fb145aabf111a7ea8a5f79f1195..2e0c6fcb5968d991305c08c9a98208e23abf257d 100644 --- a/src/parser/shell_expand/mod.rs +++ b/src/parser/shell_expand/mod.rs @@ -65,6 +65,11 @@ fn expand_brace(current: &mut String, expanders: &mut Vec<Vec<String>>, } } +fn expand_array(elements: &[&str], expand_func: &ExpanderFunctions) -> Vec<String> { + elements.iter().flat_map(|element| expand_string(element, expand_func, false)) + .collect() +} + #[allow(cyclomatic_complexity)] /// Performs shell expansions to an input string, efficiently returning the final expanded form. /// Shells must provide their own batteries for expanding tilde and variable words. @@ -87,6 +92,12 @@ pub fn expand_string(original: &str, expand_func: &ExpanderFunctions, reverse_qu for word in token_buffer.drain(..) { match word { + WordToken::Array(elements, index) => { + let expanded = match index { + Index::All => expand_array(&elements, expand_func) + }; + current.push_str(&expanded.join(" ")); + }, WordToken::ArrayVariable(array, _, index) => { if let Some(array) = (expand_func.array)(array, index) { current.push_str(&array.join(" ")); @@ -133,10 +144,15 @@ pub fn expand_string(original: &str, expand_func: &ExpanderFunctions, reverse_qu expanded_words.push(word); } } - + return expanded_words } else if token_buffer.len() == 1 { match token_buffer[0].clone() { + WordToken::Array(elements, index) => { + return match index { + Index::All => expand_array(&elements, expand_func) + }; + }, WordToken::ArrayVariable(array, quoted, index) => { return match (expand_func.array)(array, index) { Some(ref array) if quoted => vec![array.join(" ")], @@ -159,6 +175,12 @@ pub fn expand_string(original: &str, expand_func: &ExpanderFunctions, reverse_qu for word in token_buffer.drain(..) { match word { + WordToken::Array(elements, index) => { + let expanded = match index { + Index::All => expand_array(&elements, expand_func) + }; + output.push_str(&expanded.join(" ")); + }, WordToken::ArrayVariable(array, _, index) => { if let Some(array) = (expand_func.array)(array, index) { output.push_str(&array.join(" ")); diff --git a/src/parser/shell_expand/words.rs b/src/parser/shell_expand/words.rs index 60917c01a07476f6fa02a09c4e50d2b22469dce2..657ca6998aa35c2a73b3976b6d7aa878ae9427c3 100644 --- a/src/parser/shell_expand/words.rs +++ b/src/parser/shell_expand/words.rs @@ -22,7 +22,7 @@ pub enum WordToken<'a> { Whitespace(&'a str), Tilde(&'a str), Brace(Vec<&'a str>), - // Array(Vec<&str>, bool, Index) + Array(Vec<&'a str>, Index), Variable(&'a str, bool), ArrayVariable(&'a str, bool, Index), ArrayProcess(&'a str, bool, Index), @@ -131,7 +131,7 @@ impl<'a> WordIterator<'a> { // TODO: ArrayFunction // Only alphanumerical and underscores are allowed in variable names 0...47 | 58...64 | 91...94 | 96 | 123...127 => { - return WordToken::Variable(&self.data[start..self.read], self.flags & DQUOTE != 0); + return WordToken::ArrayVariable(&self.data[start..self.read], self.flags & DQUOTE != 0, Index::All); }, _ => (), } @@ -247,6 +247,51 @@ impl<'a> WordIterator<'a> { panic!("ion: fatal error with syntax validation: unterminated brace") } + + /// Contains the grammar for parsing array expression syntax + fn array<I>(&mut self, iterator: &mut I) -> WordToken<'a> + where I: Iterator<Item = u8> + { + let mut start = self.read; + let mut level = 0; + let mut whitespace = false; + let mut elements = Vec::new(); + while let Some(character) = iterator.next() { + match character { + _ if self.flags & BACKSL != 0 => self.flags ^= BACKSL, + b'\\' => self.flags ^= BACKSL, + b'\'' if self.flags & DQUOTE == 0 => self.flags ^= SQUOTE, + b'"' if self.flags & SQUOTE == 0 => self.flags ^= DQUOTE, + b' ' if self.flags & (SQUOTE + DQUOTE) == 0 && level == 0 => { + if whitespace { + self.read += 1; + start = self.read; + } else { + elements.push(&self.data[start..self.read]); + start = self.read + 1; + self.read += 1; + whitespace = true; + } + continue + }, + b'[' if self.flags & (SQUOTE + DQUOTE) == 0 => level += 1, + b']' if self.flags & (SQUOTE + DQUOTE) == 0 => { + if level == 0 { + elements.push(&self.data[start..self.read]); + self.read += 1; + return WordToken::Array(elements, Index::All); + } else { + level -= 1; + } + + }, + _ => whitespace = false + } + self.read += 1; + } + + panic!("ion: fatal error with syntax validation: unterminated array expression") + } } impl<'a> Iterator for WordIterator<'a> { @@ -282,6 +327,10 @@ impl<'a> Iterator for WordIterator<'a> { self.read += 1; return Some(self.braces(&mut iterator)); }, + b'[' if self.flags & SQUOTE == 0 => { + self.read += 1; + return Some(self.array(&mut iterator)); + }, b'@' if self.flags & SQUOTE == 0 => { match iterator.next() { Some(b'[') => { @@ -340,7 +389,7 @@ impl<'a> Iterator for WordIterator<'a> { b' ' | b'{' if self.flags & (SQUOTE + DQUOTE) == 0 => { return Some(WordToken::Normal(&self.data[start..self.read])); }, - b'$' | b'@' if self.flags & SQUOTE == 0 => { + b'$' | b'@' | b'[' if self.flags & SQUOTE == 0 => { return Some(WordToken::Normal(&self.data[start..self.read])); }, _ => (), diff --git a/src/parser/statements.rs b/src/parser/statements.rs index ddde84b0ccecf16584c77511087b0cccf28665ef..599e3e967b4ccc375f095216e79a2046188c7327 100644 --- a/src/parser/statements.rs +++ b/src/parser/statements.rs @@ -47,6 +47,7 @@ pub struct StatementSplitter<'a> { data: &'a str, read: usize, flags: u8, + array_level: u8, array_process_level: u8, process_level: u8, brace_level: u8, @@ -58,6 +59,7 @@ impl<'a> StatementSplitter<'a> { data: data, read: 0, flags: 0, + array_level: 0, array_process_level: 0, process_level: 0, brace_level: 0 @@ -112,11 +114,13 @@ impl<'a> Iterator for StatementSplitter<'a> { b'[' if self.flags & COMM_2 != 0 && self.flags & SQUOTE == 0 => { self.array_process_level += 1; }, - b']' if self.array_process_level == 0 && self.flags & SQUOTE == 0 => { + b'[' if self.flags & SQUOTE == 0 => self.array_level += 1, + b']' if self.array_process_level == 0 && self.array_level == 0 && self.flags & SQUOTE == 0 => { if error.is_none() { error = Some(StatementError::InvalidCharacter(character as char, self.read)) } }, + b']' if self.flags & SQUOTE == 0 && self.array_level != 0 => self.array_level -= 1, b']' if self.flags & SQUOTE == 0 => self.array_process_level -= 1, b'(' if self.flags & COMM_1 != 0 && self.flags & SQUOTE == 0 => { self.process_level += 1; @@ -152,7 +156,9 @@ impl<'a> Iterator for StatementSplitter<'a> { self.read = self.data.len(); match error { Some(error) => Some(Err(error)), - None if self.process_level != 0 || self.array_process_level != 0 => { + None if self.process_level != 0 || self.array_process_level != 0 || + self.array_level != 0 => + { Some(Err(StatementError::UnterminatedSubshell)) }, None if self.flags & VBRACE != 0 => Some(Err(StatementError::UnterminatedBracedVar)), diff --git a/src/shell/flow.rs b/src/shell/flow.rs index a7831dfb1754e243d29106d1dd39e0a74c3cdf13..f0e92dcc4e20cae2d92c3540dbf98a46456ee7ed 100644 --- a/src/shell/flow.rs +++ b/src/shell/flow.rs @@ -11,13 +11,24 @@ use super::assignments::let_assignment; use glob::glob; pub trait FlowLogic { + /// Receives a command and attempts to execute the contents. fn on_command(&mut self, command_string: &str); + + /// The highest layer of the flow control handling which branches into lower blocks when found. fn execute_toplevel<I>(&mut self, iterator: &mut I, statement: Statement) -> Result<(), &'static str> where I: Iterator<Item = Statement>; + + /// Executes all of the statements within a while block until a certain condition is met. fn execute_while(&mut self, expression: Pipeline, statements: Vec<Statement>); + + /// Executes all of the statements within a for block for each value specified in the range. fn execute_for(&mut self, variable: &str, values: &[String], statements: Vec<Statement>); + + /// Conditionally executes branches of statements according to evaluated expressions fn execute_if(&mut self, expression: Pipeline, success: Vec<Statement>, else_if: Vec<ElseIf>, failure: Vec<Statement>) -> bool; + + /// Simply executes all supplied statemnts. fn execute_statements(&mut self, statements: Vec<Statement>) -> bool; } @@ -265,7 +276,7 @@ impl<'a> FlowLogic for Shell<'a> { Statement::For { variable, values, mut statements } => { self.flow_control.level += 1; - // Collect all of the statements contained within the while block. + // Collect all of the statements contained within the for block. collect_loops(iterator, &mut statements, &mut self.flow_control.level); if self.flow_control.level == 0 { diff --git a/src/variables.rs b/src/variables.rs index 7b8f4c4882e10aadca6c1e032fe23f3f99c66ed9..f41e4671c41643324102b1429170389de1631ed1 100644 --- a/src/variables.rs +++ b/src/variables.rs @@ -1,3 +1,4 @@ +// TODO: Move into shell module use std::collections::HashMap; use std::env; use std::path::PathBuf; @@ -20,7 +21,9 @@ impl Default for Variables { map.insert("HISTORY_SIZE".into(), "1000".into()); map.insert("HISTORY_FILE_ENABLED".into(), "0".into()); map.insert("HISTORY_FILE_SIZE".into(), "1000".into()); - map.insert("PROMPT".into(), "\x1B]0;${USER}: ${PWD}\x07\x1B[0m\x1B[1;38;5;85m${USER}\x1B[37m:\x1B[38;5;75m${PWD}\x1B[37m#\x1B[0m ".into()); + // TODO: Handle Colors + // map.insert("PROMPT".into(), "\x1B]0;${USER}: ${PWD}\x07\x1B[0m\x1B[1;38;5;85m${USER}\x1B[37m:\x1B[38;5;75m${PWD}\x1B[37m#\x1B[0m ".into()); + map.insert("PROMPT".into(), "${USER}:${PWD}# ".into()); // Initialize the HISTORY_FILE variable env::home_dir().map(|mut home_path: PathBuf| {