| 1 | // Abstract Syntax Tree for Rush shell |
| 2 | |
| 3 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 4 | pub enum Statement { |
| 5 | /// Single complete command (simple, pipeline, control flow, etc.) |
| 6 | Complete(CompleteCommand), |
| 7 | /// Multiple complete commands (for scripts with multiple top-level commands) |
| 8 | Script(Vec<CompleteCommand>), |
| 9 | /// Empty line or comment |
| 10 | Empty, |
| 11 | } |
| 12 | |
| 13 | /// A complete command with optional background execution flag |
| 14 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 15 | pub struct CompleteCommand { |
| 16 | /// The command to execute |
| 17 | pub command: CommandType, |
| 18 | /// Whether to run in background (trailing &) |
| 19 | pub background: bool, |
| 20 | } |
| 21 | |
| 22 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 23 | pub enum CommandType { |
| 24 | /// Simple command with optional variable assignments |
| 25 | Simple(SimpleCommand), |
| 26 | /// Pipeline of commands connected by pipes |
| 27 | Pipeline(Pipeline), |
| 28 | /// Commands connected by && or || |
| 29 | AndOrList(AndOrList), |
| 30 | /// If statement |
| 31 | If(IfStatement), |
| 32 | /// While loop |
| 33 | While(WhileStatement), |
| 34 | /// For loop |
| 35 | For(ForStatement), |
| 36 | /// Select loop (menu selection) |
| 37 | Select(SelectStatement), |
| 38 | /// Case statement |
| 39 | Case(CaseStatement), |
| 40 | /// Function definition |
| 41 | Function(FunctionDef), |
| 42 | /// Subshell: (commands) |
| 43 | Subshell(Subshell), |
| 44 | /// Extended test: [[ expression ]] |
| 45 | ExtendedTest(CondExpr), |
| 46 | } |
| 47 | |
| 48 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 49 | pub struct SimpleCommand { |
| 50 | /// Variable assignments (VAR=value) |
| 51 | pub assignments: Vec<Assignment>, |
| 52 | /// Command and arguments (words that may contain expansions) |
| 53 | pub words: Vec<Word>, |
| 54 | /// I/O redirections |
| 55 | pub redirects: Vec<Redirect>, |
| 56 | } |
| 57 | |
| 58 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 59 | pub struct Pipeline { |
| 60 | /// Commands connected by pipes |
| 61 | pub commands: Vec<PipelineElement>, |
| 62 | /// Whether the pipeline is negated with ! |
| 63 | pub negated: bool, |
| 64 | } |
| 65 | |
| 66 | /// An element in a pipeline - can be a simple command, subshell, or extended test |
| 67 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 68 | pub enum PipelineElement { |
| 69 | Simple(SimpleCommand), |
| 70 | Subshell(Subshell), |
| 71 | ExtendedTest(CondExpr), |
| 72 | } |
| 73 | |
| 74 | /// Conditional expression for [[ ]] extended test |
| 75 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 76 | pub enum CondExpr { |
| 77 | /// Logical OR: expr1 || expr2 |
| 78 | Or(Box<CondExpr>, Box<CondExpr>), |
| 79 | /// Logical AND: expr1 && expr2 |
| 80 | And(Box<CondExpr>, Box<CondExpr>), |
| 81 | /// Logical NOT: ! expr |
| 82 | Not(Box<CondExpr>), |
| 83 | /// Unary test: -z, -n, -f, -d, etc. |
| 84 | Unary { op: String, operand: Word }, |
| 85 | /// Binary test: ==, !=, =~, -eq, -lt, etc. |
| 86 | Binary { left: Word, op: String, right: Word }, |
| 87 | /// Single word (true if non-empty after expansion) |
| 88 | Word(Word), |
| 89 | } |
| 90 | |
| 91 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 92 | pub enum Redirect { |
| 93 | /// Input redirection: <file |
| 94 | Input { file: Word }, |
| 95 | /// Output redirection: >file or N>file |
| 96 | Output { fd: Option<u32>, file: Word }, |
| 97 | /// Append redirection: >>file or N>>file |
| 98 | OutputAppend { fd: Option<u32>, file: Word }, |
| 99 | /// Stderr to stdout: 2>&1 |
| 100 | StderrToStdout, |
| 101 | /// All output: &>file or &>>file |
| 102 | AllOutput { file: Word, append: bool }, |
| 103 | /// Here-document: <<DELIMITER or <<-DELIMITER |
| 104 | Heredoc { |
| 105 | delimiter: String, |
| 106 | content: Vec<String>, |
| 107 | strip_tabs: bool, |
| 108 | expand: bool, |
| 109 | }, |
| 110 | /// Here-string: <<<string |
| 111 | Herestring { content: Word }, |
| 112 | /// Process substitution input: <(command) |
| 113 | /// Creates a FIFO that reads from the command's stdout |
| 114 | ProcessSubstInput { command: String }, |
| 115 | /// Process substitution output: >(command) |
| 116 | /// Creates a FIFO that writes to the command's stdin |
| 117 | ProcessSubstOutput { command: String }, |
| 118 | } |
| 119 | |
| 120 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 121 | pub struct AndOrList { |
| 122 | /// First pipeline in the list |
| 123 | pub first: Pipeline, |
| 124 | /// Remaining pipelines with their operators |
| 125 | pub rest: Vec<(AndOrOp, Pipeline)>, |
| 126 | } |
| 127 | |
| 128 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 129 | pub enum AndOrOp { |
| 130 | /// && operator (execute next if previous succeeded) |
| 131 | And, |
| 132 | /// || operator (execute next if previous failed) |
| 133 | Or, |
| 134 | } |
| 135 | |
| 136 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 137 | pub struct IfStatement { |
| 138 | /// Condition to test |
| 139 | pub condition: Box<CompleteCommand>, |
| 140 | /// Commands to execute if condition is true |
| 141 | pub then_body: Vec<CompleteCommand>, |
| 142 | /// Elif clauses |
| 143 | pub elif_clauses: Vec<ElifClause>, |
| 144 | /// Else body (optional) |
| 145 | pub else_body: Option<Vec<CompleteCommand>>, |
| 146 | } |
| 147 | |
| 148 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 149 | pub struct ElifClause { |
| 150 | /// Condition to test |
| 151 | pub condition: Box<CompleteCommand>, |
| 152 | /// Commands to execute if condition is true |
| 153 | pub then_body: Vec<CompleteCommand>, |
| 154 | } |
| 155 | |
| 156 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 157 | pub struct WhileStatement { |
| 158 | /// Condition to test |
| 159 | pub condition: Box<CompleteCommand>, |
| 160 | /// Commands to execute while condition is true |
| 161 | pub body: Vec<CompleteCommand>, |
| 162 | } |
| 163 | |
| 164 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 165 | pub struct ForStatement { |
| 166 | /// Variable name to iterate over |
| 167 | pub var_name: String, |
| 168 | /// Words to iterate through |
| 169 | pub words: Vec<Word>, |
| 170 | /// Commands to execute for each word |
| 171 | pub body: Vec<CompleteCommand>, |
| 172 | } |
| 173 | |
| 174 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 175 | pub struct SelectStatement { |
| 176 | /// Variable name to store selected item |
| 177 | pub var_name: String, |
| 178 | /// Words to present as menu options |
| 179 | pub words: Vec<Word>, |
| 180 | /// Commands to execute after selection |
| 181 | pub body: Vec<CompleteCommand>, |
| 182 | } |
| 183 | |
| 184 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 185 | pub struct CaseStatement { |
| 186 | /// Word to match against patterns |
| 187 | pub word: Word, |
| 188 | /// Case clauses with patterns and bodies |
| 189 | pub clauses: Vec<CaseClause>, |
| 190 | } |
| 191 | |
| 192 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 193 | pub struct CaseClause { |
| 194 | /// Patterns to match (connected by |) |
| 195 | pub patterns: Vec<Word>, |
| 196 | /// Commands to execute if pattern matches |
| 197 | pub body: Vec<CompleteCommand>, |
| 198 | } |
| 199 | |
| 200 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 201 | pub struct FunctionDef { |
| 202 | /// Function name |
| 203 | pub name: String, |
| 204 | /// Function body (commands to execute) |
| 205 | pub body: Vec<CompleteCommand>, |
| 206 | } |
| 207 | |
| 208 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 209 | pub struct Subshell { |
| 210 | /// Commands to execute in a subshell |
| 211 | pub commands: Vec<CompleteCommand>, |
| 212 | } |
| 213 | |
| 214 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 215 | pub struct Assignment { |
| 216 | pub name: String, |
| 217 | /// Optional array index for array[index]=value |
| 218 | pub index: Option<String>, |
| 219 | pub value: Word, |
| 220 | } |
| 221 | |
| 222 | /// A word is composed of one or more parts that will be concatenated |
| 223 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 224 | pub struct Word { |
| 225 | pub parts: Vec<WordPart>, |
| 226 | } |
| 227 | |
| 228 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 229 | pub enum WordPart { |
| 230 | /// Literal text |
| 231 | Literal(String), |
| 232 | /// Variable expansion: $VAR or ${VAR} |
| 233 | VarExpansion(VarExpansion), |
| 234 | /// Command substitution: $(cmd) |
| 235 | CommandSubstitution(String), |
| 236 | /// Brace expansion: {a,b,c} or {1..5} |
| 237 | BraceExpansion(BraceExpansion), |
| 238 | /// Arithmetic expansion: $((expr)) |
| 239 | ArithmeticExpansion(String), |
| 240 | /// Array literal: (one two three) |
| 241 | ArrayLiteral(Vec<Word>), |
| 242 | } |
| 243 | |
| 244 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 245 | pub enum BraceExpansion { |
| 246 | /// List: {a,b,c} |
| 247 | List(Vec<String>), |
| 248 | /// Sequence: {1..10} or {a..z} |
| 249 | Sequence { |
| 250 | start: String, |
| 251 | end: String, |
| 252 | increment: Option<i32>, |
| 253 | }, |
| 254 | } |
| 255 | |
| 256 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 257 | pub enum VarExpansion { |
| 258 | /// Simple: $VAR |
| 259 | Simple(String), |
| 260 | /// Braced: ${VAR} |
| 261 | Braced(String), |
| 262 | /// With default (colon): ${VAR:-default} - use default if unset OR empty |
| 263 | WithDefault { name: String, default: Box<Word> }, |
| 264 | /// With default (no colon): ${VAR-default} - use default only if unset |
| 265 | WithDefaultUnsetOnly { name: String, default: Box<Word> }, |
| 266 | /// Assign default (colon): ${VAR:=default} - assign if unset OR empty |
| 267 | AssignDefault { name: String, default: Box<Word> }, |
| 268 | /// Assign default (no colon): ${VAR=default} - assign only if unset |
| 269 | AssignDefaultUnsetOnly { name: String, default: Box<Word> }, |
| 270 | /// Use alternate if set (colon): ${VAR:+alternate} - use if set AND non-empty |
| 271 | UseIfSet { name: String, alternate: Box<Word> }, |
| 272 | /// Use alternate if set (no colon): ${VAR+alternate} - use if set (even if empty) |
| 273 | UseIfSetOnly { name: String, alternate: Box<Word> }, |
| 274 | /// Error if unset (colon): ${VAR:?message} - error if unset OR empty |
| 275 | ErrorIfUnset { name: String, message: Box<Word> }, |
| 276 | /// Error if unset (no colon): ${VAR?message} - error only if unset |
| 277 | ErrorIfUnsetOnly { name: String, message: Box<Word> }, |
| 278 | /// Length: ${#VAR} |
| 279 | Length(String), |
| 280 | /// Remove shortest prefix: ${VAR#pattern} |
| 281 | RemoveShortestPrefix { name: String, pattern: String }, |
| 282 | /// Remove longest prefix: ${VAR##pattern} |
| 283 | RemoveLongestPrefix { name: String, pattern: String }, |
| 284 | /// Remove shortest suffix: ${VAR%pattern} |
| 285 | RemoveShortestSuffix { name: String, pattern: String }, |
| 286 | /// Remove longest suffix: ${VAR%%pattern} |
| 287 | RemoveLongestSuffix { name: String, pattern: String }, |
| 288 | /// Replace first: ${VAR/pattern/replacement} |
| 289 | ReplaceFirst { name: String, pattern: String, replacement: String }, |
| 290 | /// Replace all: ${VAR//pattern/replacement} |
| 291 | ReplaceAll { name: String, pattern: String, replacement: String }, |
| 292 | /// Substring: ${VAR:offset} or ${VAR:offset:length} |
| 293 | Substring { name: String, offset: i32, length: Option<usize> }, |
| 294 | /// Uppercase first: ${VAR^} |
| 295 | UppercaseFirst(String), |
| 296 | /// Uppercase all: ${VAR^^} |
| 297 | UppercaseAll(String), |
| 298 | /// Lowercase first: ${VAR,} |
| 299 | LowercaseFirst(String), |
| 300 | /// Lowercase all: ${VAR,,} |
| 301 | LowercaseAll(String), |
| 302 | /// Array element: ${arr[index]} |
| 303 | ArrayElement { name: String, index: String }, |
| 304 | /// Array all elements (separate words): ${arr[@]} |
| 305 | ArrayAll(String), |
| 306 | /// Array all elements (single word): ${arr[*]} |
| 307 | ArrayStar(String), |
| 308 | /// Array length: ${#arr[@]} or ${#arr[*]} |
| 309 | ArrayLength(String), |
| 310 | /// Array indices: ${!arr[@]} |
| 311 | ArrayIndices(String), |
| 312 | /// Indirect expansion: ${!var} |
| 313 | Indirect(String), |
| 314 | /// Transformation: ${var@Q}, ${var@E}, etc. |
| 315 | Transform { name: String, op: char }, |
| 316 | } |
| 317 | |
| 318 | impl Pipeline { |
| 319 | pub fn new(commands: Vec<PipelineElement>) -> Self { |
| 320 | Self { commands, negated: false } |
| 321 | } |
| 322 | |
| 323 | pub fn new_negated(commands: Vec<PipelineElement>, negated: bool) -> Self { |
| 324 | Self { commands, negated } |
| 325 | } |
| 326 | |
| 327 | /// Helper to create a pipeline from simple commands |
| 328 | pub fn from_simple_commands(commands: Vec<SimpleCommand>) -> Self { |
| 329 | Self { |
| 330 | commands: commands.into_iter().map(PipelineElement::Simple).collect(), |
| 331 | negated: false, |
| 332 | } |
| 333 | } |
| 334 | |
| 335 | pub fn is_simple(&self) -> bool { |
| 336 | self.commands.len() == 1 |
| 337 | } |
| 338 | } |
| 339 | |
| 340 | impl SimpleCommand { |
| 341 | pub fn new(assignments: Vec<Assignment>, words: Vec<Word>) -> Self { |
| 342 | Self { |
| 343 | assignments, |
| 344 | words, |
| 345 | redirects: Vec::new(), |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | pub fn with_redirects( |
| 350 | assignments: Vec<Assignment>, |
| 351 | words: Vec<Word>, |
| 352 | redirects: Vec<Redirect>, |
| 353 | ) -> Self { |
| 354 | Self { |
| 355 | assignments, |
| 356 | words, |
| 357 | redirects, |
| 358 | } |
| 359 | } |
| 360 | |
| 361 | pub fn has_command(&self) -> bool { |
| 362 | !self.words.is_empty() |
| 363 | } |
| 364 | } |
| 365 | |
| 366 | impl Word { |
| 367 | pub fn new(parts: Vec<WordPart>) -> Self { |
| 368 | Self { parts } |
| 369 | } |
| 370 | |
| 371 | pub fn from_literal(s: impl Into<String>) -> Self { |
| 372 | Self { |
| 373 | parts: vec![WordPart::Literal(s.into())], |
| 374 | } |
| 375 | } |
| 376 | |
| 377 | pub fn is_literal(&self) -> bool { |
| 378 | self.parts.len() == 1 && matches!(self.parts[0], WordPart::Literal(_)) |
| 379 | } |
| 380 | } |
| 381 | |
| 382 | impl Assignment { |
| 383 | pub fn new(name: String, value: Word) -> Self { |
| 384 | Self { name, index: None, value } |
| 385 | } |
| 386 | |
| 387 | pub fn new_array(name: String, index: String, value: Word) -> Self { |
| 388 | Self { name, index: Some(index), value } |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | impl AndOrList { |
| 393 | pub fn new(first: Pipeline, rest: Vec<(AndOrOp, Pipeline)>) -> Self { |
| 394 | Self { first, rest } |
| 395 | } |
| 396 | } |
| 397 | |
| 398 | impl IfStatement { |
| 399 | pub fn new( |
| 400 | condition: Box<CompleteCommand>, |
| 401 | then_body: Vec<CompleteCommand>, |
| 402 | elif_clauses: Vec<ElifClause>, |
| 403 | else_body: Option<Vec<CompleteCommand>>, |
| 404 | ) -> Self { |
| 405 | Self { |
| 406 | condition, |
| 407 | then_body, |
| 408 | elif_clauses, |
| 409 | else_body, |
| 410 | } |
| 411 | } |
| 412 | } |
| 413 | |
| 414 | impl ElifClause { |
| 415 | pub fn new(condition: Box<CompleteCommand>, then_body: Vec<CompleteCommand>) -> Self { |
| 416 | Self { |
| 417 | condition, |
| 418 | then_body, |
| 419 | } |
| 420 | } |
| 421 | } |
| 422 | |
| 423 | impl WhileStatement { |
| 424 | pub fn new(condition: Box<CompleteCommand>, body: Vec<CompleteCommand>) -> Self { |
| 425 | Self { condition, body } |
| 426 | } |
| 427 | } |
| 428 | |
| 429 | impl ForStatement { |
| 430 | pub fn new(var_name: String, words: Vec<Word>, body: Vec<CompleteCommand>) -> Self { |
| 431 | Self { |
| 432 | var_name, |
| 433 | words, |
| 434 | body, |
| 435 | } |
| 436 | } |
| 437 | } |
| 438 | |
| 439 | impl CaseStatement { |
| 440 | pub fn new(word: Word, clauses: Vec<CaseClause>) -> Self { |
| 441 | Self { word, clauses } |
| 442 | } |
| 443 | } |
| 444 | |
| 445 | impl CaseClause { |
| 446 | pub fn new(patterns: Vec<Word>, body: Vec<CompleteCommand>) -> Self { |
| 447 | Self { patterns, body } |
| 448 | } |
| 449 | } |
| 450 | |
| 451 | impl FunctionDef { |
| 452 | pub fn new(name: String, body: Vec<CompleteCommand>) -> Self { |
| 453 | Self { name, body } |
| 454 | } |
| 455 | } |
| 456 | |
| 457 | impl Subshell { |
| 458 | pub fn new(commands: Vec<CompleteCommand>) -> Self { |
| 459 | Self { commands } |
| 460 | } |
| 461 | } |
| 462 | |
| 463 | impl CompleteCommand { |
| 464 | pub fn new(command: CommandType, background: bool) -> Self { |
| 465 | Self { command, background } |
| 466 | } |
| 467 | |
| 468 | pub fn foreground(command: CommandType) -> Self { |
| 469 | Self { |
| 470 | command, |
| 471 | background: false, |
| 472 | } |
| 473 | } |
| 474 | } |
| 475 |