rush_sh/parser/mod.rs
1//! Parser module for the Rush shell.
2//!
3//! This module provides functionality to parse tokenized shell input into an Abstract
4//! Syntax Tree (AST) that can be executed by the executor module.
5
6pub mod ast;
7mod control_flow;
8
9pub use ast::{Ast, Redirection, ShellCommand};
10use control_flow::*;
11
12use super::lexer::Token;
13
14/// Helper function to validate if a string is a valid variable name.
15/// Returns true if the name starts with a letter or underscore.
16fn is_valid_variable_name(name: &str) -> bool {
17 if let Some(first_char) = name.chars().next() {
18 first_char.is_alphabetic() || first_char == '_'
19 } else {
20 false
21 }
22}
23
24/// Helper function to create an empty body AST (a no-op that returns success).
25/// Used for empty then/else branches, empty loop bodies, and empty function bodies.
26pub(crate) fn create_empty_body_ast() -> Ast {
27 Ast::Pipeline(vec![ShellCommand {
28 args: vec!["true".to_string()],
29 redirections: Vec::new(),
30 compound: None,
31 }])
32}
33
34pub fn parse(tokens: Vec<Token>) -> Result<Ast, String> {
35 // First, try to detect and parse function definitions that span multiple lines
36 if tokens.len() >= 4
37 && let (Token::Word(_), Token::LeftParen, Token::RightParen, Token::LeftBrace) =
38 (&tokens[0], &tokens[1], &tokens[2], &tokens[3])
39 {
40 // Look for the matching RightBrace
41 // Start from the opening brace (token 3) and find its match
42 let mut brace_depth = 1; // We've already seen the opening brace at position 3
43 let mut function_end = tokens.len();
44 let mut j = 4; // Start after the opening brace
45
46 while j < tokens.len() {
47 match &tokens[j] {
48 Token::LeftBrace => {
49 brace_depth += 1;
50 j += 1;
51 }
52 Token::RightBrace => {
53 brace_depth -= 1;
54 if brace_depth == 0 {
55 function_end = j + 1; // Include the closing brace
56 break;
57 }
58 j += 1;
59 }
60 Token::If => {
61 // Skip to matching fi to avoid confusion
62 let mut if_depth = 1;
63 j += 1;
64 while j < tokens.len() && if_depth > 0 {
65 match tokens[j] {
66 Token::If => if_depth += 1,
67 Token::Fi => if_depth -= 1,
68 _ => {}
69 }
70 j += 1;
71 }
72 }
73 Token::For | Token::While | Token::Until => {
74 // Skip to matching done
75 let mut for_depth = 1;
76 j += 1;
77 while j < tokens.len() && for_depth > 0 {
78 match tokens[j] {
79 Token::For | Token::While | Token::Until => for_depth += 1,
80 Token::Done => for_depth -= 1,
81 _ => {}
82 }
83 j += 1;
84 }
85 }
86 Token::Case => {
87 // Skip to matching esac
88 j += 1;
89 while j < tokens.len() {
90 if tokens[j] == Token::Esac {
91 j += 1;
92 break;
93 }
94 j += 1;
95 }
96 }
97 _ => {
98 j += 1;
99 }
100 }
101 }
102
103 if brace_depth == 0 && function_end <= tokens.len() {
104 // We found the complete function definition
105 let function_tokens = &tokens[0..function_end];
106 let remaining_tokens = &tokens[function_end..];
107
108 let function_ast = parse_function_definition(function_tokens)?;
109
110 return if remaining_tokens.is_empty() {
111 Ok(function_ast)
112 } else {
113 // There are more commands after the function
114 let remaining_ast = parse_commands_sequentially(remaining_tokens)?;
115 Ok(Ast::Sequence(vec![function_ast, remaining_ast]))
116 };
117 }
118 }
119
120 // Also check for legacy function definition format (word with parentheses followed by brace)
121 if tokens.len() >= 2
122 && let Token::Word(ref word) = tokens[0]
123 && let Some(paren_pos) = word.find('(')
124 && word.ends_with(')')
125 && paren_pos > 0
126 && tokens[1] == Token::LeftBrace
127 {
128 return parse_function_definition(&tokens);
129 }
130
131 // Fall back to normal parsing
132 parse_commands_sequentially(&tokens)
133}
134
135/// Parses a single top-level command slice into an AST.
136///
137/// Recognizes assignments, local assignments, `return`, negation (`!`), control constructs
138/// (`if`, `case`, `for`, `while`, `until`), function definitions, and otherwise falls back to
139/// pipeline parsing to produce an `Ast` for the provided token slice.
140///
141/// # Returns
142///
143/// `Ok(Ast)` on success, `Err(String)` with a descriptive error message on failure (for example,
144/// when the slice is empty or a `!` is not followed by a command).
145///
146/// # Examples
147///
148/// ```
149/// // Note: parse_slice is a private function
150/// // This example is for documentation only
151/// ```
152fn parse_slice(tokens: &[Token]) -> Result<Ast, String> {
153 if tokens.is_empty() {
154 return Err("No commands found".to_string());
155 }
156
157 // Check if it's an assignment
158 if tokens.len() == 2 {
159 // Check for pattern: VAR= VALUE
160 if let (Token::Word(var_eq), Token::Word(value)) = (&tokens[0], &tokens[1])
161 && let Some(eq_pos) = var_eq.find('=')
162 && eq_pos > 0
163 && eq_pos < var_eq.len()
164 {
165 let var = var_eq[..eq_pos].to_string();
166 let full_value = format!("{}{}", &var_eq[eq_pos + 1..], value);
167 // Basic validation: variable name should start with letter or underscore
168 if is_valid_variable_name(&var) {
169 return Ok(Ast::Assignment {
170 var,
171 value: full_value,
172 });
173 }
174 }
175 }
176
177 // Check if it's an assignment (VAR= VALUE)
178 if tokens.len() == 2
179 && let (Token::Word(var_eq), Token::Word(value)) = (&tokens[0], &tokens[1])
180 && let Some(eq_pos) = var_eq.find('=')
181 && eq_pos > 0
182 && eq_pos == var_eq.len() - 1
183 {
184 let var = var_eq[..eq_pos].to_string();
185 // Basic validation: variable name should start with letter or underscore
186 if is_valid_variable_name(&var) {
187 return Ok(Ast::Assignment {
188 var,
189 value: value.clone(),
190 });
191 }
192 }
193
194 // Check if it's a local assignment (local VAR VALUE or local VAR= VALUE)
195 if tokens.len() == 3
196 && let (Token::Local, Token::Word(var), Token::Word(value)) =
197 (&tokens[0], &tokens[1], &tokens[2])
198 {
199 // Strip trailing = if present (handles "local var= value" format)
200 let clean_var = if var.ends_with('=') {
201 &var[..var.len() - 1]
202 } else {
203 var
204 };
205 // Basic validation: variable name should start with letter or underscore
206 if is_valid_variable_name(clean_var) {
207 return Ok(Ast::LocalAssignment {
208 var: clean_var.to_string(),
209 value: value.clone(),
210 });
211 } else {
212 return Err(format!("Invalid variable name: {}", clean_var));
213 }
214 }
215
216 // Check if it's a return statement
217 if !tokens.is_empty()
218 && tokens.len() <= 2
219 && let Token::Return = &tokens[0]
220 {
221 if tokens.len() == 1 {
222 // return (with no value, defaults to 0)
223 return Ok(Ast::Return { value: None });
224 } else if let Token::Word(word) = &tokens[1] {
225 // return value
226 return Ok(Ast::Return {
227 value: Some(word.clone()),
228 });
229 }
230 }
231
232 // Check if it's a local assignment (local VAR=VALUE)
233 if tokens.len() == 2
234 && let (Token::Local, Token::Word(var_eq)) = (&tokens[0], &tokens[1])
235 && let Some(eq_pos) = var_eq.find('=')
236 && eq_pos > 0
237 && eq_pos < var_eq.len()
238 {
239 let var = var_eq[..eq_pos].to_string();
240 let value = var_eq[eq_pos + 1..].to_string();
241 // Basic validation: variable name should start with letter or underscore
242 if is_valid_variable_name(&var) {
243 return Ok(Ast::LocalAssignment { var, value });
244 } else {
245 return Err(format!("Invalid variable name: {}", var));
246 }
247 }
248
249 // Check if it's a local assignment (local VAR) with no initial value
250 if tokens.len() == 2
251 && let (Token::Local, Token::Word(var)) = (&tokens[0], &tokens[1])
252 && !var.contains('=')
253 {
254 // Basic validation: variable name should start with letter or underscore
255 if is_valid_variable_name(var) {
256 return Ok(Ast::LocalAssignment {
257 var: var.clone(),
258 value: String::new(),
259 });
260 } else {
261 return Err(format!("Invalid variable name: {}", var));
262 }
263 }
264
265 // Check if it's an assignment (single token with =)
266 if tokens.len() == 1
267 && let Token::Word(ref word) = tokens[0]
268 && let Some(eq_pos) = word.find('=')
269 && eq_pos > 0
270 && eq_pos < word.len()
271 {
272 let var = word[..eq_pos].to_string();
273 let value = word[eq_pos + 1..].to_string();
274 // Basic validation: variable name should start with letter or underscore
275 if is_valid_variable_name(&var) {
276 return Ok(Ast::Assignment { var, value });
277 }
278 }
279
280 // Check if it's an if statement
281 if let Token::If = tokens[0] {
282 return parse_if(tokens);
283 }
284
285 // Check if it's a case statement
286 if let Token::Case = tokens[0] {
287 return parse_case(tokens);
288 }
289
290 // Check if it's a for loop
291 if let Token::For = tokens[0] {
292 return parse_for(tokens);
293 }
294
295 // Check if it's a while loop
296 if let Token::While = tokens[0] {
297 return parse_while(tokens);
298 }
299
300 // Check if it's an until loop
301 if let Token::Until = tokens[0] {
302 return parse_until(tokens);
303 }
304
305 // Check if it's a function definition
306 // Pattern: Word LeftParen RightParen LeftBrace
307 if tokens.len() >= 4
308 && let (Token::Word(word), Token::LeftParen, Token::RightParen, Token::LeftBrace) =
309 (&tokens[0], &tokens[1], &tokens[2], &tokens[3])
310 && is_valid_variable_name(word)
311 {
312 return parse_function_definition(tokens);
313 }
314
315 // Also check for function definition with parentheses in the word (legacy support)
316 if tokens.len() >= 2
317 && let Token::Word(ref word) = tokens[0]
318 && let Some(paren_pos) = word.find('(')
319 && word.ends_with(')')
320 && paren_pos > 0
321 {
322 let func_name = &word[..paren_pos];
323 if is_valid_variable_name(func_name) && tokens[1] == Token::LeftBrace {
324 return parse_function_definition(tokens);
325 }
326 }
327
328 // Check if it's a function call (word followed by arguments)
329 // For Phase 1, we'll parse as regular pipeline and handle function calls in executor
330
331 // Otherwise, parse as pipeline
332 parse_pipeline(tokens)
333}
334
335/// Helper function to parse a single command without operators.
336/// Returns the parsed AST and the number of tokens consumed.
337fn parse_single_command(tokens: &[Token]) -> Result<(Ast, usize), String> {
338 if tokens.is_empty() {
339 return Err("Expected command".to_string());
340 }
341
342 let mut i = 0;
343
344 // Skip leading newlines
345 while i < tokens.len() && tokens[i] == Token::Newline {
346 i += 1;
347 }
348
349 if i >= tokens.len() {
350 return Err("Expected command".to_string());
351 }
352
353 // Handle negation first - recursively parse what comes after !
354 if tokens[i] == Token::Bang {
355 i += 1; // Skip the bang
356
357 // Skip any newlines after the bang
358 while i < tokens.len() && tokens[i] == Token::Newline {
359 i += 1;
360 }
361
362 if i >= tokens.len() {
363 return Err("Expected command after !".to_string());
364 }
365
366 // Recursively parse the negated command
367 // IMPORTANT: This should only consume a single atomic command,
368 // not a chain with logical operators
369 let (negated_ast, consumed) = parse_single_command(&tokens[i..])?;
370 i += consumed;
371
372 // Negation only applies to the immediately following command
373 // Return immediately without consuming any following operators
374 return Ok((
375 Ast::Negation {
376 command: Box::new(negated_ast),
377 },
378 i,
379 ));
380 }
381
382 let start = i;
383
384 // Handle special constructs that have their own boundaries
385 match &tokens[i] {
386 Token::LeftParen => {
387 // Subshell - find matching paren
388 let mut paren_depth = 1;
389 i += 1;
390 while i < tokens.len() && paren_depth > 0 {
391 match tokens[i] {
392 Token::LeftParen => paren_depth += 1,
393 Token::RightParen => paren_depth -= 1,
394 _ => {}
395 }
396 i += 1;
397 }
398 if paren_depth != 0 {
399 return Err("Unmatched parenthesis".to_string());
400 }
401 }
402 Token::LeftBrace => {
403 // Command group - find matching brace
404 let mut brace_depth = 1;
405 i += 1;
406 while i < tokens.len() && brace_depth > 0 {
407 match tokens[i] {
408 Token::LeftBrace => brace_depth += 1,
409 Token::RightBrace => brace_depth -= 1,
410 _ => {}
411 }
412 i += 1;
413 }
414 if brace_depth != 0 {
415 return Err("Unmatched brace".to_string());
416 }
417 }
418 Token::If => {
419 // Find matching fi
420 let mut if_depth = 1;
421 i += 1;
422 while i < tokens.len() && if_depth > 0 {
423 match tokens[i] {
424 Token::If => if_depth += 1,
425 Token::Fi => {
426 if_depth -= 1;
427 if if_depth == 0 {
428 i += 1;
429 break;
430 }
431 }
432 _ => {}
433 }
434 i += 1;
435 }
436 }
437 Token::For | Token::While | Token::Until => {
438 // Find matching done
439 let mut loop_depth = 1;
440 i += 1;
441 while i < tokens.len() && loop_depth > 0 {
442 match tokens[i] {
443 Token::For | Token::While | Token::Until => loop_depth += 1,
444 Token::Done => {
445 loop_depth -= 1;
446 if loop_depth == 0 {
447 i += 1;
448 break;
449 }
450 }
451 _ => {}
452 }
453 i += 1;
454 }
455 }
456 Token::Case => {
457 // Find matching esac
458 i += 1;
459 while i < tokens.len() {
460 if tokens[i] == Token::Esac {
461 i += 1;
462 break;
463 }
464 i += 1;
465 }
466 }
467 _ => {
468 // Regular command/pipeline - stop at sequence separators
469 let mut brace_depth = 0;
470 let mut paren_depth = 0;
471 let mut last_was_pipe = false;
472
473 while i < tokens.len() {
474 // Check if we should stop before processing this token
475 // For logical operators (&&, ||), always stop at depth 0 after consuming at least one token
476 // For other separators, only stop after consuming at least one token
477 if i > start {
478 match &tokens[i] {
479 Token::And | Token::Or => {
480 if brace_depth == 0 && paren_depth == 0 {
481 if last_was_pipe {
482 return Err("Expected command after |".to_string());
483 }
484 break;
485 }
486 }
487 Token::Newline | Token::Semicolon => {
488 if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
489 break;
490 }
491 }
492 Token::RightBrace if brace_depth == 0 => break,
493 Token::RightParen if paren_depth == 0 => break,
494 _ => {}
495 }
496 }
497
498 // Now process the token
499 match &tokens[i] {
500 Token::LeftBrace => {
501 brace_depth += 1;
502 last_was_pipe = false;
503 }
504 Token::RightBrace => {
505 if brace_depth > 0 {
506 brace_depth -= 1;
507 last_was_pipe = false;
508 }
509 }
510 Token::LeftParen => {
511 paren_depth += 1;
512 last_was_pipe = false;
513 }
514 Token::RightParen => {
515 if paren_depth > 0 {
516 paren_depth -= 1;
517 last_was_pipe = false;
518 }
519 }
520 Token::Pipe => last_was_pipe = true,
521 Token::Word(_) => last_was_pipe = false,
522 _ => last_was_pipe = false,
523 }
524 i += 1;
525 }
526 }
527 }
528
529 let command_tokens = &tokens[start..i];
530
531 // Safety check: ensure we consumed at least one token to prevent infinite loops
532 if i == start {
533 return Err("Internal parser error: parse_single_command consumed no tokens".to_string());
534 }
535
536 let ast = parse_slice(command_tokens)?;
537 Ok((ast, i))
538}
539
540/// Helper function to parse commands with && and || operators.
541/// This builds a left-associative chain of operators.
542/// Returns the parsed AST and the number of tokens consumed.
543fn parse_next_command(tokens: &[Token]) -> Result<(Ast, usize), String> {
544 // Parse the first command
545 let (mut ast, mut i) = parse_single_command(tokens)?;
546
547 // Build left-associative chain of && and || operators iteratively
548 loop {
549 // Check if there's an && or || operator after this command
550 if i >= tokens.len() || (tokens[i] != Token::And && tokens[i] != Token::Or) {
551 break;
552 }
553
554 let operator = tokens[i].clone();
555 i += 1; // Skip the operator
556
557 // Skip any newlines after the operator
558 while i < tokens.len() && tokens[i] == Token::Newline {
559 i += 1;
560 }
561
562 if i >= tokens.len() {
563 return Err("Expected command after operator".to_string());
564 }
565
566 // Parse the next single command (without chaining)
567 let (right_ast, consumed) = parse_single_command(&tokens[i..])?;
568 i += consumed;
569
570 // Build left-associative structure
571 ast = match operator {
572 Token::And => Ast::And {
573 left: Box::new(ast),
574 right: Box::new(right_ast),
575 },
576 Token::Or => Ast::Or {
577 left: Box::new(ast),
578 right: Box::new(right_ast),
579 },
580 _ => unreachable!(),
581 };
582 }
583
584 Ok((ast, i))
585}
586
587/// Parses a slice of tokens into a top-level AST representing one or more sequential shell commands.
588///
589/// This function consumes the provided token sequence and produces an `Ast` that represents either a
590/// single command/pipeline/compound construct or a `Sequence` of commands joined by semicolons/newlines
591/// and conditional operators. It recognizes subshells, command groups, pipelines, redirections,
592/// negation (`!`), function definitions, and control-flow blocks, and composes appropriate AST nodes.
593///
594/// # Errors
595///
596/// Returns an `Err(String)` when the tokens contain a syntactic problem that prevents building a valid AST,
597/// for example unmatched braces/parentheses, an empty subshell or command group, or when no commands are present.
598///
599/// # Examples
600///
601/// ```
602/// // Note: parse_commands_sequentially is a private function
603/// // This example is for documentation only
604/// ```
605fn parse_commands_sequentially(tokens: &[Token]) -> Result<Ast, String> {
606 let mut i = 0;
607 let mut commands = Vec::new();
608
609 while i < tokens.len() {
610 // Skip whitespace and comments
611 while i < tokens.len() {
612 match &tokens[i] {
613 Token::Newline => {
614 i += 1;
615 }
616 Token::Word(word) if word.starts_with('#') => {
617 // Skip comment line
618 while i < tokens.len() && tokens[i] != Token::Newline {
619 i += 1;
620 }
621 if i < tokens.len() {
622 i += 1; // Skip the newline
623 }
624 }
625 _ => break,
626 }
627 }
628
629 if i >= tokens.len() {
630 break;
631 }
632
633 // Find the end of this command
634 let start = i;
635
636 // Check for subshell: LeftParen at start of command
637 // Must check BEFORE function definition to avoid ambiguity
638 if tokens[i] == Token::LeftParen {
639 // This is a subshell - find the matching RightParen
640 let mut paren_depth = 1;
641 let mut j = i + 1;
642
643 while j < tokens.len() && paren_depth > 0 {
644 match tokens[j] {
645 Token::LeftParen => paren_depth += 1,
646 Token::RightParen => paren_depth -= 1,
647 _ => {}
648 }
649 j += 1;
650 }
651
652 if paren_depth != 0 {
653 return Err("Unmatched parenthesis in subshell".to_string());
654 }
655
656 // Extract subshell body (tokens between parens)
657 let subshell_tokens = &tokens[i + 1..j - 1];
658
659 // Parse the subshell body recursively
660 // Empty subshells are not allowed
661 let body_ast = if subshell_tokens.is_empty() {
662 return Err("Empty subshell".to_string());
663 } else {
664 parse_commands_sequentially(subshell_tokens)?
665 };
666
667 let mut subshell_ast = Ast::Subshell {
668 body: Box::new(body_ast),
669 };
670
671 i = j; // Move past the closing paren
672
673 // Check for redirections after subshell
674 let mut redirections = Vec::new();
675 while i < tokens.len() {
676 match &tokens[i] {
677 Token::RedirOut => {
678 i += 1;
679 if i < tokens.len()
680 && let Token::Word(file) = &tokens[i]
681 {
682 redirections.push(Redirection::Output(file.clone()));
683 i += 1;
684 }
685 }
686 Token::RedirOutClobber => {
687 i += 1;
688 if i >= tokens.len() {
689 return Err("expected filename after >|".to_string());
690 }
691 if let Token::Word(file) = &tokens[i] {
692 redirections.push(Redirection::OutputClobber(file.clone()));
693 i += 1;
694 } else {
695 return Err("expected filename after >|".to_string());
696 }
697 }
698 Token::RedirIn => {
699 i += 1;
700 if i < tokens.len()
701 && let Token::Word(file) = &tokens[i]
702 {
703 redirections.push(Redirection::Input(file.clone()));
704 i += 1;
705 }
706 }
707 Token::RedirAppend => {
708 i += 1;
709 if i < tokens.len()
710 && let Token::Word(file) = &tokens[i]
711 {
712 redirections.push(Redirection::Append(file.clone()));
713 i += 1;
714 }
715 }
716 Token::RedirectFdOut(fd, file) => {
717 redirections.push(Redirection::FdOutput(*fd, file.clone()));
718 i += 1;
719 }
720 Token::RedirectFdOutClobber(fd, file) => {
721 redirections.push(Redirection::FdOutputClobber(*fd, file.clone()));
722 i += 1;
723 }
724 Token::RedirectFdIn(fd, file) => {
725 redirections.push(Redirection::FdInput(*fd, file.clone()));
726 i += 1;
727 }
728 Token::RedirectFdAppend(fd, file) => {
729 redirections.push(Redirection::FdAppend(*fd, file.clone()));
730 i += 1;
731 }
732 Token::RedirectFdDup(from_fd, to_fd) => {
733 redirections.push(Redirection::FdDuplicate(*from_fd, *to_fd));
734 i += 1;
735 }
736 Token::RedirectFdClose(fd) => {
737 redirections.push(Redirection::FdClose(*fd));
738 i += 1;
739 }
740 Token::RedirectFdInOut(fd, file) => {
741 redirections.push(Redirection::FdInputOutput(*fd, file.clone()));
742 i += 1;
743 }
744 Token::RedirHereDoc(delimiter, quoted) => {
745 redirections
746 .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
747 i += 1;
748 }
749 Token::RedirHereString(content) => {
750 redirections.push(Redirection::HereString(content.clone()));
751 i += 1;
752 }
753 _ => break,
754 }
755 }
756
757 // Check if this subshell is part of a pipeline
758 if i < tokens.len() && tokens[i] == Token::Pipe {
759 // Find end of pipeline
760 let mut end = i;
761 let mut brace_depth = 0;
762 let mut paren_depth = 0;
763 let mut last_was_pipe = true; // Started with a pipe
764 while end < tokens.len() {
765 match &tokens[end] {
766 Token::Pipe => last_was_pipe = true,
767 Token::LeftBrace => {
768 brace_depth += 1;
769 last_was_pipe = false;
770 }
771 Token::RightBrace => {
772 if brace_depth > 0 {
773 brace_depth -= 1;
774 } else {
775 break;
776 }
777 last_was_pipe = false;
778 }
779 Token::LeftParen => {
780 paren_depth += 1;
781 last_was_pipe = false;
782 }
783 Token::RightParen => {
784 if paren_depth > 0 {
785 paren_depth -= 1;
786 } else {
787 break;
788 }
789 last_was_pipe = false;
790 }
791 Token::Newline | Token::Semicolon => {
792 if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
793 break;
794 }
795 }
796 Token::Word(_) => last_was_pipe = false,
797 _ => {}
798 }
799 end += 1;
800 }
801
802 let pipeline_ast = parse_pipeline(&tokens[start..end])?;
803 commands.push(pipeline_ast);
804 i = end;
805 continue;
806 }
807
808 // If not part of a pipeline, apply redirections to the subshell itself
809 if !redirections.is_empty() {
810 subshell_ast = Ast::Pipeline(vec![ShellCommand {
811 args: Vec::new(),
812 redirections,
813 compound: Some(Box::new(subshell_ast)),
814 }]);
815 }
816
817 // Handle operators after subshell (&&, ||, ;, newline)
818 if i < tokens.len() && (tokens[i] == Token::And || tokens[i] == Token::Or) {
819 let operator = tokens[i].clone();
820 i += 1; // Skip the operator
821
822 // Skip any newlines after the operator
823 while i < tokens.len() && tokens[i] == Token::Newline {
824 i += 1;
825 }
826
827 // Parse only the next command (not the entire remaining sequence)
828 let (right_ast, consumed) = parse_next_command(&tokens[i..])?;
829 i += consumed;
830
831 // Create And or Or node
832 let combined_ast = match operator {
833 Token::And => Ast::And {
834 left: Box::new(subshell_ast),
835 right: Box::new(right_ast),
836 },
837 Token::Or => Ast::Or {
838 left: Box::new(subshell_ast),
839 right: Box::new(right_ast),
840 },
841 _ => unreachable!(),
842 };
843
844 commands.push(combined_ast);
845
846 // Skip semicolon or newline after the combined command
847 if i < tokens.len()
848 && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon)
849 {
850 i += 1;
851 }
852 continue;
853 } else {
854 commands.push(subshell_ast);
855 }
856
857 // Skip semicolon or newline after subshell
858 if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
859 i += 1;
860 }
861 continue;
862 }
863
864 // Check for command group: LeftBrace at start of command
865 if tokens[i] == Token::LeftBrace {
866 // This is a command group - find the matching RightBrace
867 let mut brace_depth = 1;
868 let mut j = i + 1;
869
870 while j < tokens.len() && brace_depth > 0 {
871 match tokens[j] {
872 Token::LeftBrace => brace_depth += 1,
873 Token::RightBrace => brace_depth -= 1,
874 _ => {}
875 }
876 j += 1;
877 }
878
879 if brace_depth != 0 {
880 return Err("Unmatched brace in command group".to_string());
881 }
882
883 // Extract group body (tokens between braces)
884 let group_tokens = &tokens[i + 1..j - 1];
885
886 // Parse the group body recursively
887 // Empty groups are not allowed
888 let body_ast = if group_tokens.is_empty() {
889 return Err("Empty command group".to_string());
890 } else {
891 parse_commands_sequentially(group_tokens)?
892 };
893
894 let mut group_ast = Ast::CommandGroup {
895 body: Box::new(body_ast),
896 };
897
898 i = j; // Move past the closing brace
899
900 // Check for redirections after command group
901 let mut redirections = Vec::new();
902 while i < tokens.len() {
903 match &tokens[i] {
904 Token::RedirOut => {
905 i += 1;
906 if i < tokens.len()
907 && let Token::Word(file) = &tokens[i]
908 {
909 redirections.push(Redirection::Output(file.clone()));
910 i += 1;
911 }
912 }
913 Token::RedirOutClobber => {
914 i += 1;
915 if i >= tokens.len() {
916 return Err("expected filename after >|".to_string());
917 }
918 if let Token::Word(file) = &tokens[i] {
919 redirections.push(Redirection::OutputClobber(file.clone()));
920 i += 1;
921 } else {
922 return Err("expected filename after >|".to_string());
923 }
924 }
925 Token::RedirIn => {
926 i += 1;
927 if i < tokens.len()
928 && let Token::Word(file) = &tokens[i]
929 {
930 redirections.push(Redirection::Input(file.clone()));
931 i += 1;
932 }
933 }
934 Token::RedirAppend => {
935 i += 1;
936 if i < tokens.len()
937 && let Token::Word(file) = &tokens[i]
938 {
939 redirections.push(Redirection::Append(file.clone()));
940 i += 1;
941 }
942 }
943 Token::RedirectFdOut(fd, file) => {
944 redirections.push(Redirection::FdOutput(*fd, file.clone()));
945 i += 1;
946 }
947 Token::RedirectFdIn(fd, file) => {
948 redirections.push(Redirection::FdInput(*fd, file.clone()));
949 i += 1;
950 }
951 Token::RedirectFdAppend(fd, file) => {
952 redirections.push(Redirection::FdAppend(*fd, file.clone()));
953 i += 1;
954 }
955 Token::RedirectFdDup(from_fd, to_fd) => {
956 redirections.push(Redirection::FdDuplicate(*from_fd, *to_fd));
957 i += 1;
958 }
959 Token::RedirectFdClose(fd) => {
960 redirections.push(Redirection::FdClose(*fd));
961 i += 1;
962 }
963 Token::RedirectFdInOut(fd, file) => {
964 redirections.push(Redirection::FdInputOutput(*fd, file.clone()));
965 i += 1;
966 }
967 Token::RedirHereDoc(delimiter, quoted) => {
968 redirections
969 .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
970 i += 1;
971 }
972 Token::RedirHereString(content) => {
973 redirections.push(Redirection::HereString(content.clone()));
974 i += 1;
975 }
976 _ => break,
977 }
978 }
979
980 // Check if this group is part of a pipeline
981 if i < tokens.len() && tokens[i] == Token::Pipe {
982 // Find end of pipeline
983 let mut end = i;
984 let mut brace_depth = 0;
985 let mut paren_depth = 0;
986 let mut last_was_pipe = true; // Started with a pipe
987 while end < tokens.len() {
988 match &tokens[end] {
989 Token::Pipe => last_was_pipe = true,
990 Token::LeftBrace => {
991 brace_depth += 1;
992 last_was_pipe = false;
993 }
994 Token::RightBrace => {
995 if brace_depth > 0 {
996 brace_depth -= 1;
997 } else {
998 break;
999 }
1000 last_was_pipe = false;
1001 }
1002 Token::LeftParen => {
1003 paren_depth += 1;
1004 last_was_pipe = false;
1005 }
1006 Token::RightParen => {
1007 if paren_depth > 0 {
1008 paren_depth -= 1;
1009 } else {
1010 break;
1011 }
1012 last_was_pipe = false;
1013 }
1014 Token::Newline | Token::Semicolon => {
1015 if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
1016 break;
1017 }
1018 }
1019 Token::Word(_) => last_was_pipe = false,
1020 _ => {}
1021 }
1022 end += 1;
1023 }
1024
1025 let pipeline_ast = parse_pipeline(&tokens[start..end])?;
1026 commands.push(pipeline_ast);
1027 i = end;
1028 continue;
1029 }
1030
1031 // If not part of a pipeline, apply redirections to the group itself
1032 if !redirections.is_empty() {
1033 group_ast = Ast::Pipeline(vec![ShellCommand {
1034 args: Vec::new(),
1035 redirections,
1036 compound: Some(Box::new(group_ast)),
1037 }]);
1038 }
1039
1040 // Handle operators after group (&&, ||, ;, newline)
1041 if i < tokens.len() && (tokens[i] == Token::And || tokens[i] == Token::Or) {
1042 let operator = tokens[i].clone();
1043 i += 1; // Skip the operator
1044
1045 // Skip any newlines after the operator
1046 while i < tokens.len() && tokens[i] == Token::Newline {
1047 i += 1;
1048 }
1049
1050 // Parse only the next command (not the entire remaining sequence)
1051 let (right_ast, consumed) = parse_next_command(&tokens[i..])?;
1052 i += consumed;
1053
1054 // Create And or Or node
1055 let combined_ast = match operator {
1056 Token::And => Ast::And {
1057 left: Box::new(group_ast),
1058 right: Box::new(right_ast),
1059 },
1060 Token::Or => Ast::Or {
1061 left: Box::new(group_ast),
1062 right: Box::new(right_ast),
1063 },
1064 _ => unreachable!(),
1065 };
1066
1067 commands.push(combined_ast);
1068
1069 // Skip semicolon or newline after the combined command
1070 if i < tokens.len()
1071 && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon)
1072 {
1073 i += 1;
1074 }
1075 continue;
1076 } else {
1077 commands.push(group_ast);
1078 }
1079
1080 // Skip semicolon or newline after group
1081 if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
1082 i += 1;
1083 }
1084 continue;
1085 }
1086
1087 // Special handling for compound commands
1088 if tokens[i] == Token::If {
1089 // For if statements, find the matching fi
1090 let mut depth = 0;
1091 while i < tokens.len() {
1092 match tokens[i] {
1093 Token::If => depth += 1,
1094 Token::Fi => {
1095 depth -= 1;
1096 if depth == 0 {
1097 i += 1; // Include the fi
1098 break;
1099 }
1100 }
1101 _ => {}
1102 }
1103 i += 1;
1104 }
1105
1106 // If we didn't find a matching fi, include all remaining tokens
1107 // This handles the case where the if statement is incomplete
1108 } else if tokens[i] == Token::For {
1109 // For for loops, find the matching done
1110 let mut depth = 1; // Start at 1 because we're already inside the for
1111 i += 1; // Move past the 'for' token
1112 while i < tokens.len() {
1113 match tokens[i] {
1114 Token::For | Token::While | Token::Until => depth += 1,
1115 Token::Done => {
1116 depth -= 1;
1117 if depth == 0 {
1118 i += 1; // Include the done
1119 break;
1120 }
1121 }
1122 _ => {}
1123 }
1124 i += 1;
1125 }
1126 } else if tokens[i] == Token::While {
1127 // For while loops, find the matching done
1128 let mut depth = 1; // Start at 1 because we're already inside the while
1129 i += 1; // Move past the 'while' token
1130 while i < tokens.len() {
1131 match tokens[i] {
1132 Token::While | Token::For | Token::Until => depth += 1,
1133 Token::Done => {
1134 depth -= 1;
1135 if depth == 0 {
1136 i += 1; // Include the done
1137 break;
1138 }
1139 }
1140 _ => {}
1141 }
1142 i += 1;
1143 }
1144 } else if tokens[i] == Token::Until {
1145 // For until loops, find the matching done
1146 let mut depth = 1; // Start at 1 because we're already inside the until
1147 i += 1; // Move past the 'until' token
1148 while i < tokens.len() {
1149 match tokens[i] {
1150 Token::Until | Token::For | Token::While => depth += 1,
1151 Token::Done => {
1152 depth -= 1;
1153 if depth == 0 {
1154 i += 1; // Include the done
1155 break;
1156 }
1157 }
1158 _ => {}
1159 }
1160 i += 1;
1161 }
1162 } else if tokens[i] == Token::Case {
1163 // For case statements, find the matching esac
1164 while i < tokens.len() {
1165 if tokens[i] == Token::Esac {
1166 i += 1; // Include the esac
1167 break;
1168 }
1169 i += 1;
1170 }
1171 } else if i + 3 < tokens.len()
1172 && matches!(tokens[i], Token::Word(_))
1173 && tokens[i + 1] == Token::LeftParen
1174 && tokens[i + 2] == Token::RightParen
1175 && tokens[i + 3] == Token::LeftBrace
1176 {
1177 // This is a function definition - find the matching closing brace
1178 let mut brace_depth = 1;
1179 i += 4; // Skip to after opening brace
1180 while i < tokens.len() && brace_depth > 0 {
1181 match tokens[i] {
1182 Token::LeftBrace => brace_depth += 1,
1183 Token::RightBrace => brace_depth -= 1,
1184 _ => {}
1185 }
1186 i += 1;
1187 }
1188 } else {
1189 // Sanity check: we shouldn't be starting a command with an operator
1190 if matches!(tokens[i], Token::And | Token::Or | Token::Semicolon) {
1191 return Err(format!(
1192 "Unexpected operator at command start: {:?}",
1193 tokens[i]
1194 ));
1195 }
1196
1197 // For simple commands, stop at newline, semicolon, &&, or ||
1198 // But check if the next token after newline is a control flow keyword
1199 let mut brace_depth = 0;
1200 let mut paren_depth = 0;
1201 let mut last_was_pipe = false;
1202 while i < tokens.len() {
1203 match &tokens[i] {
1204 Token::LeftBrace => {
1205 brace_depth += 1;
1206 last_was_pipe = false;
1207 }
1208 Token::RightBrace => {
1209 if brace_depth > 0 {
1210 brace_depth -= 1;
1211 } else {
1212 break;
1213 }
1214 last_was_pipe = false;
1215 }
1216 Token::LeftParen => {
1217 paren_depth += 1;
1218 last_was_pipe = false;
1219 }
1220 Token::RightParen => {
1221 if paren_depth > 0 {
1222 paren_depth -= 1;
1223 } else {
1224 break;
1225 }
1226 last_was_pipe = false;
1227 }
1228 Token::Pipe => last_was_pipe = true,
1229 Token::Newline | Token::Semicolon | Token::And | Token::Or => {
1230 if brace_depth == 0 && paren_depth == 0 && !last_was_pipe {
1231 break;
1232 }
1233 }
1234 Token::Word(_) => last_was_pipe = false,
1235 _ => {}
1236 }
1237 i += 1;
1238 }
1239 }
1240
1241 let command_tokens = &tokens[start..i];
1242 if !command_tokens.is_empty() {
1243 // Don't try to parse orphaned else/elif/fi tokens
1244 if command_tokens.len() == 1 {
1245 match command_tokens[0] {
1246 Token::Else | Token::Elif | Token::Fi => {
1247 // Skip orphaned control flow tokens
1248 if i < tokens.len()
1249 && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon)
1250 {
1251 i += 1;
1252 }
1253 continue;
1254 }
1255 _ => {}
1256 }
1257 }
1258
1259 // Use parse_next_command to handle operators
1260 let (ast, consumed) = parse_next_command(&tokens[start..])?;
1261 i = start + consumed;
1262
1263 commands.push(ast);
1264 }
1265
1266 if i < tokens.len() && (tokens[i] == Token::Newline || tokens[i] == Token::Semicolon) {
1267 i += 1;
1268 }
1269 }
1270
1271 if commands.is_empty() {
1272 return Err("No commands found".to_string());
1273 }
1274
1275 if commands.len() == 1 {
1276 Ok(commands.into_iter().next().unwrap())
1277 } else {
1278 Ok(Ast::Sequence(commands))
1279 }
1280}
1281
1282/// Parses a sequence of tokens into an `Ast::Pipeline` representing one or more pipeline stages.
1283///
1284/// The resulting pipeline contains one `ShellCommand` per stage with collected `args`,
1285/// ordered `redirections`, and an optional `compound` (subshell or command group). Returns an
1286/// error if the tokens contain unmatched braces/parentheses, an unexpected token, or no commands.
1287///
1288/// # Examples
1289///
1290/// ```
1291/// // Note: parse_pipeline is a private function
1292/// // This example is for documentation only
1293/// ```
1294fn parse_pipeline(tokens: &[Token]) -> Result<Ast, String> {
1295 let mut commands = Vec::new();
1296 let mut current_cmd = ShellCommand::default();
1297
1298 let mut i = 0;
1299 while i < tokens.len() {
1300 let token = &tokens[i];
1301 match token {
1302 Token::LeftBrace => {
1303 // Start of command group in pipeline
1304 // Find matching RightBrace
1305 let mut brace_depth = 1;
1306 let mut j = i + 1;
1307
1308 while j < tokens.len() && brace_depth > 0 {
1309 match tokens[j] {
1310 Token::LeftBrace => brace_depth += 1,
1311 Token::RightBrace => brace_depth -= 1,
1312 _ => {}
1313 }
1314 j += 1;
1315 }
1316
1317 if brace_depth != 0 {
1318 return Err("Unmatched brace in pipeline".to_string());
1319 }
1320
1321 // Parse group body
1322 let group_tokens = &tokens[i + 1..j - 1];
1323
1324 // Empty groups are valid and equivalent to 'true'
1325 let body_ast = if group_tokens.is_empty() {
1326 create_empty_body_ast()
1327 } else {
1328 parse_commands_sequentially(group_tokens)?
1329 };
1330
1331 // Create ShellCommand with compound command group
1332 current_cmd.compound = Some(Box::new(Ast::CommandGroup {
1333 body: Box::new(body_ast),
1334 }));
1335
1336 i = j; // Move past closing brace
1337
1338 // Check for redirections after command group
1339 while i < tokens.len() {
1340 match &tokens[i] {
1341 Token::RedirOut => {
1342 i += 1;
1343 if i < tokens.len()
1344 && let Token::Word(file) = &tokens[i]
1345 {
1346 current_cmd
1347 .redirections
1348 .push(Redirection::Output(file.clone()));
1349 i += 1;
1350 }
1351 }
1352 Token::RedirOutClobber => {
1353 i += 1;
1354 if i >= tokens.len() {
1355 return Err("expected filename after >|".to_string());
1356 }
1357 if let Token::Word(file) = &tokens[i] {
1358 current_cmd
1359 .redirections
1360 .push(Redirection::OutputClobber(file.clone()));
1361 i += 1;
1362 } else {
1363 return Err("expected filename after >|".to_string());
1364 }
1365 }
1366 Token::RedirIn => {
1367 i += 1;
1368 if i < tokens.len()
1369 && let Token::Word(file) = &tokens[i]
1370 {
1371 current_cmd
1372 .redirections
1373 .push(Redirection::Input(file.clone()));
1374 i += 1;
1375 }
1376 }
1377 Token::RedirAppend => {
1378 i += 1;
1379 if i < tokens.len()
1380 && let Token::Word(file) = &tokens[i]
1381 {
1382 current_cmd
1383 .redirections
1384 .push(Redirection::Append(file.clone()));
1385 i += 1;
1386 }
1387 }
1388 Token::RedirectFdOut(fd, file) => {
1389 current_cmd
1390 .redirections
1391 .push(Redirection::FdOutput(*fd, file.clone()));
1392 i += 1;
1393 }
1394 Token::RedirectFdOutClobber(fd, file) => {
1395 current_cmd
1396 .redirections
1397 .push(Redirection::FdOutputClobber(*fd, file.clone()));
1398 i += 1;
1399 }
1400 Token::RedirectFdIn(fd, file) => {
1401 current_cmd
1402 .redirections
1403 .push(Redirection::FdInput(*fd, file.clone()));
1404 i += 1;
1405 }
1406 Token::RedirectFdAppend(fd, file) => {
1407 current_cmd
1408 .redirections
1409 .push(Redirection::FdAppend(*fd, file.clone()));
1410 i += 1;
1411 }
1412 Token::RedirectFdDup(from_fd, to_fd) => {
1413 current_cmd
1414 .redirections
1415 .push(Redirection::FdDuplicate(*from_fd, *to_fd));
1416 i += 1;
1417 }
1418 Token::RedirectFdClose(fd) => {
1419 current_cmd.redirections.push(Redirection::FdClose(*fd));
1420 i += 1;
1421 }
1422 Token::RedirectFdInOut(fd, file) => {
1423 current_cmd
1424 .redirections
1425 .push(Redirection::FdInputOutput(*fd, file.clone()));
1426 i += 1;
1427 }
1428 Token::RedirHereDoc(delimiter, quoted) => {
1429 current_cmd
1430 .redirections
1431 .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
1432 i += 1;
1433 }
1434 Token::RedirHereString(content) => {
1435 current_cmd
1436 .redirections
1437 .push(Redirection::HereString(content.clone()));
1438 i += 1;
1439 }
1440 Token::Pipe => {
1441 // End of this pipeline stage
1442 break;
1443 }
1444 _ => break,
1445 }
1446 }
1447
1448 // Stage will be pushed at next | or end of loop
1449 continue;
1450 }
1451 Token::LeftParen => {
1452 // Start of subshell in pipeline
1453 // Find matching RightParen
1454 let mut paren_depth = 1;
1455 let mut j = i + 1;
1456
1457 while j < tokens.len() && paren_depth > 0 {
1458 match tokens[j] {
1459 Token::LeftParen => paren_depth += 1,
1460 Token::RightParen => paren_depth -= 1,
1461 _ => {}
1462 }
1463 j += 1;
1464 }
1465
1466 if paren_depth != 0 {
1467 return Err("Unmatched parenthesis in pipeline".to_string());
1468 }
1469
1470 // Parse subshell body
1471 let subshell_tokens = &tokens[i + 1..j - 1];
1472
1473 // Empty subshells are valid and equivalent to 'true'
1474 let body_ast = if subshell_tokens.is_empty() {
1475 create_empty_body_ast()
1476 } else {
1477 parse_commands_sequentially(subshell_tokens)?
1478 };
1479
1480 // Create ShellCommand with compound subshell
1481 // Create ShellCommand with compound subshell
1482 current_cmd.compound = Some(Box::new(Ast::Subshell {
1483 body: Box::new(body_ast),
1484 }));
1485
1486 i = j; // Move past closing paren
1487
1488 // Check for redirections after subshell
1489 while i < tokens.len() {
1490 match &tokens[i] {
1491 Token::RedirOut => {
1492 i += 1;
1493 if i < tokens.len()
1494 && let Token::Word(file) = &tokens[i]
1495 {
1496 current_cmd
1497 .redirections
1498 .push(Redirection::Output(file.clone()));
1499 i += 1;
1500 }
1501 }
1502 Token::RedirOutClobber => {
1503 i += 1;
1504 if i >= tokens.len() {
1505 return Err("expected filename after >|".to_string());
1506 }
1507 if let Token::Word(file) = &tokens[i] {
1508 current_cmd
1509 .redirections
1510 .push(Redirection::OutputClobber(file.clone()));
1511 i += 1;
1512 } else {
1513 return Err("expected filename after >|".to_string());
1514 }
1515 }
1516 Token::RedirIn => {
1517 i += 1;
1518 if i < tokens.len()
1519 && let Token::Word(file) = &tokens[i]
1520 {
1521 current_cmd
1522 .redirections
1523 .push(Redirection::Input(file.clone()));
1524 i += 1;
1525 }
1526 }
1527 Token::RedirAppend => {
1528 i += 1;
1529 if i < tokens.len()
1530 && let Token::Word(file) = &tokens[i]
1531 {
1532 current_cmd
1533 .redirections
1534 .push(Redirection::Append(file.clone()));
1535 i += 1;
1536 }
1537 }
1538 Token::RedirectFdOut(fd, file) => {
1539 current_cmd
1540 .redirections
1541 .push(Redirection::FdOutput(*fd, file.clone()));
1542 i += 1;
1543 }
1544 Token::RedirectFdOutClobber(fd, file) => {
1545 current_cmd
1546 .redirections
1547 .push(Redirection::FdOutputClobber(*fd, file.clone()));
1548 i += 1;
1549 }
1550 Token::RedirectFdIn(fd, file) => {
1551 current_cmd
1552 .redirections
1553 .push(Redirection::FdInput(*fd, file.clone()));
1554 i += 1;
1555 }
1556 Token::RedirectFdAppend(fd, file) => {
1557 current_cmd
1558 .redirections
1559 .push(Redirection::FdAppend(*fd, file.clone()));
1560 i += 1;
1561 }
1562 Token::RedirectFdDup(from_fd, to_fd) => {
1563 current_cmd
1564 .redirections
1565 .push(Redirection::FdDuplicate(*from_fd, *to_fd));
1566 i += 1;
1567 }
1568 Token::RedirectFdClose(fd) => {
1569 current_cmd.redirections.push(Redirection::FdClose(*fd));
1570 i += 1;
1571 }
1572 Token::RedirectFdInOut(fd, file) => {
1573 current_cmd
1574 .redirections
1575 .push(Redirection::FdInputOutput(*fd, file.clone()));
1576 i += 1;
1577 }
1578 Token::RedirHereDoc(delimiter, quoted) => {
1579 current_cmd
1580 .redirections
1581 .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
1582 i += 1;
1583 }
1584 Token::RedirHereString(content) => {
1585 current_cmd
1586 .redirections
1587 .push(Redirection::HereString(content.clone()));
1588 i += 1;
1589 }
1590 Token::Pipe => {
1591 // End of this pipeline stage
1592 break;
1593 }
1594 _ => break,
1595 }
1596 }
1597
1598 // Stage will be pushed at next | or end of loop
1599 continue;
1600 }
1601 Token::Word(word) => {
1602 current_cmd.args.push(word.clone());
1603 }
1604 Token::Local => {
1605 current_cmd.args.push("local".to_string());
1606 }
1607 Token::Return => {
1608 current_cmd.args.push("return".to_string());
1609 }
1610 Token::Break => {
1611 current_cmd.args.push("break".to_string());
1612 }
1613 Token::Continue => {
1614 current_cmd.args.push("continue".to_string());
1615 }
1616 // Handle keywords as command arguments
1617 // When keywords appear in pipeline context (not at start of command),
1618 // they should be treated as regular word arguments
1619 Token::If => {
1620 current_cmd.args.push("if".to_string());
1621 }
1622 Token::Then => {
1623 current_cmd.args.push("then".to_string());
1624 }
1625 Token::Else => {
1626 current_cmd.args.push("else".to_string());
1627 }
1628 Token::Elif => {
1629 current_cmd.args.push("elif".to_string());
1630 }
1631 Token::Fi => {
1632 current_cmd.args.push("fi".to_string());
1633 }
1634 Token::Case => {
1635 current_cmd.args.push("case".to_string());
1636 }
1637 Token::In => {
1638 current_cmd.args.push("in".to_string());
1639 }
1640 Token::Esac => {
1641 current_cmd.args.push("esac".to_string());
1642 }
1643 Token::For => {
1644 current_cmd.args.push("for".to_string());
1645 }
1646 Token::While => {
1647 current_cmd.args.push("while".to_string());
1648 }
1649 Token::Until => {
1650 current_cmd.args.push("until".to_string());
1651 }
1652 Token::Do => {
1653 current_cmd.args.push("do".to_string());
1654 }
1655 Token::Done => {
1656 current_cmd.args.push("done".to_string());
1657 }
1658 Token::Pipe => {
1659 if !current_cmd.args.is_empty() || current_cmd.compound.is_some() {
1660 commands.push(current_cmd.clone());
1661 current_cmd = ShellCommand::default();
1662 }
1663 }
1664 // Basic redirections (backward compatible)
1665 Token::RedirIn => {
1666 i += 1;
1667 if i < tokens.len()
1668 && let Token::Word(ref file) = tokens[i]
1669 {
1670 current_cmd
1671 .redirections
1672 .push(Redirection::Input(file.clone()));
1673 }
1674 }
1675 Token::RedirOut => {
1676 i += 1;
1677 if i < tokens.len()
1678 && let Token::Word(ref file) = tokens[i]
1679 {
1680 current_cmd
1681 .redirections
1682 .push(Redirection::Output(file.clone()));
1683 }
1684 }
1685 Token::RedirOutClobber => {
1686 i += 1;
1687 if i >= tokens.len() {
1688 return Err("expected filename after >|".to_string());
1689 }
1690 if let Token::Word(ref file) = tokens[i] {
1691 current_cmd
1692 .redirections
1693 .push(Redirection::OutputClobber(file.clone()));
1694 } else {
1695 return Err("expected filename after >|".to_string());
1696 }
1697 }
1698 Token::RedirAppend => {
1699 i += 1;
1700 if i < tokens.len()
1701 && let Token::Word(ref file) = tokens[i]
1702 {
1703 current_cmd
1704 .redirections
1705 .push(Redirection::Append(file.clone()));
1706 }
1707 }
1708 Token::RedirHereDoc(delimiter, quoted) => {
1709 // Store delimiter and quoted flag - content will be read by executor
1710 current_cmd
1711 .redirections
1712 .push(Redirection::HereDoc(delimiter.clone(), quoted.to_string()));
1713 }
1714 Token::RedirHereString(content) => {
1715 current_cmd
1716 .redirections
1717 .push(Redirection::HereString(content.clone()));
1718 }
1719 // File descriptor redirections
1720 Token::RedirectFdIn(fd, file) => {
1721 current_cmd
1722 .redirections
1723 .push(Redirection::FdInput(*fd, file.clone()));
1724 }
1725 Token::RedirectFdOut(fd, file) => {
1726 current_cmd
1727 .redirections
1728 .push(Redirection::FdOutput(*fd, file.clone()));
1729 }
1730 Token::RedirectFdOutClobber(fd, file) => {
1731 current_cmd
1732 .redirections
1733 .push(Redirection::FdOutputClobber(*fd, file.clone()));
1734 }
1735 Token::RedirectFdAppend(fd, file) => {
1736 current_cmd
1737 .redirections
1738 .push(Redirection::FdAppend(*fd, file.clone()));
1739 }
1740 Token::RedirectFdDup(from_fd, to_fd) => {
1741 current_cmd
1742 .redirections
1743 .push(Redirection::FdDuplicate(*from_fd, *to_fd));
1744 }
1745 Token::RedirectFdClose(fd) => {
1746 current_cmd.redirections.push(Redirection::FdClose(*fd));
1747 }
1748 Token::RedirectFdInOut(fd, file) => {
1749 current_cmd
1750 .redirections
1751 .push(Redirection::FdInputOutput(*fd, file.clone()));
1752 }
1753 Token::RightParen => {
1754 // Check if this looks like a function call pattern: Word LeftParen ... RightParen
1755 // If so, treat it as a function call even if the function doesn't exist
1756 if !current_cmd.args.is_empty()
1757 && i > 0
1758 && let Token::LeftParen = tokens[i - 1]
1759 {
1760 // This looks like a function call pattern, treat as function call
1761 // For now, we'll handle this in the executor by checking if it's a function
1762 // If not a function, the executor will handle the error gracefully
1763 break;
1764 }
1765 return Err("Unexpected ) in pipeline".to_string());
1766 }
1767 Token::Newline => {
1768 // Ignore newlines in pipelines if they follow a pipe or if we are at the start of a stage
1769 if current_cmd.args.is_empty() && current_cmd.compound.is_none() {
1770 // This newline is between commands or at the start, skip it
1771 } else {
1772 break;
1773 }
1774 }
1775 Token::And | Token::Or | Token::Semicolon => {
1776 // These tokens end the current pipeline
1777 // They will be handled by parse_commands_sequentially
1778 break;
1779 }
1780 _ => {
1781 return Err(format!("Unexpected token in pipeline: {:?}", token));
1782 }
1783 }
1784 i += 1;
1785 }
1786
1787 if !current_cmd.args.is_empty() || current_cmd.compound.is_some() {
1788 commands.push(current_cmd);
1789 }
1790
1791 if commands.is_empty() {
1792 return Err("No commands found".to_string());
1793 }
1794
1795 Ok(Ast::Pipeline(commands))
1796}
1797
1798#[cfg(test)]
1799mod tests;