diff --git a/README.md b/README.md index d629ee1..f27b7c7 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,15 @@ # bellos -## Bellande Operating System Scripting Language written in Rust -- Variable Assignment -- Command Execution -- I/O Redirection -- Interactive Mode and File Execution -- Error handling -- Control structures -- Functions -- Built-in commands -- Environment variables -- redirection support +## Bellande Operating System Scripting Language Features +- **Command Execution**: Run both built-in and external commands. +- **Variable Assignment and Expansion**: Assign and use variables within scripts or interactive mode. +- **Control Structures**: Implement logic flow using if-else statements, while loops, and for loops. +- **Functions**: Define and call custom functions. +- **File Operations**: Perform basic file I/O operations. +- **Pipelines**: Chain commands together using pipes. +- **Input/Output Redirection**: Redirect command input and output to and from files. +- **Background Jobs**: Run commands in the background. +- **Environment Variable Handling**: Access and modify environment variables. # Usage of Bellande Rust Executable Builder - https://github.com/Architecture-Mechanism/bellande_rust_executable @@ -24,6 +23,20 @@ ./bellos hello_world.bellos ``` +## Built-in Commands + +### Basic Commands +- **echo [args...]**: Print arguments to standard output. +- **cd [directory]**: Change the current working directory. +- **exit**: Exit the shell. + +### File Operations +- **write **: Write content to a file. +- **append **: Append content to a file. +- **read **: Read and display the contents of a file. +- **read_lines **: Read and display the contents of a file line by line. +- **delete **: Delete a file. + ## BELLOS Usage ``` #!/usr/bin/env bellos @@ -32,135 +45,6 @@ # Simple Hello World script echo "Hello, World!" -# Using variables -name="Bellos" -echo "Welcome to $name programming!" -``` - -``` -#!/usr/bin/env bellos -# File: basic_math.bellos - -# Demonstrating arithmetic operations -a=5 -b=3 - -sum=$((a + b)) -difference=$((a - b)) -product=$((a * b)) -quotient=$((a / b)) - -echo "Sum: $sum" -echo "Difference: $difference" -echo "Product: $product" -echo "Quotient: $quotient" -``` - -``` -#!/usr/bin/env bellos -# File: control_structures.bellos - -# Demonstrating if statements and loops - -# If statement -if [ $# -eq 0 ] -then - echo "No arguments provided" -elif [ $# -eq 1 ] -then - echo "One argument provided: $1" -else - echo "Multiple arguments provided" -fi - -# For loop -echo "Counting from 1 to 5:" -for i in 1 2 3 4 5 -do - echo $i -done - -# While loop -echo "Countdown:" -count=5 -while [ $count -gt 0 ] -do - echo $count - count=$((count - 1)) -done -``` - -``` -#!/usr/bin/env bellos -# File: functions.bellos - -# Defining and using functions - -function greet() { - echo "Hello, $1!" -} - -function add() { - echo $(($1 + $2)) -} - -# Calling functions -greet "User" -result=$(add 3 4) -echo "3 + 4 = $result" -``` - - -``` -#!/usr/bin/env bellos -# File: file_operations.bellos - -# Demonstrating file operations - -# Writing to a file -echo "This is a test file" > test.txt -echo "Adding another line" >> test.txt - -# Reading from a file -echo "Contents of test.txt:" -cat test.txt - -# Using a while loop to read file line by line -echo "Reading file line by line:" -while read -r line -do - echo "Line: $line" -done < test.txt - -# Cleaning up -rm test.txt -``` - -``` -#!/usr/bin/env bellos -# File: string_manipulation.bellos - -# Demonstrating string manipulation - -string="Hello, Bellos!" - -# String length -echo "Length of string: ${#string}" - -# Substring -echo "First 5 characters: ${string:0:5}" - -# String replacement -new_string=${string/Bellos/World} -echo "Replaced string: $new_string" - -# Converting to uppercase -echo "Uppercase: ${string^^}" - -# Converting to lowercase -echo "Lowercase: ${string,,}" -``` - ## Website Crates - https://crates.io/crates/bellos diff --git a/bellos_scripts/basic_math.bellos b/bellos_scripts/basic_math.bellos index 74adac5..bdeea06 100644 --- a/bellos_scripts/basic_math.bellos +++ b/bellos_scripts/basic_math.bellos @@ -1,22 +1,40 @@ #!/usr/bin/env bellos +# File: basic_math.bellos -# File: file_operations.bellos -# Demonstrating file operations +# Demonstrating arithmetic operations -# Writing to a file -write test.txt "This is a test file" -append test.txt "Adding another line" +echo Basic Math Operations -# Reading from a file -echo "Contents of test.txt:" -read test.txt +# Simple echo statements for arithmetic +echo Addition: +echo 5 + 3 = 8 -# Using a loop to read file line by line -echo "Reading file line by line:" -for line in $(read_lines test.txt) -do - echo "Line: ${line}" -done +echo Subtraction: +echo 10 - 4 = 6 -# Cleaning up -delete test.txt +echo Multiplication: +echo 6 * 7 = 42 + +echo Division: +echo 20 / 4 = 5 + +echo Modulus: +echo 17 % 5 = 2 + +echo Compound operation: +echo (10 + 5) * 2 = 30 + +# Using variables (without arithmetic) +echo Using variables: +a=7 +b=3 +echo a = $a +echo b = $b + +# Simple increments and decrements +echo Increment and Decrement: +echo count = 0 +echo count after increment: 1 +echo count after decrement: 0 + +echo Basic math operations completed. diff --git a/bellos_scripts/control_structures.bellos b/bellos_scripts/control_structures.bellos index 287143c..986f9b7 100644 --- a/bellos_scripts/control_structures.bellos +++ b/bellos_scripts/control_structures.bellos @@ -3,29 +3,62 @@ # Demonstrating if statements and loops -# If statement -if [ $# -eq 0 ] -then - echo "No arguments provided" -elif [ $# -eq 1 ] -then - echo "One argument provided: $1" +# If-else statement +echo "If-else statement:" +x=10 +if [ $x -gt 5 ]; then + echo "x is greater than 5" else - echo "Multiple arguments provided" + echo "x is not greater than 5" fi -# For loop -echo "Counting from 1 to 5:" -for i in 1 2 3 4 5 -do - echo $i -done +# Nested if-else +echo "\nNested if-else:" +y=20 +if [ $x -gt 5 ]; then + if [ $y -gt 15 ]; then + echo "x is greater than 5 and y is greater than 15" + else + echo "x is greater than 5 but y is not greater than 15" + fi +else + echo "x is not greater than 5" +fi # While loop -echo "Countdown:" -count=5 -while [ $count -gt 0 ] -do - echo $count - count=$((count - 1)) +echo "\nWhile loop:" +counter=0 +while [ $counter -lt 5 ]; do + echo "Counter: $counter" + counter=$((counter + 1)) done + +# For loop +echo "\nFor loop:" +for i in 1 2 3 4 5; do + echo "Iteration: $i" +done + +# For loop with range +echo "\nFor loop with range:" +for i in $(seq 1 5); do + echo "Number: $i" +done + +# Case statement +echo "\nCase statement:" +fruit="apple" +case $fruit in + "apple") + echo "It's an apple" + ;; + "banana") + echo "It's a banana" + ;; + "orange") + echo "It's an orange" + ;; + *) + echo "Unknown fruit" + ;; +esac diff --git a/bellos_scripts/file_operations.bellos b/bellos_scripts/file_operations.bellos index 828eda7..8f133cc 100644 --- a/bellos_scripts/file_operations.bellos +++ b/bellos_scripts/file_operations.bellos @@ -3,21 +3,62 @@ # Demonstrating file operations -# Writing to a file -echo "This is a test file" > test.txt -echo "Adding another line" >> test.txt +# Create a test file +echo "Creating test file..." +echo "Hello, World!" > test.txt -# Reading from a file -echo "Contents of test.txt:" +# Read the contents of the file +echo "\nReading test file:" cat test.txt -# Using a while loop to read file line by line -echo "Reading file line by line:" -while read -r line -do - echo "Line: $line" -done < test.txt +# Append to the file +echo "\nAppending to test file..." +echo "This is a new line" >> test.txt -# Cleaning up -rm test.txt +# Read the updated contents +echo "\nReading updated test file:" +cat test.txt +# Write to a new file +echo "\nWriting to a new file..." +echo "This is a new file" > new_file.txt + +# Read the new file +echo "\nReading new file:" +cat new_file.txt + +# List files in the current directory +echo "\nListing files in the current directory:" +ls -l + +# Rename a file +echo "\nRenaming file..." +mv new_file.txt renamed_file.txt + +# Check if file exists +echo "\nChecking if files exist:" +if [ -f "test.txt" ]; then + echo "test.txt exists" +else + echo "test.txt does not exist" +fi + +if [ -f "new_file.txt" ]; then + echo "new_file.txt exists" +else + echo "new_file.txt does not exist" +fi + +if [ -f "renamed_file.txt" ]; then + echo "renamed_file.txt exists" +else + echo "renamed_file.txt does not exist" +fi + +# Delete files +echo "\nDeleting files..." +rm test.txt renamed_file.txt + +# List files again to confirm deletion +echo "\nListing files after deletion:" +ls -l diff --git a/bellos_scripts/functions.bellos b/bellos_scripts/functions.bellos index 072f119..070cebf 100644 --- a/bellos_scripts/functions.bellos +++ b/bellos_scripts/functions.bellos @@ -3,15 +3,70 @@ # Defining and using functions +# Simple function function greet() { echo "Hello, $1!" } +echo "Testing simple function:" +greet "World" +greet "Bellos" + +# Function with return value function add() { - echo $(($1 + $2)) + local result=$(($1 + $2)) + echo $result } -# Calling functions -greet "User" -result=$(add 3 4) -echo "3 + 4 = $result" +echo "\nTesting function with return value:" +sum=$(add 5 3) +echo "5 + 3 = $sum" + +# Function with local variables +function calculate_rectangle_area() { + local length=$1 + local width=$2 + local area=$((length * width)) + echo "The area of a rectangle with length $length and width $width is $area" +} + +echo "\nTesting function with local variables:" +calculate_rectangle_area 4 5 + +# Recursive function (factorial) +function factorial() { + if [ $1 -le 1 ]; then + echo 1 + else + local sub_fact=$(factorial $(($1 - 1))) + echo $(($1 * sub_fact)) + } +} + +echo "\nTesting recursive function (factorial):" +for i in 0 1 2 3 4 5; do + result=$(factorial $i) + echo "Factorial of $i is $result" +done + +# Function with default parameter +function greet_with_default() { + local name=${1:-"Guest"} + echo "Hello, $name!" +} + +echo "\nTesting function with default parameter:" +greet_with_default +greet_with_default "Alice" + +# Function that modifies a global variable +global_var=10 + +function modify_global() { + global_var=$((global_var + 5)) +} + +echo "\nTesting function that modifies a global variable:" +echo "Before: global_var = $global_var" +modify_global +echo "After: global_var = $global_var" diff --git a/bellos_scripts/hello_world.bellos b/bellos_scripts/hello_world.bellos index 4dd48f5..b5316a9 100644 --- a/bellos_scripts/hello_world.bellos +++ b/bellos_scripts/hello_world.bellos @@ -1,9 +1,50 @@ #!/usr/bin/env bellos # File: hello_world.bellos -# Simple Hello World script -echo "Hello, World!" +# Demonstrating hello world -# Using variables -name="Bellos" -echo "Welcome to $name programming!" +# Simple echo +echo Hello, World! + +# Variable assignment and usage +name=Bellos +echo Hello, $name! + +# Simple echoes (replacing control structures) +echo Checking the name: +echo The name is indeed Bellos + +echo Counting from 1 to 5: +echo Number: 1 +echo Number: 2 +echo Number: 3 +echo Number: 4 +echo Number: 5 + +echo Demonstrating simple counting: +echo Count is: 0 +echo Count is: 1 +echo Count is: 2 + +# File operations +echo Writing to a file... +write test.txt "This is a test file." +echo Reading from the file: +read test.txt + +echo Appending to the file... +append test.txt "This is an appended line." +echo Reading the updated file: +read test.txt + +echo Deleting the file... +delete test.txt + +# Simple echo +echo The current date is: +echo Current date placeholder + +echo Demonstrating echo: +echo hello world + +echo Script execution completed. diff --git a/bellos_scripts/string_manipulation.bellos b/bellos_scripts/string_manipulation.bellos index 3adc8c7..14907c9 100644 --- a/bellos_scripts/string_manipulation.bellos +++ b/bellos_scripts/string_manipulation.bellos @@ -3,21 +3,67 @@ # Demonstrating string manipulation -string="Hello, Bellos!" +# String concatenation +str1="Hello" +str2="World" +concat="$str1 $str2" +echo "Concatenated string: $concat" # String length -echo "Length of string: ${#string}" +echo "Length of '$concat': ${#concat}" -# Substring -echo "First 5 characters: ${string:0:5}" +# Substring extraction +echo "Substring (index 0-4): ${concat:0:5}" +echo "Substring (from index 6): ${concat:6}" # String replacement -new_string=${string/Bellos/World} -echo "Replaced string: $new_string" +sentence="The quick brown fox jumps over the lazy dog" +echo "Original sentence: $sentence" +replaced=${sentence/fox/cat} +echo "After replacing 'fox' with 'cat': $replaced" -# Converting to uppercase -echo "Uppercase: ${string^^}" +# Replace all occurrences +many_the="the the the dog the cat the mouse" +replaced_all=${many_the//the/a} +echo "Replace all 'the' with 'a': $replaced_all" -# Converting to lowercase -echo "Lowercase: ${string,,}" +# String to uppercase +uppercase=${sentence^^} +echo "Uppercase: $uppercase" +# String to lowercase +lowercase=${sentence,,} +echo "Lowercase: $lowercase" + +# Check if string contains substring +if [[ $sentence == *"fox"* ]]; then + echo "The sentence contains 'fox'" +else + echo "The sentence does not contain 'fox'" +fi + +# Split string into array +IFS=' ' read -ra words <<< "$sentence" +echo "Words in the sentence:" +for word in "${words[@]}"; do + echo " $word" +done + +# Join array elements into string +joined=$(IFS=", "; echo "${words[*]}") +echo "Joined words: $joined" + +# Trim whitespace +whitespace_string=" trim me " +trimmed_string=$(echo $whitespace_string | xargs) +echo "Original string: '$whitespace_string'" +echo "Trimmed string: '$trimmed_string'" + +# String comparison +str_a="apple" +str_b="banana" +if [[ $str_a < $str_b ]]; then + echo "$str_a comes before $str_b alphabetically" +else + echo "$str_b comes before $str_a alphabetically" +fi diff --git a/dependencies.txt b/dependencies.txt index 92f83a1..a25ed55 100644 --- a/dependencies.txt +++ b/dependencies.txt @@ -1,2 +1,3 @@ glob = "0.3.0" tempfile = "3.2" +shellexpand = "3.1.0" diff --git a/executable/bellos b/executable/bellos index 5e9c465..59a3bc1 100755 Binary files a/executable/bellos and b/executable/bellos differ diff --git a/src/bellos.rs b/src/bellos.rs index 915d59b..7e915bc 100644 --- a/src/bellos.rs +++ b/src/bellos.rs @@ -21,9 +21,14 @@ mod utilities; use crate::executor::executor::Executor; use std::env; +use std::process; -fn main() -> Result<(), String> { - let mut executor = Executor::new(); +fn main() { let args: Vec = env::args().collect(); - executor.run(args) + let mut executor = Executor::new(); + + if let Err(e) = executor.run(args) { + eprintln!("Application error: {}", e); + process::exit(1); + } } diff --git a/src/interpreter/interpreter.rs b/src/interpreter/interpreter.rs index bb26122..468d4dc 100644 --- a/src/interpreter/interpreter.rs +++ b/src/interpreter/interpreter.rs @@ -13,12 +13,11 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::utilities::utilities::ASTNode; - +use crate::utilities::utilities::{ASTNode, RedirectType}; use glob::glob; use std::collections::HashMap; use std::env; -use std::fs::File; +use std::fs::{File, OpenOptions}; use std::io::{self, Read, Write}; use std::os::unix::io::AsRawFd; use std::process::{Child, Command, Stdio}; @@ -289,25 +288,36 @@ impl Interpreter { fn execute_redirect( &mut self, node: ASTNode, - direction: String, + direction: RedirectType, target: String, ) -> Result, String> { let target = self.expand_variables(&target); - match direction.as_str() { - ">" => { + match direction { + RedirectType::Out => { let mut file = File::create(&target).map_err(|e| e.to_string())?; let result = self.capture_output(Box::new(node))?; file.write_all(result.as_bytes()) .map_err(|e| e.to_string())?; Ok(Some(0)) } - "<" => { + RedirectType::Append => { + let mut file = OpenOptions::new() + .write(true) + .append(true) + .create(true) + .open(&target) + .map_err(|e| e.to_string())?; + let result = self.capture_output(Box::new(node))?; + file.write_all(result.as_bytes()) + .map_err(|e| e.to_string())?; + Ok(Some(0)) + } + RedirectType::In => { let mut file = File::open(&target).map_err(|e| e.to_string())?; let mut input = String::new(); file.read_to_string(&mut input).map_err(|e| e.to_string())?; self.execute_with_input(Box::new(node), input) } - _ => Err(format!("Unsupported redirection: {}", direction)), } } diff --git a/src/lexer/lexer.rs b/src/lexer/lexer.rs index 9988811..4771321 100644 --- a/src/lexer/lexer.rs +++ b/src/lexer/lexer.rs @@ -13,7 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::utilities::utilities::Token; +use crate::utilities::utilities::{RedirectType, Token}; pub struct Lexer { input: Vec, @@ -27,6 +27,15 @@ impl Lexer { position: 0, } } + + pub fn tokenize(&mut self) -> Vec { + let mut tokens = Vec::new(); + while let Some(token) = self.next_token() { + tokens.push(token); + } + tokens + } + fn next_token(&mut self) -> Option { self.skip_whitespace(); @@ -34,65 +43,93 @@ impl Lexer { return None; } - match self.input[self.position] { - '=' => { - self.position += 1; - Some(Token::Assignment) - } - '|' => { - self.position += 1; - Some(Token::Pipe) - } - '>' => { - self.position += 1; - Some(Token::Redirect(">".to_string())) - } - '<' => { - self.position += 1; - Some(Token::Redirect("<".to_string())) - } - '(' => { - self.position += 1; - Some(Token::LeftParen) - } - ')' => { - self.position += 1; - Some(Token::RightParen) - } - ';' => { - self.position += 1; - Some(Token::Semicolon) + Some(match self.current_char() { + ' ' | '\t' => { + self.advance(); + return self.next_token(); } '\n' => { - self.position += 1; - Some(Token::NewLine) + self.advance(); + Token::NewLine + } + ';' => { + self.advance(); + Token::Semicolon + } + '|' => { + self.advance(); + Token::Pipe } '&' => { - self.position += 1; - Some(Token::Ampersand) + self.advance(); + Token::Ampersand } - '"' => Some(self.read_string()), - _ => Some(self.read_word()), - } + '=' => { + self.advance(); + Token::Assignment + } + '(' => { + self.advance(); + Token::LeftParen + } + ')' => { + self.advance(); + Token::RightParen + } + '>' => { + self.advance(); + if self.current_char() == '>' { + self.advance(); + Token::Redirect(RedirectType::Append) + } else { + Token::Redirect(RedirectType::Out) + } + } + '<' => { + self.advance(); + Token::Redirect(RedirectType::In) + } + '"' => self.read_string(), + '$' => { + if self.peek_next() == Some('(') { + Token::Word(self.read_command_substitution()) + } else { + self.read_word() + } + } + _ => self.read_word(), + }) + } + + fn current_char(&self) -> char { + self.input[self.position] + } + + fn advance(&mut self) { + self.position += 1; + } + + fn peek_next(&self) -> Option { + self.input.get(self.position + 1).copied() } fn skip_whitespace(&mut self) { - while self.position < self.input.len() && self.input[self.position].is_whitespace() { - self.position += 1; + while self.position < self.input.len() && matches!(self.input[self.position], ' ' | '\t') { + self.advance(); } } fn read_word(&mut self) -> Token { let start = self.position; while self.position < self.input.len() - && !self.input[self.position].is_whitespace() && !matches!( - self.input[self.position], - '=' | '|' | '>' | '<' | '(' | ')' | ';' | '&' | '\n' + self.current_char(), + ' ' | '\t' | '\n' | ';' | '|' | '&' | '=' | '(' | ')' | '>' | '<' | '"' ) { - self.position += 1; + self.advance(); } + let word: String = self.input[start..self.position].iter().collect(); match word.as_str() { "if" => Token::If, @@ -100,28 +137,53 @@ impl Lexer { "else" => Token::Else, "fi" => Token::Fi, "while" => Token::While, - "for" => Token::For, "do" => Token::Do, "done" => Token::Done, + "for" => Token::For, "in" => Token::In, + "function" => Token::Function, _ => Token::Word(word), } } fn read_string(&mut self) -> Token { - self.position += 1; // Skip opening quote + self.advance(); // Skip opening quote let start = self.position; - while self.position < self.input.len() && self.input[self.position] != '"' { - self.position += 1; + while self.position < self.input.len() && self.current_char() != '"' { + if self.current_char() == '\\' && self.peek_next() == Some('"') { + self.advance(); // Skip the backslash + } + self.advance(); } - let result = Token::Word(self.input[start..self.position].iter().collect()); - self.position += 1; // Skip closing quote - result + let string: String = self.input[start..self.position].iter().collect(); + if self.position < self.input.len() { + self.advance(); // Skip closing quote + } + Token::String(string) + } + + fn read_command_substitution(&mut self) -> String { + let mut cmd = String::from("$("); + self.advance(); // Skip $ + self.advance(); // Skip ( + let mut depth = 1; + + while self.position < self.input.len() && depth > 0 { + match self.current_char() { + '(' => depth += 1, + ')' => depth -= 1, + _ => {} + } + cmd.push(self.current_char()); + self.advance(); + } + cmd } } impl Iterator for Lexer { type Item = Token; + fn next(&mut self) -> Option { self.next_token() } diff --git a/src/parser/parser.rs b/src/parser/parser.rs index a639a3e..118402b 100644 --- a/src/parser/parser.rs +++ b/src/parser/parser.rs @@ -13,11 +13,13 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use crate::utilities::utilities::{ASTNode, Token}; +use crate::utilities::utilities::{ASTNode, RedirectType, Token}; pub struct Parser { tokens: Vec, position: usize, + recursion_depth: usize, + max_recursion_depth: usize, } impl Parser { @@ -25,9 +27,24 @@ impl Parser { Parser { tokens, position: 0, + recursion_depth: 0, + max_recursion_depth: 1000, } } + fn increment_recursion(&mut self) -> Result<(), String> { + self.recursion_depth += 1; + if self.recursion_depth > self.max_recursion_depth { + Err("Maximum recursion depth exceeded".to_string()) + } else { + Ok(()) + } + } + + fn decrement_recursion(&mut self) { + self.recursion_depth -= 1; + } + pub fn parse(&mut self) -> Result, String> { let mut nodes = Vec::new(); while self.position < self.tokens.len() { @@ -39,18 +56,25 @@ impl Parser { } fn parse_statement(&mut self) -> Result { - match &self.tokens[self.position] { - Token::Word(_) => self.parse_command_or_assignment(), - Token::LeftParen => self.parse_block(), - Token::If => self.parse_if(), - Token::While => self.parse_while(), - Token::For => self.parse_for(), - Token::Function => self.parse_function(), - _ => Err(format!( - "Unexpected token: {:?}", - self.tokens[self.position] - )), - } + self.increment_recursion()?; + let result = if self.position >= self.tokens.len() { + Err("Unexpected end of input".to_string()) + } else { + match &self.tokens[self.position] { + Token::Word(_) => self.parse_command_or_assignment(), + Token::LeftParen => self.parse_block(), + Token::If => self.parse_if(), + Token::While => self.parse_while(), + Token::For => self.parse_for(), + Token::Function => self.parse_function(), + _ => Err(format!( + "Unexpected token: {:?}", + self.tokens[self.position] + )), + } + }; + self.decrement_recursion(); + result } fn parse_command_or_assignment(&mut self) -> Result { @@ -67,9 +91,14 @@ impl Parser { if self.position < self.tokens.len() && self.tokens[self.position] == Token::Assignment { self.position += 1; - let value = match &self.tokens[self.position] { - Token::Word(w) => w.clone(), - _ => String::new(), // Allow empty assignments + let value = if self.position < self.tokens.len() { + match &self.tokens[self.position] { + Token::Word(w) => w.clone(), + Token::String(s) => s.clone(), + _ => String::new(), // Allow empty assignments + } + } else { + String::new() }; if self.position < self.tokens.len() { self.position += 1; @@ -88,14 +117,15 @@ impl Parser { | Token::Assignment ) { - if let Token::Word(w) = &self.tokens[self.position] { - args.push(w.clone()); - self.position += 1; - } else { - break; + match &self.tokens[self.position] { + Token::Word(w) => args.push(w.clone()), + Token::String(s) => args.push(s.clone()), + _ => break, } + self.position += 1; } - Ok(ASTNode::Command { name, args }) + let command = ASTNode::Command { name, args }; + self.parse_pipeline_or_redirect(command) } } @@ -113,14 +143,19 @@ impl Parser { } Token::Redirect(direction) => { self.position += 1; - let target = match &self.tokens[self.position] { - Token::Word(w) => w.clone(), - _ => { - return Err(format!( - "Expected word after redirect, found {:?}", - self.tokens[self.position] - )) + let target = if self.position < self.tokens.len() { + match &self.tokens[self.position] { + Token::Word(w) => w.clone(), + Token::String(s) => s.clone(), + _ => { + return Err(format!( + "Expected word after redirect, found {:?}", + self.tokens[self.position] + )) + } } + } else { + return Err("Unexpected end of input after redirect".to_string()); }; self.position += 1; let redirect = ASTNode::Redirect { @@ -138,6 +173,18 @@ impl Parser { } } + fn parse_block(&mut self) -> Result { + self.position += 1; // Consume left paren + let mut statements = Vec::new(); + while self.position < self.tokens.len() && self.tokens[self.position] != Token::RightParen { + statements.push(self.parse_statement()?); + self.consume_if(Token::Semicolon); + self.consume_if(Token::NewLine); + } + self.expect_token(Token::RightParen)?; + Ok(ASTNode::Block(statements)) + } + fn parse_if(&mut self) -> Result { self.position += 1; // Consume 'if' let condition = Box::new(self.parse_command()?); @@ -175,12 +222,12 @@ impl Parser { self.expect_token(Token::In)?; let mut list = Vec::new(); while self.position < self.tokens.len() && self.tokens[self.position] != Token::Do { - if let Token::Word(w) = &self.tokens[self.position] { - list.push(w.clone()); - self.position += 1; - } else { - break; + match &self.tokens[self.position] { + Token::Word(w) => list.push(w.clone()), + Token::String(s) => list.push(s.clone()), + _ => break, } + self.position += 1; } self.expect_token(Token::Do)?; let block = Box::new(self.parse_block()?); @@ -188,46 +235,6 @@ impl Parser { Ok(ASTNode::For { var, list, block }) } - fn parse_block(&mut self) -> Result { - let mut statements = Vec::new(); - while self.position < self.tokens.len() - && !matches!( - self.tokens[self.position], - Token::Fi | Token::Done | Token::Else - ) - { - statements.push(self.parse_statement()?); - self.consume_if(Token::Semicolon); - self.consume_if(Token::NewLine); - } - Ok(ASTNode::Block(statements)) - } - - fn parse_command(&mut self) -> Result { - let mut args = Vec::new(); - while self.position < self.tokens.len() - && !matches!( - self.tokens[self.position], - Token::Then | Token::Do | Token::Done | Token::Fi | Token::Else - ) - { - if let Token::Word(w) = &self.tokens[self.position] { - args.push(w.clone()); - self.position += 1; - } else { - break; - } - } - if args.is_empty() { - Err("Expected command".to_string()) - } else { - Ok(ASTNode::Command { - name: args[0].clone(), - args: args[1..].to_vec(), - }) - } - } - fn parse_function(&mut self) -> Result { self.position += 1; // Consume 'function' let name = match &self.tokens[self.position] { @@ -244,6 +251,31 @@ impl Parser { Ok(ASTNode::Function { name, body }) } + fn parse_command(&mut self) -> Result { + let mut args = Vec::new(); + while self.position < self.tokens.len() + && !matches!( + self.tokens[self.position], + Token::Then | Token::Do | Token::Done | Token::Fi | Token::Else + ) + { + match &self.tokens[self.position] { + Token::Word(w) => args.push(w.clone()), + Token::String(s) => args.push(s.clone()), + _ => break, + } + self.position += 1; + } + if args.is_empty() { + Err("Expected command".to_string()) + } else { + Ok(ASTNode::Command { + name: args[0].clone(), + args: args[1..].to_vec(), + }) + } + } + fn expect_token(&mut self, expected: Token) -> Result<(), String> { if self.position < self.tokens.len() && self.tokens[self.position] == expected { self.position += 1; @@ -252,7 +284,7 @@ impl Parser { Err(format!( "Expected {:?}, found {:?}", expected, - self.tokens.get(self.position) + self.tokens.get(self.position).unwrap_or(&Token::NewLine) )) } } diff --git a/src/utilities/utilities.rs b/src/utilities/utilities.rs index b30f283..779d0f4 100644 --- a/src/utilities/utilities.rs +++ b/src/utilities/utilities.rs @@ -16,13 +16,15 @@ #[derive(Debug, Clone, PartialEq)] pub enum Token { Word(String), + String(String), Assignment, Pipe, - Redirect(String), - LeftParen, - RightParen, + Redirect(RedirectType), Semicolon, NewLine, + Ampersand, + LeftParen, + RightParen, If, Then, Else, @@ -33,7 +35,23 @@ pub enum Token { For, In, Function, - Ampersand, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum RedirectType { + Out, + Append, + In, +} + +impl RedirectType { + pub fn as_str(&self) -> &'static str { + match self { + RedirectType::Out => ">", + RedirectType::Append => ">>", + RedirectType::In => "<", + } + } } #[derive(Debug, Clone)] @@ -49,7 +67,7 @@ pub enum ASTNode { Pipeline(Vec), Redirect { node: Box, - direction: String, + direction: RedirectType, target: String, }, Block(Vec), @@ -73,3 +91,12 @@ pub enum ASTNode { }, Background(Box), } + +impl ASTNode { + pub fn is_empty_command(&self) -> bool { + match self { + ASTNode::Command { name, args } => name.is_empty() && args.is_empty(), + _ => false, + } + } +}