initial commit

This commit is contained in:
2025-11-01 22:45:55 +01:00
commit 78959d4f22
19 changed files with 2027 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target

534
Cargo.lock generated Normal file
View File

@@ -0,0 +1,534 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "assert_cmd"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85"
dependencies = [
"anstyle",
"bstr",
"libc",
"predicates 3.1.3",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "autocfg"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "bstr"
version = "1.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
dependencies = [
"memchr",
"regex-automata",
"serde",
]
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "difference"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "difflib"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
[[package]]
name = "float-cmp"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4"
dependencies = [
"num-traits",
]
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "memchr"
version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "mio"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "predicates"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df"
dependencies = [
"difference",
"float-cmp",
"normalize-line-endings",
"predicates-core",
"regex",
]
[[package]]
name = "predicates"
version = "3.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
dependencies = [
"anstyle",
"difflib",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
[[package]]
name = "predicates-tree"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
dependencies = [
"predicates-core",
"termtree",
]
[[package]]
name = "proc-macro2"
version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "rush"
version = "0.1.0"
dependencies = [
"assert_cmd",
"predicates 1.0.8",
"serde",
"serde_json",
"thiserror",
"tokio",
]
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
"serde_core",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b"
dependencies = [
"libc",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "syn"
version = "2.0.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58917d35242480a05c2897064da0a80589a2a0476c9a3f2fdc83b53502e917"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "termtree"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wait-timeout"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
dependencies = [
"libc",
]
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"

14
Cargo.toml Normal file
View File

@@ -0,0 +1,14 @@
[package]
name = "rush"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
thiserror = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dev-dependencies]
assert_cmd = "2.0"
predicates = "1.0"

6
README.md Normal file
View File

@@ -0,0 +1,6 @@
# rush - A Modern Shell Scripting Language Interpreter
## Overview
Rush is a modern shell scripting language interpreter designed to execute scripts written in the `.rsh` format. It supports features such as variable assignment, control flow, and parallel execution, making it a powerful tool for automating tasks and scripting.
More info coming soon

14
src/error/mod.rs Normal file
View File

@@ -0,0 +1,14 @@
#[derive(Debug)]
pub enum RushError {
SyntaxError(String),
RuntimeError(String),
VariableError(String),
}
impl std::fmt::Display for RushError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl std::error::Error for RushError {}

367
src/interpreter/executor.rs Normal file
View File

@@ -0,0 +1,367 @@
use crate::parser::ast::{AstNode, Command, Statement};
use crate::runtime::builtins;
use crate::runtime::environment::Environment;
use std::process::Command as StdCommand;
pub struct Executor<'a> {
env: &'a mut Environment,
}
impl<'a> Executor<'a> {
pub fn new(env: &'a mut Environment) -> Self {
Executor { env }
}
pub fn execute_node(&mut self, node: AstNode) {
match node {
AstNode::VariableAssignment { name, value } => {
if let AstNode::Literal(val) = *value {
self.env.set_variable(name, val);
}
}
AstNode::Command { name, args } => {
// convert args to strings
let arg_strings: Vec<String> = args
.into_iter()
.filter_map(|arg| {
if let AstNode::Literal(s) = arg {
// Check if its a variable reference
if s.starts_with('$') {
self.env.get_variable(&s[1..]).map(|v| v.clone())
} else {
Some(s)
}
} else {
None
}
})
.collect();
// built in commands :D
//todo: move and add more builtins
if name == "echo" {
builtins::echo(arg_strings);
} else if name == "exit" {
let code = arg_strings
.first()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
builtins::exit(code);
} else if name == "require_root" {
builtins::require_root();
} else {
// execute external cmd
let cmd = Command {
name: name.clone(),
args: arg_strings,
};
self.execute_command(cmd);
}
}
AstNode::ControlFlow {
condition: _,
then_branch,
else_branch,
} => {
println!("TODO: Execute if statement");
for node in then_branch {
self.execute_node(node);
}
if let Some(else_nodes) = else_branch {
for node in else_nodes {
self.execute_node(node);
}
}
}
AstNode::For { var, items, body } => {
// execute for loop by iterating over items
for item in items {
// set loop variable
self.env.set_variable(var.clone(), item);
// Execute body
for node in &body {
self.execute_node(node.clone());
}
}
}
AstNode::Parallel { blocks } => {
// create multithreaded runtime
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(4) //todo: make configurable or dynamic
.enable_all()
.build()
.unwrap();
runtime.block_on(async {
let mut handles = vec![];
for block in blocks {
// clone the environment for this block to access variables
let env = self.env.clone();
// Spawn each block as a separate task (can run on different threads)
let handle = tokio::task::spawn_blocking(move || {
for node in block {
// Execute node
if let AstNode::Command { name, args } = node {
let arg_strings: Vec<String> = args
.into_iter()
.filter_map(|arg| {
if let AstNode::Literal(s) = arg {
// Check if its a variable reference
if s.starts_with('$') {
env.get_variable(&s[1..]).map(|v| v.clone())
} else {
Some(s)
}
} else {
None
}
})
.collect();
if name == "echo" {
println!("{}", arg_strings.join(" "));
} else {
// execute external cmd
match std::process::Command::new(&name)
.args(&arg_strings)
.output()
{
Ok(output) => {
if !output.stdout.is_empty() {
print!(
"{}",
String::from_utf8_lossy(&output.stdout)
);
}
if !output.stderr.is_empty() {
eprint!(
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
}
Err(e) => {
eprintln!("Failed to execute '{}': {}", name, e);
}
}
}
}
}
});
handles.push(handle);
}
// wait for all
for handle in handles {
let _ = handle.await;
}
});
}
AstNode::Workers { count, body } => {
// execute with limited concurrency using a semaphore and multi-threaded runtime
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(count.max(2)) // use at least 2 threads, or the worker count
.enable_all()
.build()
.unwrap();
rt.block_on(async {
use std::sync::Arc;
use tokio::sync::Semaphore;
// clone environment for workers
let env = self.env.clone();
let semaphore = Arc::new(Semaphore::new(count));
let mut handles = vec![];
for node in body {
let sem = semaphore.clone();
let env_clone = env.clone();
// clone the node for the async task
let node_clone = node.clone();
// Use spawn_blocking for CPU bound work to run on thread pool
let handle = tokio::task::spawn_blocking(move || {
// Acquire semaphore permit (limits concurrency)
// Note: We need to do this in a blocking context
let rt_inner = tokio::runtime::Handle::current();
let _permit = rt_inner.block_on(async { sem.acquire().await.unwrap() });
// execute the node based on its type
match node_clone {
AstNode::Command { name, args } => {
let arg_strings: Vec<String> = args
.into_iter()
.filter_map(|arg| {
if let AstNode::Literal(s) = arg {
// Check if it's a variable reference
if s.starts_with('$') {
env_clone
.get_variable(&s[1..])
.map(|v| v.clone())
} else {
Some(s)
}
} else {
None
}
})
.collect();
if name == "echo" {
println!("{}", arg_strings.join(" "));
} else {
match std::process::Command::new(&name)
.args(&arg_strings)
.output()
{
Ok(output) => {
if !output.stdout.is_empty() {
print!(
"{}",
String::from_utf8_lossy(&output.stdout)
);
}
if !output.stderr.is_empty() {
eprint!(
"{}",
String::from_utf8_lossy(&output.stderr)
);
}
}
Err(e) => {
eprintln!("Failed to execute '{}': {}", name, e);
}
}
}
}
AstNode::For {
var,
items,
body: for_body,
} => {
// execute for loop
for item in items {
for for_node in &for_body {
// simple execution for commands in for loops
if let AstNode::Command { name, args } = for_node {
let arg_strings: Vec<String> = args
.iter()
.filter_map(|arg| {
if let AstNode::Literal(s) = arg {
if s.starts_with('$') {
let var_name = &s[1..];
if var_name == var {
// loop var
Some(item.clone())
} else {
// Environment var
env_clone
.get_variable(var_name)
.map(|v| v.clone())
}
} else {
Some(s.clone())
}
} else {
None
}
})
.collect();
if name == "echo" {
println!("{}", arg_strings.join(" "));
}
}
}
}
}
_ => {
//todo: other node types not yet supported in workers
}
}
});
handles.push(handle);
}
// wait for all tasks to complete
for handle in handles {
let _ = handle.await;
}
});
}
AstNode::Literal(_) => {
// literals dont execute on their own
}
}
}
pub fn execute(&mut self, statement: Statement) {
match statement {
Statement::Command(cmd) => self.execute_command(cmd),
Statement::Assignment(var, value) => self.env.set_variable(var, value),
Statement::For { var, items, body } => {
// todo: Implement for loop execution
println!(
"todo: Execute for loop with var '{}' over {} items",
var,
items.len()
);
for _stmt in body {
// execute each statement in the loop body
}
}
Statement::If {
condition,
then_branch,
else_branch,
} => {
// todo: Implement if statement execution
println!("todo: Execute if with condition '{}'", condition);
for _stmt in then_branch {
// Execute then branch
}
if let Some(else_stmts) = else_branch {
for _stmt in else_stmts {
// Execute else branch
}
}
}
Statement::Parallel { blocks } => {
// todo: Implement parallel execution
println!("todo: Execute {} parallel blocks", blocks.len());
}
}
}
fn execute_command(&mut self, command: Command) {
let result = StdCommand::new(&command.name).args(&command.args).output();
match result {
Ok(output) => {
if !output.stdout.is_empty() {
print!("{}", String::from_utf8_lossy(&output.stdout));
}
if !output.stderr.is_empty() {
eprint!("{}", String::from_utf8_lossy(&output.stderr));
}
if !output.status.success() {
eprintln!(
"Command '{}' failed with exit code: {:?}",
command.name,
output.status.code()
);
}
}
Err(e) => {
eprintln!("Failed to execute command '{}': {}", command.name, e);
}
}
}
}

32
src/interpreter/mod.rs Normal file
View File

@@ -0,0 +1,32 @@
pub mod executor;
pub mod parallel;
use crate::parser::Parser;
use crate::runtime::environment::Environment;
use executor::Executor;
/// Main entry point for executing rush scripts
pub fn execute(content: &str) {
// create a new parser
let parser = Parser::new();
// parse the script content
let program = match parser.parse(content) {
Ok(prog) => prog,
Err(e) => {
eprintln!("Parse error: {}", e);
std::process::exit(1);
}
};
// create a new environment
let mut env = Environment::new();
// create an executor
let mut executor = Executor::new(&mut env);
// execute each statement in the program
for statement in program.statements {
executor.execute_node(statement);
}
}

View File

@@ -0,0 +1,39 @@
use tokio::task;
pub async fn execute_parallel<F, Fut>(tasks: Vec<F>)
where
F: FnOnce() -> Fut + Send + 'static,
Fut: std::future::Future<Output = ()> + Send + 'static,
{
let handles: Vec<_> = tasks
.into_iter()
.map(|task| {
task::spawn(async move {
task().await;
})
})
.collect();
for handle in handles {
let _ = handle.await;
}
}
// simpler version for synchronous tasks
pub async fn execute_parallel_sync<F>(tasks: Vec<F>)
where
F: FnOnce() + Send + 'static,
{
let handles: Vec<_> = tasks
.into_iter()
.map(|task| {
task::spawn_blocking(move || {
task();
})
})
.collect();
for handle in handles {
let _ = handle.await;
}
}

123
src/lexer/mod.rs Normal file
View File

@@ -0,0 +1,123 @@
#[derive(Debug, PartialEq)]
pub enum Token {
Identifier(String),
Number(i64),
Assign,
Plus,
Minus,
Multiply,
Divide,
If,
Else,
While,
Print,
Eof,
}
pub struct Lexer<'a> {
input: &'a str,
position: usize,
current_char: Option<char>,
}
impl<'a> Lexer<'a> {
pub fn new(input: &'a str) -> Self {
Lexer {
input,
position: 0,
current_char: input.chars().next(),
}
}
fn advance(&mut self) {
self.position += 1;
self.current_char = self.input.chars().nth(self.position);
}
fn skip_whitespace(&mut self) {
while let Some(c) = self.current_char {
if c.is_whitespace() {
self.advance();
} else {
break;
}
}
}
pub fn next_token(&mut self) -> Token {
while let Some(c) = self.current_char {
if c.is_whitespace() {
self.skip_whitespace();
continue;
}
if c.is_alphabetic() {
let identifier = self.collect_identifier();
return match identifier.as_str() {
"if" => Token::If,
"else" => Token::Else,
"while" => Token::While,
"print" => Token::Print,
_ => Token::Identifier(identifier),
};
}
if c.is_digit(10) {
return Token::Number(self.collect_number());
}
if c == '=' {
self.advance();
return Token::Assign;
}
if c == '+' {
self.advance();
return Token::Plus;
}
if c == '-' {
self.advance();
return Token::Minus;
}
if c == '*' {
self.advance();
return Token::Multiply;
}
if c == '/' {
self.advance();
return Token::Divide;
}
self.advance();
}
Token::Eof
}
fn collect_identifier(&mut self) -> String {
let start_pos = self.position;
while let Some(c) = self.current_char {
if c.is_alphanumeric() {
self.advance();
} else {
break;
}
}
self.input[start_pos..self.position].to_string()
}
fn collect_number(&mut self) -> i64 {
let start_pos = self.position;
while let Some(c) = self.current_char {
if c.is_digit(10) {
self.advance();
} else {
break;
}
}
self.input[start_pos..self.position].parse().unwrap()
}
}

5
src/lib.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod error;
pub mod interpreter;
pub mod lexer;
pub mod parser;
pub mod runtime;

20
src/main.rs Normal file
View File

@@ -0,0 +1,20 @@
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
eprintln!("Usage: rush <script.rsh>");
std::process::exit(1);
}
let script_path = &args[1];
let content = std::fs::read_to_string(script_path).expect("Failed to read the script file");
let stripped_content = if content.starts_with("#!") {
content.lines().skip(1).collect::<Vec<&str>>().join("\n")
} else {
content
};
rush::interpreter::execute(&stripped_content);
}

60
src/parser/ast.rs Normal file
View File

@@ -0,0 +1,60 @@
#[derive(Debug, Clone)]
pub enum AstNode {
VariableAssignment {
name: String,
value: Box<AstNode>,
},
ControlFlow {
condition: Box<AstNode>,
then_branch: Vec<AstNode>,
else_branch: Option<Vec<AstNode>>,
},
Command {
name: String,
args: Vec<AstNode>,
},
For {
var: String,
items: Vec<String>,
body: Vec<AstNode>,
},
Parallel {
blocks: Vec<Vec<AstNode>>,
},
Workers {
count: usize,
body: Vec<AstNode>,
},
Literal(String),
}
#[derive(Debug, Clone)]
pub struct Program {
pub statements: Vec<AstNode>,
}
// types for executor compatibility
#[derive(Debug, Clone)]
pub struct Command {
pub name: String,
pub args: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum Statement {
Command(Command),
Assignment(String, String),
For {
var: String,
items: Vec<String>,
body: Vec<Statement>,
},
If {
condition: String,
then_branch: Vec<Statement>,
else_branch: Option<Vec<Statement>>,
},
Parallel {
blocks: Vec<Vec<Statement>>,
},
}

393
src/parser/mod.rs Normal file
View File

@@ -0,0 +1,393 @@
pub mod ast;
use crate::error::RushError;
pub use ast::*;
use std::collections::HashSet;
pub struct Parser {
// todo: add fields for the parser state
}
impl Parser {
pub fn new() -> Self {
Parser {
// todo: init fields
}
}
pub fn parse(&self, input: &str) -> Result<Program, RushError> {
let mut statements = Vec::new();
let lines: Vec<&str> = input.lines().collect();
let mut i = 0;
while i < lines.len() {
let (node, next_i) = self.parse_statement(&lines, i)?;
if let Some(n) = node {
statements.push(n);
}
i = next_i;
}
let program = Program { statements };
// validate variables before returning
self.validate_variables(&program)?;
Ok(program)
}
fn validate_variables(&self, program: &Program) -> Result<(), RushError> {
use std::collections::HashSet;
let mut defined_vars: HashSet<String> = HashSet::new();
for statement in &program.statements {
self.validate_node(statement, &mut defined_vars)?;
}
Ok(())
}
fn validate_node(
&self,
node: &AstNode,
defined_vars: &mut HashSet<String>,
) -> Result<(), RushError> {
match node {
AstNode::VariableAssignment { name, value } => {
// first check if the value uses any undefined variables
self.check_node_for_undefined_vars(value, defined_vars)?;
// then add this variable to the defined set
defined_vars.insert(name.clone());
}
AstNode::Command { name: _, args } => {
// check all arguments for variable references
for arg in args {
self.check_node_for_undefined_vars(arg, defined_vars)?;
}
}
AstNode::For {
var,
items: _,
body,
} => {
// create a new scope for the for loop variable
let mut loop_scope = defined_vars.clone();
loop_scope.insert(var.clone());
// validate the body with the loop variable in scope
for body_node in body {
self.validate_node(body_node, &mut loop_scope)?;
}
}
AstNode::Parallel { blocks } => {
// each parallel block sees the current scope but can't define new vars
for block in blocks {
for block_node in block {
let mut parallel_scope = defined_vars.clone();
self.validate_node(block_node, &mut parallel_scope)?;
}
}
}
AstNode::Workers { count: _, body } => {
// workers blocks see the current scope
for worker_node in body {
let mut worker_scope = defined_vars.clone();
self.validate_node(worker_node, &mut worker_scope)?;
}
}
AstNode::ControlFlow {
condition,
then_branch,
else_branch,
} => {
self.check_node_for_undefined_vars(condition, defined_vars)?;
for node in then_branch {
self.validate_node(node, defined_vars)?;
}
if let Some(else_nodes) = else_branch {
for node in else_nodes {
self.validate_node(node, defined_vars)?;
}
}
}
AstNode::Literal(_) => {
// literals dont use vars
}
}
Ok(())
}
fn check_node_for_undefined_vars(
&self,
node: &AstNode,
defined_vars: &HashSet<String>,
) -> Result<(), RushError> {
match node {
AstNode::Literal(s) => {
// check if this is a variable reference
if s.starts_with('$') {
let var_name = &s[1..];
if !defined_vars.contains(var_name) {
return Err(RushError::VariableError(format!(
"Variable '{}' is used before being defined",
var_name
)));
}
}
}
AstNode::VariableAssignment { name: _, value } => {
self.check_node_for_undefined_vars(value, defined_vars)?;
}
AstNode::Command { name: _, args } => {
for arg in args {
self.check_node_for_undefined_vars(arg, defined_vars)?;
}
}
_ => {
// other node types dont directly contain variable references
}
}
Ok(())
}
fn parse_statement(
&self,
lines: &[&str],
mut i: usize,
) -> Result<(Option<AstNode>, usize), RushError> {
let line = lines[i].trim();
// skip empty lines and comments
if line.is_empty() || line.starts_with('#') {
return Ok((None, i + 1));
}
// parse variable assignment: name = "value"
// make sure it's actually an assignment (not a command with = in args)
if line.contains('=')
&& !line.starts_with("if")
&& !line.starts_with("for")
&& !line.starts_with("echo")
{
let parts: Vec<&str> = line.splitn(2, '=').collect();
if parts.len() == 2 {
let var_name = parts[0].trim();
// only treat as assignment if var_name is a valid identifier (no spaces, no special chars)
if !var_name.is_empty() && var_name.chars().all(|c| c.is_alphanumeric() || c == '_')
{
let value = parts[1].trim().trim_matches('"').to_string();
return Ok((
Some(AstNode::VariableAssignment {
name: var_name.to_string(),
value: Box::new(AstNode::Literal(value)),
}),
i + 1,
));
}
}
}
// parse for loop: for var in items { ... }
if line.starts_with("for ") {
// parse: for i in 1 2 3 {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 4 && parts[0] == "for" && parts[2] == "in" {
let var_name = parts[1].to_string();
// collect items until we hit '{'
let mut items = Vec::new();
for part in &parts[3..] {
if *part == "{" {
break;
}
items.push(part.trim_matches('"').to_string());
}
// parse body
i += 1;
let mut body = Vec::new();
while i < lines.len() && !lines[i].trim().starts_with('}') {
let (node, next_i) = self.parse_statement(lines, i)?;
if let Some(n) = node {
body.push(n);
}
i = next_i;
}
i += 1; // skip closing brace
return Ok((
Some(AstNode::For {
var: var_name,
items,
body,
}),
i,
));
}
}
// parse parallel block: parallel { ... }
if line.starts_with("parallel") && line.contains('{') {
i += 1;
let mut blocks = Vec::new();
while i < lines.len() {
let inner_line = lines[i].trim();
// end of parallel block
if inner_line.starts_with('}') {
i += 1;
break;
}
// handle 'run {' blocks inside parallel
if inner_line.starts_with("run") && inner_line.contains('{') {
let mut current_block = Vec::new();
// check if single line
if inner_line.contains('}') {
let start = inner_line.find('{').unwrap() + 1;
let end = inner_line.rfind('}').unwrap();
let content = &inner_line[start..end].trim();
// parse commands separated by semicolons
for cmd_part in content.split(';') {
let cmd_trimmed = cmd_part.trim();
if !cmd_trimmed.is_empty() {
let parts: Vec<&str> = cmd_trimmed.split_whitespace().collect();
if !parts.is_empty() {
let cmd_name = parts[0].to_string();
let args: Vec<AstNode> = parts[1..]
.iter()
.map(|arg| {
AstNode::Literal(arg.trim_matches('"').to_string())
})
.collect();
current_block.push(AstNode::Command {
name: cmd_name,
args,
});
}
}
}
blocks.push(current_block);
i += 1;
} else {
// multi line run block
i += 1;
while i < lines.len() {
let run_line = lines[i].trim();
// end of this run block
if run_line.starts_with('}') {
blocks.push(current_block);
i += 1;
break;
}
// handle multiple commands on one line separated by semicolons
if run_line.contains(';') {
for cmd_part in run_line.split(';') {
let cmd_trimmed = cmd_part.trim();
if !cmd_trimmed.is_empty() {
let parts: Vec<&str> =
cmd_trimmed.split_whitespace().collect();
if !parts.is_empty() {
let cmd_name = parts[0].to_string();
let args: Vec<AstNode> = parts[1..]
.iter()
.map(|arg| {
AstNode::Literal(
arg.trim_matches('"').to_string(),
)
})
.collect();
current_block.push(AstNode::Command {
name: cmd_name,
args,
});
}
}
}
i += 1;
} else {
let (node, next_i) = self.parse_statement(lines, i)?;
if let Some(n) = node {
current_block.push(n);
}
i = next_i;
}
}
}
} else {
// direct statement in parallel block (not inside a run {})
let (node, next_i) = self.parse_statement(lines, i)?;
if let Some(n) = node {
blocks.push(vec![n]);
}
i = next_i;
}
}
return Ok((Some(AstNode::Parallel { blocks }), i));
}
// parse workers N {} block
if line.starts_with("workers ") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let worker_count = parts[1].parse::<usize>().unwrap_or(1);
i += 1;
let mut body = Vec::new();
while i < lines.len() && !lines[i].trim().starts_with('}') {
let (node, next_i) = self.parse_statement(lines, i)?;
if let Some(n) = node {
body.push(n);
}
i = next_i;
}
i += 1; // skip closing brace
return Ok((
Some(AstNode::Workers {
count: worker_count,
body,
}),
i,
));
}
}
// parse command: echo "text" or any other command
if !line.is_empty() {
let parts: Vec<&str> = line.split_whitespace().collect();
if !parts.is_empty() {
let cmd_name = parts[0].to_string();
let args: Vec<AstNode> = parts[1..]
.iter()
.map(|arg| {
// kep $ for variable references, remove quotes
let cleaned = arg.trim_matches('"');
AstNode::Literal(cleaned.to_string())
})
.collect();
return Ok((
Some(AstNode::Command {
name: cmd_name,
args,
}),
i + 1,
));
}
}
Ok((None, i + 1))
}
}
//todo: add additional functions and implementations for the parser

31
src/runtime/builtins.rs Normal file
View File

@@ -0,0 +1,31 @@
pub fn echo(args: Vec<String>) {
println!("{}", args.join(" "));
}
pub fn require_root() {
if !is_root() {
eprintln!("This command requires root privileges.");
std::process::exit(1);
}
}
pub fn exit(code: i32) {
std::process::exit(code);
}
fn is_root() -> bool {
// check if the current user is root by checking UID
// on Unix systems, root has UID 0
#[cfg(unix)]
{
// try to get process UID using USER env variable
std::env::var("USER").map(|u| u == "root").unwrap_or(false)
}
#[cfg(not(unix))]
{
//todo
// On non-Unix systems, assume not root
false
}
}

View File

@@ -0,0 +1,26 @@
use std::collections::HashMap;
#[derive(Clone)]
pub struct Environment {
variables: HashMap<String, String>,
}
impl Environment {
pub fn new() -> Self {
Environment {
variables: HashMap::new(),
}
}
pub fn set_variable(&mut self, name: String, value: String) {
self.variables.insert(name, value);
}
pub fn get_variable(&self, name: &str) -> Option<&String> {
self.variables.get(name)
}
pub fn remove_variable(&mut self, name: &str) {
self.variables.remove(name);
}
}

2
src/runtime/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod builtins;
pub mod environment;

254
tests/interpreter_tests.rs Normal file
View File

@@ -0,0 +1,254 @@
use rush::interpreter::executor::Executor;
use rush::parser::Parser;
use rush::runtime::environment::Environment;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_variable_assignment() {
// Test variable assignment functionality
let parser = Parser::new();
let result = parser.parse("name = \"test\"");
assert!(result.is_ok());
let program = result.unwrap();
let mut env = Environment::new();
let mut executor = Executor::new(&mut env);
for stmt in program.statements {
executor.execute_node(stmt);
}
assert_eq!(env.get_variable("name"), Some(&"test".to_string()));
}
#[test]
fn test_simple_command() {
// Test basic command execution doesn't panic
let parser = Parser::new();
let result = parser.parse("echo \"test\"");
assert!(result.is_ok());
}
#[test]
fn test_script_parsing() {
// Test the parsing of a complete .rsh script
let script = r#"
name = "Louis"
echo "Hello" $name
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_ok());
let program = result.unwrap();
assert!(program.statements.len() >= 2);
}
#[test]
fn test_undefined_variable_detection() {
// Test that using an undefined variable is caught
let script = r#"
echo "Hello" $undefined_var
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_err());
let err_msg = format!("{:?}", result.unwrap_err());
assert!(err_msg.contains("undefined_var"));
}
#[test]
fn test_out_of_order_variable_detection() {
// Test that using a variable before it's defined is caught
let script = r#"
echo "Hello" $name
name = "Louis"
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_err());
let err_msg = format!("{:?}", result.unwrap_err());
assert!(err_msg.contains("name"));
}
#[test]
fn test_valid_variable_order() {
// Test that defining before using works correctly
let script = r#"
name = "Louis"
echo "Hello" $name
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_ok());
}
#[test]
fn test_for_loop_variable_scope() {
// Test that for loop variables are properly scoped
let script = r#"
for i in 1 2 3 {
echo "Number:" $i
}
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_ok());
}
#[test]
fn test_for_loop_variable_isolation() {
// Test that for loop variables don't leak to outer scope
let script = r#"
for i in 1 2 3 {
echo "Inside:" $i
}
echo "Outside:" $i
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_err());
let err_msg = format!("{:?}", result.unwrap_err());
assert!(err_msg.contains("i"));
}
#[test]
fn test_parallel_block_variable_access() {
// Test that parallel blocks can access outer scope variables
let script = r#"
name = "Test"
parallel {
run { echo "Task 1:" $name }
run { echo "Task 2:" $name }
}
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_ok());
}
#[test]
fn test_parallel_block_undefined_variable() {
// Test that parallel blocks catch undefined variables
let script = r#"
parallel {
run { echo "Task:" $undefined }
}
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_err());
}
#[test]
fn test_workers_block_variable_access() {
// Test that workers blocks can access outer scope variables
let script = r#"
msg = "Worker"
workers 2 {
echo $msg "Task 1"
echo $msg "Task 2"
}
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_ok());
}
#[test]
fn test_workers_with_for_loop() {
// Test that workers can contain for loops with variables
let script = r#"
prefix = "Item"
workers 2 {
for num in 1 2 3 {
echo $prefix $num
}
}
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_ok());
}
#[test]
fn test_nested_for_loops() {
// Test nested for loops with proper scoping
let script = r#"
for i in 1 2 {
for j in a b {
echo $i $j
}
}
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_ok());
}
#[test]
fn test_multiple_variables() {
// Test multiple variable definitions and usage
let script = r#"
user = "Alice"
project = "RushShell"
version = "1.0"
echo $user "is working on" $project "version" $version
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_ok());
}
#[test]
fn test_for_loop_with_outer_variables() {
// Test for loops can access both loop vars and outer vars
let script = r#"
prefix = "Item"
for num in 1 2 3 {
echo $prefix $num
}
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_ok());
}
#[test]
fn test_comprehensive_variable_usage() {
// Test a comprehensive script with all variable features
let script = r#"
user = "Alice"
project = "RushShell"
echo "Welcome" $user "to" $project
for item in "docs" "tests" "src" {
echo $user "is checking" $item
}
parallel {
run { echo "Parallel 1:" $user }
run { echo "Parallel 2:" $project }
}
workers 2 {
for num in 1 2 {
echo $project "worker task" $num
}
}
echo "Done with" $project
"#;
let parser = Parser::new();
let result = parser.parse(script);
assert!(result.is_ok());
}
}

42
tests/lexer_tests.rs Normal file
View File

@@ -0,0 +1,42 @@
use rush::lexer::{Lexer, Token};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tokenization() {
let mut lexer = Lexer::new("x = 10");
assert_eq!(lexer.next_token(), Token::Identifier("x".to_string()));
assert_eq!(lexer.next_token(), Token::Assign);
assert_eq!(lexer.next_token(), Token::Number(10));
assert_eq!(lexer.next_token(), Token::Eof);
}
#[test]
fn test_identifier_tokens() {
let mut lexer = Lexer::new("foo bar");
assert_eq!(lexer.next_token(), Token::Identifier("foo".to_string()));
assert_eq!(lexer.next_token(), Token::Identifier("bar".to_string()));
assert_eq!(lexer.next_token(), Token::Eof);
}
#[test]
fn test_control_flow_tokens() {
let mut lexer = Lexer::new("if else while");
assert_eq!(lexer.next_token(), Token::If);
assert_eq!(lexer.next_token(), Token::Else);
assert_eq!(lexer.next_token(), Token::While);
assert_eq!(lexer.next_token(), Token::Eof);
}
#[test]
fn test_operators() {
let mut lexer = Lexer::new("+ - * /");
assert_eq!(lexer.next_token(), Token::Plus);
assert_eq!(lexer.next_token(), Token::Minus);
assert_eq!(lexer.next_token(), Token::Multiply);
assert_eq!(lexer.next_token(), Token::Divide);
assert_eq!(lexer.next_token(), Token::Eof);
}
}

64
tests/parser_tests.rs Normal file
View File

@@ -0,0 +1,64 @@
use rush::parser::{AstNode, Parser};
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_variable_assignment() {
// Test parsing a variable assignment
let parser = Parser::new();
let input = "x = \"10\"";
let result = parser.parse(input);
assert!(result.is_ok());
let program = result.unwrap();
assert_eq!(program.statements.len(), 1);
match &program.statements[0] {
AstNode::VariableAssignment { name, .. } => {
assert_eq!(name, "x");
}
_ => panic!("Expected variable assignment"),
}
}
#[test]
fn test_command_parsing() {
// Test parsing a simple command
let parser = Parser::new();
let input = "echo \"Hello World\"";
let result = parser.parse(input);
assert!(result.is_ok());
let program = result.unwrap();
assert_eq!(program.statements.len(), 1);
match &program.statements[0] {
AstNode::Command { name, .. } => {
assert_eq!(name, "echo");
}
_ => panic!("Expected command"),
}
}
#[test]
fn test_multiple_statements() {
// Test parsing multiple statements
let parser = Parser::new();
let input = "name = \"Louis\"\necho \"Hello\" $name";
let result = parser.parse(input);
assert!(result.is_ok());
let program = result.unwrap();
assert_eq!(program.statements.len(), 2);
}
#[test]
fn test_skip_comments() {
// Test that comments are skipped
let parser = Parser::new();
let input = "# This is a comment\necho \"test\"";
let result = parser.parse(input);
assert!(result.is_ok());
let program = result.unwrap();
assert_eq!(program.statements.len(), 1);
}
}