tenseleyflow/rush / 8a29037

Browse files

feat: implement control flow execution and fix parsing

- Add control_flow.rs module with execution for if/while/for/case statements
- Add Statement::Script variant for multi-line scripts
- Fix grammar to reserve keywords (if, then, fi, while, do, done, for, in, case, esac)
- Fix bare_word_part to exclude semicolons
- Make NEWLINE silent to prevent parse tree pollution
- Update command_line grammar to support multiple commands
- Add keyword_boundary rule for proper keyword matching
- Update CLI to handle Statement::Script and execute all control flow types
- Add parser tests for if statements and multiline scripts

If statements are fully working. While/for/case loops need additional testing and fixes.
Authored by espadonne
SHA
8a290370a5444114b61d3287f201306d973b7134
Parents
31bc17e
Tree
1ac0916

11 changed files

StatusFile+-
A .fackr/workspace.json 29 0
M crates/rush-cli/src/main.rs 46 25
A crates/rush-executor/src/control_flow.rs 161 0
M crates/rush-executor/src/lib.rs 2 0
M crates/rush-parser/src/ast.rs 3 1
M crates/rush-parser/src/grammar.pest 22 14
M crates/rush-parser/src/parser.rs 44 2
A docs/RUSH.md 10 0
A docs/roadmap_overview.md 114 0
A docs/roadmap_phase0.md 29 0
A docs/roadmap_phase1.md 62 0
.fackr/workspace.jsonadded
@@ -0,0 +1,29 @@
1
+{
2
+  "active_tab": 0,
3
+  "tabs": [
4
+    {
5
+      "files": [
6
+        {
7
+          "path": "RUSH.md",
8
+          "is_orphan": false
9
+        }
10
+      ],
11
+      "active_pane": 0,
12
+      "panes": [
13
+        {
14
+          "buffer_idx": 0,
15
+          "cursor_line": 9,
16
+          "cursor_col": 71,
17
+          "viewport_line": 0,
18
+          "viewport_col": 0,
19
+          "bounds": {
20
+            "x_start": 0.0,
21
+            "y_start": 0.0,
22
+            "x_end": 1.0,
23
+            "y_end": 1.0
24
+          }
25
+        }
26
+      ]
27
+    }
28
+  ]
29
+}
crates/rush-cli/src/main.rsmodified
@@ -67,18 +67,14 @@ fn execute_file(path: &str) -> ExitCode {
67
     };
67
     };
68
 
68
 
69
     let mut context = Context::new();
69
     let mut context = Context::new();
70
-    let mut last_exit_code = 0;
70
+    // Parse and execute the entire file as one unit to support multi-line control flow
71
-    for line in content.lines() {
71
+    match execute_line(&content, &mut context, false) {
72
-        match execute_line(line, &mut context, false) {
72
+        Ok(code) => ExitCode::from(code as u8),
73
-            Ok(code) => last_exit_code = code,
73
+        Err(e) => {
74
-            Err(e) => {
74
+            eprintln!("rush: {}", e);
75
-                eprintln!("rush: {}", e);
75
+            ExitCode::from(1)
76
-                last_exit_code = 1;
77
-            }
78
         }
76
         }
79
     }
77
     }
80
-
81
-    ExitCode::from(last_exit_code as u8)
82
 }
78
 }
83
 
79
 
84
 /// Execute commands from stdin
80
 /// Execute commands from stdin
@@ -108,8 +104,7 @@ fn execute_stdin() -> ExitCode {
108
 
104
 
109
 /// Execute a single line of shell input
105
 /// Execute a single line of shell input
110
 fn execute_line(line: &str, context: &mut Context, interactive: bool) -> Result<i32, String> {
106
 fn execute_line(line: &str, context: &mut Context, interactive: bool) -> Result<i32, String> {
111
-    use rush_executor::{execute_pipeline, execute_simple_with_redirects};
107
+    use rush_parser::{parse_line, Statement};
112
-    use rush_parser::{parse_line, CompleteCommand, Statement};
113
 
108
 
114
     let statement = parse_line(line).map_err(|e| e.to_string())?;
109
     let statement = parse_line(line).map_err(|e| e.to_string())?;
115
 
110
 
@@ -118,6 +113,13 @@ fn execute_line(line: &str, context: &mut Context, interactive: bool) -> Result<
118
         Statement::Complete(complete_cmd) => {
113
         Statement::Complete(complete_cmd) => {
119
             execute_complete_command(&complete_cmd, context, interactive)
114
             execute_complete_command(&complete_cmd, context, interactive)
120
         }
115
         }
116
+        Statement::Script(commands) => {
117
+            let mut last_exit_code = 0;
118
+            for cmd in commands {
119
+                last_exit_code = execute_complete_command(&cmd, context, interactive)?;
120
+            }
121
+            Ok(last_exit_code)
122
+        }
121
     }
123
     }
122
 }
124
 }
123
 
125
 
@@ -126,7 +128,10 @@ fn execute_complete_command(
126
     context: &mut Context,
128
     context: &mut Context,
127
     interactive: bool,
129
     interactive: bool,
128
 ) -> Result<i32, String> {
130
 ) -> Result<i32, String> {
129
-    use rush_executor::{execute_and_or_list, execute_pipeline, execute_simple_with_redirects};
131
+    use rush_executor::{
132
+        execute_and_or_list, execute_case, execute_for, execute_if, execute_pipeline,
133
+        execute_simple_with_redirects, execute_while,
134
+    };
130
     use rush_parser::CompleteCommand;
135
     use rush_parser::CompleteCommand;
131
 
136
 
132
     match cmd {
137
     match cmd {
@@ -154,21 +159,37 @@ fn execute_complete_command(
154
             context.set_exit_status(exit_code);
159
             context.set_exit_status(exit_code);
155
             Ok(exit_code)
160
             Ok(exit_code)
156
         }
161
         }
157
-        CompleteCommand::If(_) => {
162
+        CompleteCommand::If(if_stmt) => {
158
-            // TODO: Implement if statement execution
163
+            let exit_code = execute_if(if_stmt, context)
159
-            Err("If statements not yet implemented".to_string())
164
+                .map(|result| result.exit_code())
165
+                .map_err(|e| e.to_string())?;
166
+
167
+            context.set_exit_status(exit_code);
168
+            Ok(exit_code)
160
         }
169
         }
161
-        CompleteCommand::While(_) => {
170
+        CompleteCommand::While(while_stmt) => {
162
-            // TODO: Implement while loop execution
171
+            let exit_code = execute_while(while_stmt, context)
163
-            Err("While loops not yet implemented".to_string())
172
+                .map(|result| result.exit_code())
173
+                .map_err(|e| e.to_string())?;
174
+
175
+            context.set_exit_status(exit_code);
176
+            Ok(exit_code)
164
         }
177
         }
165
-        CompleteCommand::For(_) => {
178
+        CompleteCommand::For(for_stmt) => {
166
-            // TODO: Implement for loop execution
179
+            let exit_code = execute_for(for_stmt, context)
167
-            Err("For loops not yet implemented".to_string())
180
+                .map(|result| result.exit_code())
181
+                .map_err(|e| e.to_string())?;
182
+
183
+            context.set_exit_status(exit_code);
184
+            Ok(exit_code)
168
         }
185
         }
169
-        CompleteCommand::Case(_) => {
186
+        CompleteCommand::Case(case_stmt) => {
170
-            // TODO: Implement case statement execution
187
+            let exit_code = execute_case(case_stmt, context)
171
-            Err("Case statements not yet implemented".to_string())
188
+                .map(|result| result.exit_code())
189
+                .map_err(|e| e.to_string())?;
190
+
191
+            context.set_exit_status(exit_code);
192
+            Ok(exit_code)
172
         }
193
         }
173
     }
194
     }
174
 }
195
 }
crates/rush-executor/src/control_flow.rsadded
@@ -0,0 +1,161 @@
1
+use rush_expand::Context;
2
+use rush_parser::{CaseStatement, CompleteCommand, ForStatement, IfStatement, WhileStatement};
3
+use crate::{ExecutionResult, PipelineError};
4
+
5
+/// Execute an if statement
6
+pub fn execute_if(
7
+    if_stmt: &IfStatement,
8
+    context: &mut Context,
9
+) -> Result<ExecutionResult, PipelineError> {
10
+    // Execute the condition and check if it succeeded
11
+    let condition_result = execute_complete_command(&if_stmt.condition, context)?;
12
+
13
+    if condition_result.success() {
14
+        // Condition was true, execute then body
15
+        execute_command_list(&if_stmt.then_body, context)
16
+    } else {
17
+        // Check elif clauses
18
+        for elif in &if_stmt.elif_clauses {
19
+            let elif_result = execute_complete_command(&elif.condition, context)?;
20
+            if elif_result.success() {
21
+                return execute_command_list(&elif.then_body, context);
22
+            }
23
+        }
24
+
25
+        // No elif matched, execute else body if present
26
+        if let Some(else_body) = &if_stmt.else_body {
27
+            execute_command_list(else_body, context)
28
+        } else {
29
+            // No else clause, return success (standard shell behavior)
30
+            Ok(crate::command::success_result())
31
+        }
32
+    }
33
+}
34
+
35
+/// Execute a while loop
36
+pub fn execute_while(
37
+    while_stmt: &WhileStatement,
38
+    context: &mut Context,
39
+) -> Result<ExecutionResult, PipelineError> {
40
+    let mut last_result = crate::command::success_result();
41
+
42
+    loop {
43
+        // Execute the condition
44
+        let condition_result = execute_complete_command(&while_stmt.condition, context)?;
45
+
46
+        if !condition_result.success() {
47
+            // Condition failed, exit loop
48
+            break;
49
+        }
50
+
51
+        // Execute the loop body
52
+        last_result = execute_command_list(&while_stmt.body, context)?;
53
+    }
54
+
55
+    Ok(last_result)
56
+}
57
+
58
+/// Execute a for loop
59
+pub fn execute_for(
60
+    for_stmt: &ForStatement,
61
+    context: &mut Context,
62
+) -> Result<ExecutionResult, PipelineError> {
63
+    use rush_expand::expand_word;
64
+
65
+    let mut last_result = crate::command::success_result();
66
+
67
+    // Expand all the words in the list
68
+    let mut values = Vec::new();
69
+    for word in &for_stmt.words {
70
+        let expanded = expand_word(word, context)
71
+            .map_err(|e| PipelineError::ExpansionError(e.to_string()))?;
72
+        values.push(expanded);
73
+    }
74
+
75
+    // Execute the loop body for each value
76
+    for value in values {
77
+        // Set the loop variable
78
+        context.set_var(&for_stmt.var_name, &value);
79
+
80
+        // Execute the loop body
81
+        last_result = execute_command_list(&for_stmt.body, context)?;
82
+    }
83
+
84
+    Ok(last_result)
85
+}
86
+
87
+/// Execute a case statement
88
+pub fn execute_case(
89
+    case_stmt: &CaseStatement,
90
+    context: &mut Context,
91
+) -> Result<ExecutionResult, PipelineError> {
92
+    use rush_expand::expand_word;
93
+
94
+    // Expand the word to match against
95
+    let word_value = expand_word(&case_stmt.word, context)
96
+        .map_err(|e| PipelineError::ExpansionError(e.to_string()))?;
97
+
98
+    // Try each case clause
99
+    for clause in &case_stmt.clauses {
100
+        // Check if any pattern matches
101
+        for pattern in &clause.patterns {
102
+            let pattern_value = expand_word(pattern, context)
103
+                .map_err(|e| PipelineError::ExpansionError(e.to_string()))?;
104
+
105
+            // TODO: Implement glob pattern matching
106
+            // For now, just do exact string matching
107
+            if word_value == pattern_value {
108
+                // Pattern matched, execute this clause's commands
109
+                return execute_command_list(&clause.body, context);
110
+            }
111
+        }
112
+    }
113
+
114
+    // No clause matched, return success (standard shell behavior)
115
+    Ok(crate::command::success_result())
116
+}
117
+
118
+/// Execute a complete command (helper for recursive execution)
119
+fn execute_complete_command(
120
+    cmd: &CompleteCommand,
121
+    context: &mut Context,
122
+) -> Result<ExecutionResult, PipelineError> {
123
+    match cmd {
124
+        CompleteCommand::Simple(simple_cmd) => {
125
+            Ok(crate::execute_simple_with_redirects(simple_cmd, context, false)?)
126
+        }
127
+        CompleteCommand::Pipeline(pipeline) => {
128
+            crate::execute_pipeline(pipeline, context)
129
+        }
130
+        CompleteCommand::AndOrList(and_or_list) => {
131
+            crate::execute_and_or_list(and_or_list, context)
132
+        }
133
+        CompleteCommand::If(if_stmt) => {
134
+            execute_if(if_stmt, context)
135
+        }
136
+        CompleteCommand::While(while_stmt) => {
137
+            execute_while(while_stmt, context)
138
+        }
139
+        CompleteCommand::For(for_stmt) => {
140
+            execute_for(for_stmt, context)
141
+        }
142
+        CompleteCommand::Case(case_stmt) => {
143
+            execute_case(case_stmt, context)
144
+        }
145
+    }
146
+}
147
+
148
+/// Execute a list of commands
149
+fn execute_command_list(
150
+    commands: &[CompleteCommand],
151
+    context: &mut Context,
152
+) -> Result<ExecutionResult, PipelineError> {
153
+    let mut last_result = crate::command::success_result();
154
+
155
+    for cmd in commands {
156
+        last_result = execute_complete_command(cmd, context)?;
157
+        context.set_exit_status(last_result.exit_code());
158
+    }
159
+
160
+    Ok(last_result)
161
+}
crates/rush-executor/src/lib.rsmodified
@@ -1,12 +1,14 @@
1
 // Rush executor - Command execution engine
1
 // Rush executor - Command execution engine
2
 
2
 
3
 pub mod command;
3
 pub mod command;
4
+pub mod control_flow;
4
 pub mod pipeline;
5
 pub mod pipeline;
5
 pub mod redirect;
6
 pub mod redirect;
6
 pub mod terminal;
7
 pub mod terminal;
7
 pub mod test_builtin;
8
 pub mod test_builtin;
8
 
9
 
9
 pub use command::{execute_command, ExecutionError, ExecutionResult};
10
 pub use command::{execute_command, ExecutionError, ExecutionResult};
11
+pub use control_flow::{execute_case, execute_for, execute_if, execute_while};
10
 pub use pipeline::{execute_and_or_list, execute_pipeline, execute_simple_with_redirects, PipelineError};
12
 pub use pipeline::{execute_and_or_list, execute_pipeline, execute_simple_with_redirects, PipelineError};
11
 pub use redirect::RedirectError;
13
 pub use redirect::RedirectError;
12
 
14
 
crates/rush-parser/src/ast.rsmodified
@@ -2,8 +2,10 @@
2
 
2
 
3
 #[derive(Debug, Clone, PartialEq, Eq)]
3
 #[derive(Debug, Clone, PartialEq, Eq)]
4
 pub enum Statement {
4
 pub enum Statement {
5
-    /// Complete command (simple, pipeline, control flow, etc.)
5
+    /// Single complete command (simple, pipeline, control flow, etc.)
6
     Complete(CompleteCommand),
6
     Complete(CompleteCommand),
7
+    /// Multiple complete commands (for scripts with multiple top-level commands)
8
+    Script(Vec<CompleteCommand>),
7
     /// Empty line or comment
9
     /// Empty line or comment
8
     Empty,
10
     Empty,
9
 }
11
 }
crates/rush-parser/src/grammar.pestmodified
@@ -5,10 +5,11 @@ ws = _{ " " | "\t" }
5
 COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* }
5
 COMMENT = _{ "#" ~ (!NEWLINE ~ ANY)* }
6
 
6
 
7
 // Top-level input
7
 // Top-level input
8
-input = { SOI ~ ws* ~ command_line ~ ws* ~ EOI }
8
+trailing_sep = _{ (NEWLINE | ";")* }
9
+input = { SOI ~ ws* ~ command_line ~ ws* ~ trailing_sep ~ ws* ~ EOI }
9
 
10
 
10
-// Command line: can be compound commands or pipelines
11
+// Command line: can be multiple complete commands separated by newlines/semicolons
11
-command_line = { complete_command? }
12
+command_line = { (complete_command ~ (separator ~ complete_command)* ~ separator?)? }
12
 
13
 
13
 // Complete command: can be a compound command or an and_or list
14
 // Complete command: can be a compound command or an and_or list
14
 complete_command = {
15
 complete_command = {
@@ -27,31 +28,34 @@ and_or_op = { "&&" | "||" }
27
 pipeline = { simple_command ~ (ws* ~ "|" ~ ws* ~ simple_command)* }
28
 pipeline = { simple_command ~ (ws* ~ "|" ~ ws* ~ simple_command)* }
28
 
29
 
29
 // Control flow statements
30
 // Control flow statements
31
+// Keywords must be followed by whitespace, separator, or EOI (not word characters)
32
+keyword_boundary = _{ &(ws | NEWLINE | ";" | EOI) }
33
+
30
 if_statement = {
34
 if_statement = {
31
-    "if" ~ separator ~ complete_command ~ separator ~ "then" ~ separator ~ command_list ~ separator
35
+    "if" ~ keyword_boundary ~ separator ~ complete_command ~ separator ~ "then" ~ keyword_boundary ~ separator ~ command_list ~ separator
32
     ~ elif_clause*
36
     ~ elif_clause*
33
     ~ else_clause?
37
     ~ else_clause?
34
-    ~ "fi"
38
+    ~ "fi" ~ keyword_boundary
35
 }
39
 }
36
 
40
 
37
 elif_clause = {
41
 elif_clause = {
38
-    "elif" ~ separator ~ complete_command ~ separator ~ "then" ~ separator ~ command_list ~ separator
42
+    "elif" ~ keyword_boundary ~ separator ~ complete_command ~ separator ~ "then" ~ keyword_boundary ~ separator ~ command_list ~ separator
39
 }
43
 }
40
 
44
 
41
 else_clause = {
45
 else_clause = {
42
-    "else" ~ separator ~ command_list ~ separator
46
+    "else" ~ keyword_boundary ~ separator ~ command_list ~ separator
43
 }
47
 }
44
 
48
 
45
 while_statement = {
49
 while_statement = {
46
-    "while" ~ separator ~ complete_command ~ separator ~ "do" ~ separator ~ command_list ~ separator ~ "done"
50
+    "while" ~ keyword_boundary ~ separator ~ complete_command ~ separator ~ "do" ~ keyword_boundary ~ separator ~ command_list ~ separator ~ "done" ~ keyword_boundary
47
 }
51
 }
48
 
52
 
49
 for_statement = {
53
 for_statement = {
50
-    "for" ~ ws+ ~ var_name ~ ws+ ~ "in" ~ (ws+ ~ word)* ~ separator ~ "do" ~ separator ~ command_list ~ separator ~ "done"
54
+    "for" ~ keyword_boundary ~ ws+ ~ var_name ~ ws+ ~ "in" ~ keyword_boundary ~ (ws+ ~ word)* ~ separator ~ "do" ~ keyword_boundary ~ separator ~ command_list ~ separator ~ "done" ~ keyword_boundary
51
 }
55
 }
52
 
56
 
53
 case_statement = {
57
 case_statement = {
54
-    "case" ~ ws+ ~ word ~ ws+ ~ "in" ~ separator ~ case_clause* ~ "esac"
58
+    "case" ~ keyword_boundary ~ ws+ ~ word ~ ws+ ~ "in" ~ keyword_boundary ~ separator ~ case_clause* ~ "esac" ~ keyword_boundary
55
 }
59
 }
56
 
60
 
57
 case_clause = {
61
 case_clause = {
@@ -61,7 +65,7 @@ case_clause = {
61
 pattern = { word }
65
 pattern = { word }
62
 
66
 
63
 // Command list: one or more complete commands separated by newlines or semicolons
67
 // Command list: one or more complete commands separated by newlines or semicolons
64
-command_list = { (complete_command ~ separator)* ~ complete_command? }
68
+command_list = { complete_command ~ (separator ~ complete_command)* }
65
 
69
 
66
 // Separator: newlines, semicolons, or whitespace
70
 // Separator: newlines, semicolons, or whitespace
67
 separator = _{ (ws* ~ (NEWLINE | ";") ~ ws*)+ | ws+ }
71
 separator = _{ (ws* ~ (NEWLINE | ";") ~ ws*)+ | ws+ }
@@ -121,10 +125,14 @@ var_modifier = {
121
 command_substitution = { "$(" ~ command_subst_content ~ ")" }
125
 command_substitution = { "$(" ~ command_subst_content ~ ")" }
122
 command_subst_content = @{ (!(")" | NEWLINE) ~ ANY)* }
126
 command_subst_content = @{ (!(")" | NEWLINE) ~ ANY)* }
123
 
127
 
128
+// Reserved keywords that cannot be used as bare words
129
+keyword = { ("if" | "then" | "elif" | "else" | "fi" | "while" | "do" | "done" | "for" | "in" | "case" | "esac") ~ &(ws | NEWLINE | ";" | EOI) }
130
+
124
 // Bare word part: literal text (no special characters)
131
 // Bare word part: literal text (no special characters)
125
-// Note: We exclude $, |, &, <, > to handle expansion, pipes, operators, and redirects separately
132
+// Note: We exclude $, |, &, <, >, ; to handle expansion, pipes, operators, redirects, and separators
126
 // "=" is allowed in words (needed for test command), assignments use explicit var_name pattern
133
 // "=" is allowed in words (needed for test command), assignments use explicit var_name pattern
127
-bare_word_part = @{ (!(" " | "\t" | NEWLINE | "#" | "\"" | "'" | "$" | "|" | "&" | "<" | ">") ~ ANY)+ }
134
+// Keywords are reserved and cannot be used as bare words
135
+bare_word_part = @{ !keyword ~ (!(" " | "\t" | NEWLINE | "#" | "\"" | "'" | "$" | "|" | "&" | "<" | ">" | ";") ~ ANY)+ }
128
 
136
 
129
 // Quoted strings (single quotes prevent expansion, double quotes allow it)
137
 // Quoted strings (single quotes prevent expansion, double quotes allow it)
130
 quoted_string = { double_quoted | single_quoted }
138
 quoted_string = { double_quoted | single_quoted }
@@ -142,4 +150,4 @@ double_quoted_text = @{ (!"\"" ~ !"$" ~ ANY)+ }
142
 // Single quotes: no expansion (literal)
150
 // Single quotes: no expansion (literal)
143
 single_quoted = @{ "'" ~ (!"'" ~ ANY)* ~ "'" }
151
 single_quoted = @{ "'" ~ (!"'" ~ ANY)* ~ "'" }
144
 
152
 
145
-NEWLINE = { "\n" | "\r\n" }
153
+NEWLINE = _{ "\n" | "\r\n" }
crates/rush-parser/src/parser.rsmodified
@@ -54,15 +54,22 @@ pub fn parse_line(input: &str) -> Result<Statement, ParseError> {
54
 }
54
 }
55
 
55
 
56
 fn parse_command_line(pair: pest::iterators::Pair<Rule>) -> Result<Statement, ParseError> {
56
 fn parse_command_line(pair: pest::iterators::Pair<Rule>) -> Result<Statement, ParseError> {
57
+    let mut commands = Vec::new();
58
+
57
     for inner_pair in pair.into_inner() {
59
     for inner_pair in pair.into_inner() {
58
         match inner_pair.as_rule() {
60
         match inner_pair.as_rule() {
59
             Rule::complete_command => {
61
             Rule::complete_command => {
60
-                return Ok(Statement::Complete(parse_complete_command(inner_pair)?));
62
+                commands.push(parse_complete_command(inner_pair)?);
61
             }
63
             }
62
             _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
64
             _ => return Err(ParseError::UnexpectedRule(inner_pair.as_rule())),
63
         }
65
         }
64
     }
66
     }
65
-    Ok(Statement::Empty)
67
+
68
+    match commands.len() {
69
+        0 => Ok(Statement::Empty),
70
+        1 => Ok(Statement::Complete(commands.into_iter().next().unwrap())),
71
+        _ => Ok(Statement::Script(commands)),
72
+    }
66
 }
73
 }
67
 
74
 
68
 fn parse_complete_command(pair: pest::iterators::Pair<Rule>) -> Result<CompleteCommand, ParseError> {
75
 fn parse_complete_command(pair: pest::iterators::Pair<Rule>) -> Result<CompleteCommand, ParseError> {
@@ -828,4 +835,39 @@ mod tests {
828
             _ => panic!("Expected Pipeline"),
835
             _ => panic!("Expected Pipeline"),
829
         }
836
         }
830
     }
837
     }
838
+
839
+    #[test]
840
+    fn test_parse_if_statement_newlines() {
841
+        let input = "if test 5 -eq 5\nthen\necho equal\nfi";
842
+        let result = parse_line(input);
843
+        match result {
844
+            Ok(Statement::Complete(CompleteCommand::If(_))) => {}
845
+            Ok(other) => panic!("Expected If, got: {:?}", other),
846
+            Err(e) => panic!("Parse error: {}", e),
847
+        }
848
+    }
849
+
850
+    #[test]
851
+    fn test_parse_if_statement() {
852
+        let input = "if test 5 -eq 5; then echo equal; fi";
853
+        let result = parse_line(input);
854
+        match result {
855
+            Ok(Statement::Complete(CompleteCommand::If(_))) => {}
856
+            Ok(other) => panic!("Expected If, got: {:?}", other),
857
+            Err(e) => panic!("Parse error: {}", e),
858
+        }
859
+    }
860
+
861
+    #[test]
862
+    fn test_parse_multiline_script() {
863
+        let input = "echo hello\necho world\n";
864
+        let result = parse_line(input);
865
+        match result {
866
+            Ok(Statement::Script(cmds)) => {
867
+                assert_eq!(cmds.len(), 2);
868
+            }
869
+            Ok(other) => panic!("Expected Script, got: {:?}", other),
870
+            Err(e) => panic!("Parse error: {}", e),
871
+        }
872
+    }
831
 }
873
 }
docs/RUSH.mdadded
@@ -0,0 +1,10 @@
1
+# RUSH
2
+the rust shell
3
+
4
+rush aims to be a modern, friendly, and fast shell
5
+
6
+it should have 100% parity with bash, but also have some of the friendly features of fish.
7
+
8
+it should follow all standard shell protocols and follow other FOSS projects for guidance.
9
+
10
+we commit often and avoid coauthorship and Generated with ... messages.
docs/roadmap_overview.mdadded
@@ -0,0 +1,114 @@
1
+# Rush Shell - Development Roadmap
2
+
3
+## Vision
4
+
5
+A modern, friendly, and fast shell with 100% bash compatibility and fish-like features.
6
+
7
+## Development Approach
8
+
9
+**Incremental**: Start with minimal working REPL, add features iteratively. Each phase produces a working shell with progressively more capabilities.
10
+
11
+## Phases
12
+
13
+### Phase 0: Project Setup ✅
14
+**Status**: Completed
15
+**Goal**: Establish workspace structure
16
+- 7-crate workspace architecture
17
+- All dependencies configured
18
+- Directory structure established
19
+
20
+### Phase 1: Minimal REPL 🔜
21
+**Status**: Next
22
+**Goal**: Interactive shell that executes basic commands
23
+- Simple command parsing
24
+- External command execution
25
+- Basic REPL with reedline
26
+- **Validation**: Can run `ls`, `pwd`, `echo hello world`
27
+
28
+### Phase 2: Variables & Expansion
29
+**Goal**: Shell variables and expansions
30
+- Variable assignments and references
31
+- Parameter expansion (`${VAR}`, `${VAR:-default}`)
32
+- Command substitution `$(cmd)`
33
+- Word splitting
34
+
35
+### Phase 3: Pipelines & Redirection
36
+**Goal**: Pipelines and I/O redirection
37
+- Pipeline syntax: `cmd1 | cmd2 | cmd3`
38
+- Redirections: `<`, `>`, `>>`, `2>`, `2>&1`, `&>`
39
+- Pipe creation and management
40
+
41
+### Phase 4: Control Flow
42
+**Goal**: if/while/for/case statements
43
+- Conditional evaluation (if/then/else/fi)
44
+- Loop constructs (while/for)
45
+- Case statement matching
46
+- Built-in test command
47
+
48
+### Phase 5: Job Control
49
+**Goal**: Background jobs, fg/bg, signals
50
+- Job tracking and process groups
51
+- Terminal control
52
+- Signal handling (SIGCHLD, SIGTSTP, SIGINT)
53
+- Built-ins: `jobs`, `fg`, `bg`
54
+
55
+### Phase 6: Fish Features
56
+**Goal**: Syntax highlighting, suggestions, completions
57
+- Real-time syntax highlighting
58
+- History-based auto-suggestions
59
+- Smart tab completion
60
+- Better error messages
61
+
62
+### Phase 7: Bash Compatibility Hardening
63
+**Goal**: Close compatibility gaps
64
+- Extended glob patterns
65
+- Advanced parameter expansion
66
+- Brace expansion, arithmetic expansion
67
+- Here-documents, functions, arrays, subshells
68
+
69
+## Success Criteria
70
+
71
+- **Phase 1-3**: Daily command-line use (files, git, basic scripts)
72
+- **Phase 4-5**: Run moderately complex bash scripts unmodified
73
+- **Phase 6**: Better UX than bash for interactive use
74
+- **Phase 7**: 90%+ compatibility with common bash patterns
75
+
76
+## Key Features
77
+
78
+### Bash Compatibility
79
+- Command execution and pipelines
80
+- Variables and expansions
81
+- Control flow (if/while/for/case)
82
+- Job control
83
+- Full POSIX shell compliance
84
+
85
+### Fish-Inspired Features
86
+- Syntax highlighting
87
+- Autosuggestions from history
88
+- Better error messages
89
+- Improved tab completion
90
+
91
+## Architecture
92
+
93
+7-crate modular design:
94
+- **rush-cli**: Main binary and REPL orchestration
95
+- **rush-parser**: Pest PEG parser for shell syntax
96
+- **rush-expand**: Variable/glob expansion engine
97
+- **rush-executor**: Command execution and pipelines
98
+- **rush-job**: Job control and process management
99
+- **rush-eval**: Control flow evaluation engine
100
+- **rush-interactive**: Fish-like interactive features
101
+
102
+## Technology Stack
103
+
104
+- **Parser**: Pest (PEG)
105
+- **REPL**: reedline
106
+- **Terminal**: crossterm
107
+- **Unix APIs**: nix
108
+- **Glob**: globset
109
+- **Error handling**: thiserror
110
+
111
+## Current Status
112
+
113
+**Phase 0**: ✅ Complete - Workspace established and compiling
114
+**Phase 1**: 🔜 Ready to begin - Minimal REPL implementation next
docs/roadmap_phase0.mdadded
@@ -0,0 +1,29 @@
1
+# Phase 0: Project Setup
2
+
3
+**Status**: ✅ Completed
4
+**Goal**: Establish workspace structure
5
+
6
+## Tasks
7
+
8
+- [x] Initialize Cargo workspace with all 7 crates
9
+- [x] Add dependencies to each crate's Cargo.toml
10
+- [x] Create basic crate structure (lib.rs/main.rs files)
11
+- [x] Set up directory structure
12
+
13
+## Deliverable
14
+
15
+✅ Compiling workspace with placeholder code
16
+
17
+## Crates Created
18
+
19
+1. **rush-cli** - Main binary and REPL (binary crate)
20
+2. **rush-parser** - Lexer/parser (library)
21
+3. **rush-expand** - Variable/glob expansion (library)
22
+4. **rush-executor** - Command execution (library)
23
+5. **rush-job** - Job control (library)
24
+6. **rush-eval** - Control flow evaluation (library)
25
+7. **rush-interactive** - Fish-like features (library)
26
+
27
+## Next Steps
28
+
29
+Proceed to Phase 1: Minimal REPL
docs/roadmap_phase1.mdadded
@@ -0,0 +1,62 @@
1
+# Phase 1: Minimal Shell (Interactive + Non-Interactive)
2
+
3
+**Status**: 🚧 In Progress
4
+**Goal**: Full-featured shell that executes basic commands in both interactive and non-interactive modes
5
+
6
+## Tasks
7
+
8
+### rush-cli
9
+- [ ] Command-line argument parsing (clap)
10
+  - [ ] Interactive mode (default when terminal)
11
+  - [ ] `-c "command"` flag for command strings
12
+  - [ ] Script file execution
13
+  - [ ] Stdin detection (isatty)
14
+- [ ] Interactive mode with reedline
15
+  - [ ] Basic REPL loop (read → parse → execute → print)
16
+  - [ ] Prompt rendering
17
+  - [ ] Ctrl+C and Ctrl+D handling
18
+- [ ] Non-interactive mode
19
+  - [ ] Read from file
20
+  - [ ] Read from stdin
21
+  - [ ] Execute and exit with proper status code
22
+
23
+### rush-parser
24
+- [ ] Create minimal pest grammar (simple commands only)
25
+- [ ] Define AST types: `SimpleCommand { args: Vec<String> }`
26
+- [ ] Implement parser that handles: `ls`, `echo hello`, `cat file.txt`
27
+- [ ] Handle empty lines and comments (#)
28
+
29
+### rush-executor
30
+- [ ] Execute external commands via `std::process::Command`
31
+- [ ] PATH resolution
32
+- [ ] Return exit codes
33
+- [ ] Proper stdout/stderr handling
34
+- [ ] Environment variable inheritance
35
+
36
+## Validation Criteria
37
+
38
+### Interactive Mode
39
+Must be able to run interactively:
40
+- `ls`
41
+- `pwd`
42
+- `echo hello world`
43
+- Exit with Ctrl+D or `exit` command
44
+
45
+### Non-Interactive Mode
46
+Must work with:
47
+- `rush -c "ls"`
48
+- `echo "pwd" | rush`
49
+- `rush script.sh` (where script.sh contains commands)
50
+- Proper exit codes (0 on success, non-zero on failure)
51
+
52
+## Files to Create
53
+
54
+1. `crates/rush-parser/src/grammar.pest` - Pest PEG grammar
55
+2. `crates/rush-parser/src/ast.rs` - AST type definitions
56
+3. `crates/rush-parser/src/parser.rs` - Parser implementation
57
+4. `crates/rush-cli/src/repl.rs` - REPL loop
58
+5. `crates/rush-executor/src/command.rs` - Command execution
59
+
60
+## Next Steps
61
+
62
+After Phase 1 completion, proceed to Phase 2: Variables & Expansion